diff --git a/client/src/containers/Accounts/components.js b/client/src/containers/Accounts/components.js
index 1453e4fe6..0ea6e45ea 100644
--- a/client/src/containers/Accounts/components.js
+++ b/client/src/containers/Accounts/components.js
@@ -115,7 +115,7 @@ export function NormalCell({ cell: { value } }) {
export function BalanceCell({ cell }) {
const account = cell.row.original;
- return account.amount ? (
+ return account.amount !== null ? (
diff --git a/client/src/containers/Alerts/Currencies/CurrencyDeleteAlert.js b/client/src/containers/Alerts/Currencies/CurrencyDeleteAlert.js
index 48c40f7dd..2f89ee8fb 100644
--- a/client/src/containers/Alerts/Currencies/CurrencyDeleteAlert.js
+++ b/client/src/containers/Alerts/Currencies/CurrencyDeleteAlert.js
@@ -45,7 +45,13 @@ function CurrencyDeleteAlert({
});
closeAlert(name);
})
- .catch(() => {
+ .catch(({ response: { data: { errors } } }) => {
+ if (errors.find(e => e.type === 'CANNOT_DELETE_BASE_CURRENCY')) {
+ AppToaster.show({
+ intent: Intent.DANGER,
+ message: 'Cannot delete the base currency.'
+ });
+ }
closeAlert(name);
});
};
diff --git a/client/src/containers/Alerts/Users/UserActivateAlert.js b/client/src/containers/Alerts/Users/UserActivateAlert.js
index e9d98a6e8..28737e586 100644
--- a/client/src/containers/Alerts/Users/UserActivateAlert.js
+++ b/client/src/containers/Alerts/Users/UserActivateAlert.js
@@ -1,6 +1,69 @@
+import React from 'react';
+import { FormattedMessage as T, useIntl } from 'react-intl';
+import { Alert, Intent } from '@blueprintjs/core';
+import { AppToaster } from 'components';
+import { useActivateUser } from 'hooks/query';
+import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
+import withAlertActions from 'containers/Alert/withAlertActions';
+import { compose } from 'utils';
-function UserActivateAlert() {
-
-}
\ No newline at end of file
+/**
+ * User inactivate alert.
+ */
+function UserActivateAlert({
+ // #ownProps
+ name,
+
+ // #withAlertStoreConnect
+ isOpen,
+ payload: { userId },
+
+ // #withAlertActions
+ closeAlert,
+}) {
+ const { formatMessage } = useIntl();
+
+ const { mutateAsync: userActivateMutate } = useActivateUser();
+
+ const handleConfirmActivate = () => {
+ userActivateMutate(userId)
+ .then(() => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_user_has_been_activated_successfully',
+ }),
+ intent: Intent.SUCCESS,
+ });
+ closeAlert(name);
+ })
+ .catch((error) => {
+ closeAlert(name);
+ });
+ };
+
+ const handleCancel = () => {
+ closeAlert(name);
+ };
+
+ return (
+ }
+ confirmButtonText={}
+ intent={Intent.WARNING}
+ isOpen={isOpen}
+ onCancel={handleCancel}
+ onConfirm={handleConfirmActivate}
+ >
+
+
+
+
+ );
+}
+
+export default compose(
+ withAlertStoreConnect(),
+ withAlertActions,
+)(UserActivateAlert);
diff --git a/client/src/containers/Alerts/Users/UserDeleteAlert.js b/client/src/containers/Alerts/Users/UserDeleteAlert.js
index 13ccbb1da..5c6f8471b 100644
--- a/client/src/containers/Alerts/Users/UserDeleteAlert.js
+++ b/client/src/containers/Alerts/Users/UserDeleteAlert.js
@@ -40,6 +40,7 @@ function UserDeleteAlert({
}),
intent: Intent.SUCCESS,
});
+ closeAlert(name);
})
.catch(({ response: { data: { errors } } }) => {
if (errors.find(e => e.type === 'CANNOT_DELETE_LAST_USER')) {
diff --git a/client/src/containers/Alerts/Users/UserInactivateAlert.js b/client/src/containers/Alerts/Users/UserInactivateAlert.js
index 335452283..b6894ae98 100644
--- a/client/src/containers/Alerts/Users/UserInactivateAlert.js
+++ b/client/src/containers/Alerts/Users/UserInactivateAlert.js
@@ -36,9 +36,16 @@ function UserInactivateAlert({
}),
intent: Intent.SUCCESS,
});
+ closeAlert(name);
})
- .catch((error) => {
-
+ .catch(({ response: { data: { errors } } }) => {
+ if (errors.find(e => e.type === 'CANNOT.TOGGLE.ACTIVATE.AUTHORIZED.USER')) {
+ AppToaster.show({
+ message: 'You could not activate/inactivate the same authorized user.',
+ intent: Intent.DANGER,
+ });
+ }
+ closeAlert(name);
});
};
diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js
index 440802aed..f3448171a 100644
--- a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js
+++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js
@@ -1,9 +1,8 @@
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo } from 'react';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { AppToaster } from 'components';
-import { pick } from 'lodash';
import CurrencyFormContent from './CurrencyFormContent';
import { useCurrencyFormContext } from './CurrencyFormProvider';
@@ -18,6 +17,7 @@ import { compose, transformToForm } from 'utils';
const defaultInitialValues = {
currency_name: '',
currency_code: '',
+ currency_sign: '',
};
/**
@@ -59,7 +59,7 @@ function CurrencyForm({
const afterSubmit = () => {
closeDialog(dialogName);
};
-
+ // Handle the request success.
const onSuccess = ({ response }) => {
AppToaster.show({
message: formatMessage({
@@ -71,9 +71,14 @@ function CurrencyForm({
});
afterSubmit(response);
};
-
// Handle the response error.
- const onError = (errors) => {
+ const onError = ({ response: { data: { errors } } }) => {
+ if (errors.find(e => e.type === 'CURRENCY_CODE_EXISTS')) {
+ AppToaster.show({
+ message: 'The given currency code is already exists.',
+ intent: Intent.DANGER,
+ });
+ }
setSubmitting(false);
};
if (isEditMode) {
diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js
index 31c6dfe8b..556069355 100644
--- a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js
+++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js
@@ -10,6 +10,7 @@ const Schema = Yup.object().shape({
.max(4)
.required()
.label(formatMessage({ id: 'currency_code_' })),
+ currency_sign: Yup.string().required(),
});
export const CreateCurrencyFormSchema = Schema;
diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js
index d8c1dc07b..5bcba93c4 100644
--- a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js
+++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js
@@ -1,6 +1,5 @@
import React from 'react';
import { CurrencyFormProvider } from './CurrencyFormProvider';
-import { pick } from 'lodash';
import CurrencyForm from './CurrencyForm';
import withCurrencyDetail from 'containers/Currencies/withCurrencyDetail';
diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js
index 61df70c3f..c24588e34 100644
--- a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js
+++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js
@@ -1,24 +1,47 @@
import React from 'react';
-import {
- Classes,
- FormGroup,
- InputGroup,
-} from '@blueprintjs/core';
+import { Classes, FormGroup, InputGroup } from '@blueprintjs/core';
import { FastField } from 'formik';
import { FormattedMessage as T } from 'react-intl';
-import {
- ErrorMessage,
- FieldRequiredHint,
-} from 'components';
+
+import { useCurrencyFormContext } from './CurrencyFormProvider';
+import { ErrorMessage, FieldRequiredHint, ListSelect } from 'components';
import { useAutofocus } from 'hooks';
-import { inputIntent } from 'utils';
+import { inputIntent, currenciesOptions } from 'utils';
+/**
+ * Currency form fields.
+ */
export default function CurrencyFormFields() {
const currencyNameFieldRef = useAutofocus();
-
+
+ const { isEditMode } = useCurrencyFormContext();
+
return (
+
+ {({
+ form: { setFieldValue },
+ field: { value },
+ meta: { error, touched },
+ }) => (
+
+ {
+ setFieldValue('currency_code', currency.currency_code);
+ setFieldValue('currency_name', currency.name);
+ setFieldValue('currency_sign', currency.symbol);
+ }}
+ disabled={isEditMode}
+ />
+
+ )}
+
{/* ----------- Currency name ----------- */}
{({ field, field: { value }, meta: { error, touched } }) => (
@@ -38,15 +61,14 @@ export default function CurrencyFormFields() {
)}
{/* ----------- Currency Code ----------- */}
-
+
{({ field, field: { value }, meta: { error, touched } }) => (
}
+ label={}
labelInfo={}
- className={'form-group--currency-code'}
+ className={'form-group--currency-sign'}
intent={inputIntent({ error, touched })}
- helperText={}
- // inline={true}
+ helperText={}
>
diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js
index 545b86b47..aa74f8539 100644
--- a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js
+++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js
@@ -1,5 +1,5 @@
import React, { createContext } from 'react';
-import { useCurrencies, useEditCurrency, useCreateCurrency } from 'hooks/query';
+import { useEditCurrency, useCreateCurrency } from 'hooks/query';
import { DialogContent } from 'components';
const CurrencyFormContext = createContext();
@@ -7,27 +7,22 @@ const CurrencyFormContext = createContext();
/**
* Currency Form page provider.
*/
-
function CurrencyFormProvider({ isEditMode, currency, dialogName, ...props }) {
// Create and edit item currency mutations.
const { mutateAsync: createCurrencyMutate } = useCreateCurrency();
const { mutateAsync: editCurrencyMutate } = useEditCurrency();
- // fetch Currencies list.
- const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
-
// Provider state.
const provider = {
createCurrencyMutate,
editCurrencyMutate,
dialogName,
currency,
- isCurrenciesLoading,
isEditMode,
};
return (
-
+
);
diff --git a/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js b/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js
index ceadb4a8e..5bbb54cda 100644
--- a/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js
+++ b/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js
@@ -52,6 +52,7 @@ function CurrenciesDataTable({
loading={isCurrenciesLoading}
progressBarLoading={isCurrenciesLoading}
TableLoadingRenderer={TableSkeletonRows}
+ ContextMenu={ActionMenuList}
noInitialFetch={true}
payload={{
onDeleteCurrency: handleDeleteCurrency,
diff --git a/client/src/containers/Preferences/Currencies/components.js b/client/src/containers/Preferences/Currencies/components.js
index 41aa096b1..6a0a8a9c7 100644
--- a/client/src/containers/Preferences/Currencies/components.js
+++ b/client/src/containers/Preferences/Currencies/components.js
@@ -69,6 +69,7 @@ export function useCurrenciesTableColumns() {
{
Header: 'Currency sign',
width: 120,
+ accessor: 'currency_sign'
},
{
id: 'actions',
diff --git a/client/src/containers/Preferences/Users/UsersAlerts.js b/client/src/containers/Preferences/Users/UsersAlerts.js
index 73717281f..689004d2e 100644
--- a/client/src/containers/Preferences/Users/UsersAlerts.js
+++ b/client/src/containers/Preferences/Users/UsersAlerts.js
@@ -1,14 +1,14 @@
import React from 'react';
import UserDeleteAlert from 'containers/Alerts/Users/UserDeleteAlert';
import UserInactivateAlert from 'containers/Alerts/Users/UserInactivateAlert';
-// import UserActivateAlert from 'containers/Alerts/UserActivateAlert';
+import UserActivateAlert from 'containers/Alerts/Users/UserActivateAlert';
export default function UsersAlerts() {
return (
<>
- {/* */}
+
>
);
}
diff --git a/client/src/containers/Preferences/Users/UsersDataTable.js b/client/src/containers/Preferences/Users/UsersDataTable.js
index 3efcbf238..15bcf9053 100644
--- a/client/src/containers/Preferences/Users/UsersDataTable.js
+++ b/client/src/containers/Preferences/Users/UsersDataTable.js
@@ -2,6 +2,8 @@ import React, { useCallback } from 'react';
import { compose } from 'utils';
import { DataTable } from 'components';
+import { useResendInvitation } from 'hooks/query';
+import AppToaster from 'components/AppToaster';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
@@ -10,6 +12,7 @@ import withAlertActions from 'containers/Alert/withAlertActions';
import { ActionsMenu, useUsersListColumns } from './components';
import { useUsersListContext } from './UsersProvider';
+import { Intent } from '@blueprintjs/core';
/**
* Users datatable.
@@ -49,6 +52,26 @@ function UsersDataTable({
},
[openAlert]
);
+
+ const { mutateAsync: resendInviation } = useResendInvitation();
+
+ const handleResendInvitation = useCallback(
+ (user) => {
+ resendInviation(user.id).then(() => {
+ AppToaster.show({
+ message: 'User invitation has been re-sent to the user.',
+ intent: Intent.SUCCESS
+ });
+ }).catch(({ response: { data: { errors } } }) => {
+ if (errors.find(e => e.type === 'USER_RECENTLY_INVITED')) {
+ AppToaster.show({
+ message: 'This person was recently invited. No need to invite them again just yet.',
+ intent: Intent.DANGER
+ });
+ }
+ });
+ }
+ )
// Users list columns.
const columns = useUsersListColumns();
@@ -67,9 +90,10 @@ function UsersDataTable({
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditUserAction,
- onActivate: handleInactivateUser,
- onInactivate: handleActivateuser,
- onDelete: handleDeleteUser
+ onActivate: handleActivateuser,
+ onInactivate: handleInactivateUser,
+ onDelete: handleDeleteUser,
+ onResendInvitation: handleResendInvitation
}}
/>
);
diff --git a/client/src/containers/Preferences/Users/components.js b/client/src/containers/Preferences/Users/components.js
index e4235c077..0118c569f 100644
--- a/client/src/containers/Preferences/Users/components.js
+++ b/client/src/containers/Preferences/Users/components.js
@@ -25,12 +25,7 @@ function AvatarCell(row) {
*/
export function ActionsMenu({
row: { original },
- payload: {
- onEdit,
- onInactivate,
- onActivate,
- onDelete
- }
+ payload: { onEdit, onInactivate, onActivate, onDelete, onResendInvitation },
}) {
const { formatMessage } = useIntl();
@@ -44,9 +39,26 @@ export function ActionsMenu({
/>
+ {original.active ? (
+ }
+ />
+ ) : (
+ }
+ />
+ )}
+
+
+
}
/>
@@ -64,7 +76,7 @@ export function ActionsMenu({
* Status accessor.
*/
function StatusAccessor(user) {
- return !user.invite_accepted_at ? (
+ return !user.is_invite_accepted ? (
@@ -93,6 +105,10 @@ function ActionsCell(props) {
);
}
+function FullNameAccessor(user) {
+ return user.is_invite_accepted ? user.full_name : user.email;
+}
+
export const useUsersListColumns = () => {
const { formatMessage } = useIntl();
@@ -107,7 +123,7 @@ export const useUsersListColumns = () => {
{
id: 'full_name',
Header: formatMessage({ id: 'full_name' }),
- accessor: 'full_name',
+ accessor: FullNameAccessor,
width: 150,
},
{
diff --git a/client/src/hooks/query/invite.js b/client/src/hooks/query/invite.js
index aab936bf5..b1b7960ff 100644
--- a/client/src/hooks/query/invite.js
+++ b/client/src/hooks/query/invite.js
@@ -27,4 +27,14 @@ export const useInviteMetaByToken = (token, props) => {
...props
}
);
+}
+
+
+export const useResendInvitation = (props) => {
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ (userId) => apiRequest.post(`invite/resend/${userId}`),
+ props
+ )
}
\ No newline at end of file
diff --git a/client/src/hooks/query/users.js b/client/src/hooks/query/users.js
index 0298e8c0c..cf513d371 100644
--- a/client/src/hooks/query/users.js
+++ b/client/src/hooks/query/users.js
@@ -47,10 +47,30 @@ export function useInactivateUser(props) {
const queryClient = useQueryClient();
return useMutation(
- ([id, values]) => apiRequest.post(`users/${id}/inactivate`, values),
+ (userId) => apiRequest.put(`users/${userId}/inactivate`),
{
- onSuccess: (res, [id, values]) => {
- queryClient.invalidateQueries([t.USER, id]);
+ onSuccess: (res, userId) => {
+ queryClient.invalidateQueries([t.USER, userId]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
+
+
+export function useActivateUser(props) {
+ const apiRequest = useApiRequest();
+ const queryClient = useQueryClient();
+
+ return useMutation(
+ (userId) => apiRequest.put(`users/${userId}/activate`),
+ {
+ onSuccess: (res, userId) => {
+ queryClient.invalidateQueries([t.USER, userId]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js
index a87f81eba..16ff6167e 100644
--- a/client/src/lang/en/index.js
+++ b/client/src/lang/en/index.js
@@ -318,6 +318,7 @@ export default {
edit_user: 'Edit User',
edit_invite: 'Edit Invite',
inactivate_user: 'Inactivate User',
+ activate_user: 'Activate User',
delete_user: 'Delete User',
full_name: 'Full Name',
the_user_has_been_inactivated_successfully:
@@ -1033,4 +1034,6 @@ export default {
'This customer cannot be deleted as it is associated with transactions.',
this_vendor_cannot_be_deleted_as_it_is_associated_with_transactions:
'This vendor cannot be deleted as it is associated with transactions.',
+
+ currency_sign: 'Currency sign',
};
diff --git a/client/src/style/pages/Billing/BillingPage.scss b/client/src/style/pages/Billing/BillingPage.scss
index 3583e6898..7250ddaa8 100644
--- a/client/src/style/pages/Billing/BillingPage.scss
+++ b/client/src/style/pages/Billing/BillingPage.scss
@@ -14,11 +14,11 @@
}
.plan-radio,
.period-radio{
- background: transparent;
+ background: #fff;
border-color: #bbcad4;
&.is-selected{
- background: #f1f3fb;
+ background: #fff;
border-color: #0269ff
}
}
diff --git a/client/src/style/pages/Currency/CurrencyFormDialog.scss b/client/src/style/pages/Currency/CurrencyFormDialog.scss
index ba6b4d0a2..9440a1cdf 100644
--- a/client/src/style/pages/Currency/CurrencyFormDialog.scss
+++ b/client/src/style/pages/Currency/CurrencyFormDialog.scss
@@ -12,4 +12,10 @@
height: 170px;
}
}
+
+ .bp3-dialog-footer{
+ .bp3-button{
+ min-width: 75px;
+ }
+ }
}
diff --git a/client/src/utils.js b/client/src/utils.js
index 937cabf89..891916f12 100644
--- a/client/src/utils.js
+++ b/client/src/utils.js
@@ -1,5 +1,7 @@
import moment from 'moment';
import _, { castArray } from 'lodash';
+import Currencies from 'js-money/lib/currency';
+
import { Intent } from '@blueprintjs/core';
import Currency from 'js-money/lib/currency';
import accounting from 'accounting';
@@ -620,3 +622,17 @@ export const updateTableRow = (rowIndex, columnId, value) => (old) => {
export const transformGeneralSettings = (data) => {
return _.mapKeys(data, (value, key) => _.snakeCase(key));
};
+
+const getCurrenciesOptions = () => {
+ return Object.keys(Currencies).map((currencyCode) => {
+ const currency = Currencies[currencyCode];
+
+ return {
+ ...currency,
+ currency_code: currencyCode,
+ formatted_name: `${currencyCode} - ${currency.name}`,
+ };
+ })
+}
+
+export const currenciesOptions = getCurrenciesOptions();
\ No newline at end of file
diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts
index 82601b7b4..ccce9c4a8 100644
--- a/server/src/api/controllers/Accounts.ts
+++ b/server/src/api/controllers/Accounts.ts
@@ -23,12 +23,6 @@ export default class AccountsController extends BaseController {
router() {
const router = Router();
- router.post(
- '/bulk/:type(activate|inactivate)',
- [...this.bulkSelectIdsQuerySchema],
- this.validationResult,
- asyncMiddleware(this.bulkToggleActivateAccounts.bind(this))
- );
router.post(
'/:id/activate',
[...this.accountParamSchema],
@@ -77,13 +71,6 @@ export default class AccountsController extends BaseController {
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors
);
- router.delete(
- '/',
- [...this.bulkSelectIdsQuerySchema],
- this.validationResult,
- asyncMiddleware(this.deleteBulkAccounts.bind(this)),
- this.catchServiceErrors
- );
router.delete(
'/:id',
[...this.accountParamSchema],
@@ -140,13 +127,6 @@ export default class AccountsController extends BaseController {
];
}
- get bulkSelectIdsQuerySchema() {
- return [
- query('ids').isArray({ min: 1 }),
- query('ids.*').isNumeric().toInt(),
- ];
- }
-
get closingAccountSchema() {
return [
check('to_account_id').exists().isNumeric().toInt(),
@@ -293,62 +273,6 @@ export default class AccountsController extends BaseController {
}
}
- /**
- * Bulk activate/inactivate accounts.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async bulkToggleActivateAccounts(
- req: Request,
- res: Response,
- next: Function
- ) {
- const { type } = req.params;
- const { tenantId } = req;
- const { ids: accountsIds } = req.query;
-
- try {
- const isActive = type === 'activate' ? true : false;
- await this.accountsService.activateAccounts(
- tenantId,
- accountsIds,
- isActive
- );
-
- const activatedText = isActive ? 'activated' : 'inactivated';
-
- return res.status(200).send({
- ids: accountsIds,
- message: `The given accounts have been ${activatedText} successfully`,
- });
- } catch (error) {
- next(error);
- }
- }
-
- /**
- * Deletes accounts in bulk.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async deleteBulkAccounts(req: Request, res: Response, next: NextFunction) {
- const { ids: accountsIds } = req.query;
- const { tenantId } = req;
-
- try {
- await this.accountsService.deleteAccounts(tenantId, accountsIds);
-
- return res.status(200).send({
- ids: accountsIds,
- message: 'The given accounts have been deleted successfully.',
- });
- } catch (error) {
- next(error);
- }
- }
-
/**
* Retrieve accounts datatable list.
* @param {Request} req
diff --git a/server/src/api/controllers/Authentication.ts b/server/src/api/controllers/Authentication.ts
index 9e8d77824..7e344c333 100644
--- a/server/src/api/controllers/Authentication.ts
+++ b/server/src/api/controllers/Authentication.ts
@@ -7,13 +7,13 @@ import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AuthenticationService from 'services/Authentication';
import { ILoginDTO, ISystemUser, IRegisterDTO } from 'interfaces';
-import { ServiceError, ServiceErrors } from "exceptions";
+import { ServiceError, ServiceErrors } from 'exceptions';
import { DATATYPES_LENGTH } from 'data/DataTypes';
import LoginThrottlerMiddleware from 'api/middleware/LoginThrottlerMiddleware';
import config from 'config';
@Service()
-export default class AuthenticationController extends BaseController{
+export default class AuthenticationController extends BaseController {
@Inject()
authService: AuthenticationService;
@@ -116,19 +116,21 @@ export default class AuthenticationController extends BaseController{
* Country validator.
*/
countryValidator(value, { req }) {
- const { countries: { whitelist, blacklist } } = config.registration;
+ const {
+ countries: { whitelist, blacklist },
+ } = config.registration;
const foundCountry = countries.findOne('countryCode', value);
if (!foundCountry) {
throw new Error('The country code is invalid.');
}
if (
- // Focus with me! In case whitelist is not empty and the given coutry is not
+ // Focus with me! In case whitelist is not empty and the given coutry is not
// in whitelist throw the error.
- //
- // Or in case the blacklist is not empty and the given country exists
+ //
+ // Or in case the blacklist is not empty and the given country exists
// in the blacklist throw the goddamn error.
- (whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
+ (whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
(blacklist.length > 0 && blacklist.indexOf(value) !== -1)
) {
throw new Error('The country code is not supported yet.');
@@ -153,7 +155,9 @@ export default class AuthenticationController extends BaseController{
*/
get resetPasswordSchema(): ValidationChain[] {
return [
- check('password').exists().isLength({ min: 5 })
+ check('password')
+ .exists()
+ .isLength({ min: 5 })
.custom((value, { req }) => {
if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match");
@@ -168,15 +172,13 @@ export default class AuthenticationController extends BaseController{
* Send reset password validation schema.
*/
get sendResetPasswordSchema(): ValidationChain[] {
- return [
- check('email').exists().isEmail().trim().escape(),
- ];
+ return [check('email').exists().isEmail().trim().escape()];
}
/**
* Handle user login.
- * @param {Request} req
- * @param {Response} res
+ * @param {Request} req
+ * @param {Response} res
*/
async login(req: Request, res: Response, next: Function): Response {
const userDTO: ILoginDTO = this.matchedBodyData(req);
@@ -194,14 +196,16 @@ export default class AuthenticationController extends BaseController{
/**
* Organization register handler.
- * @param {Request} req
- * @param {Response} res
+ * @param {Request} req
+ * @param {Response} res
*/
async register(req: Request, res: Response, next: Function) {
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
try {
- const registeredUser: ISystemUser = await this.authService.register(registerDTO);
+ const registeredUser: ISystemUser = await this.authService.register(
+ registerDTO
+ );
return res.status(200).send({
type: 'success',
@@ -215,8 +219,8 @@ export default class AuthenticationController extends BaseController{
/**
* Send reset password handler
- * @param {Request} req
- * @param {Response} res
+ * @param {Request} req
+ * @param {Response} res
*/
async sendResetPassword(req: Request, res: Response, next: Function) {
const { email } = this.matchedBodyData(req);
@@ -226,11 +230,10 @@ export default class AuthenticationController extends BaseController{
return res.status(200).send({
code: 'SEND_RESET_PASSWORD_SUCCESS',
- message: 'The reset password message has been sent successfully.'
+ message: 'The reset password message has been sent successfully.',
});
- } catch(error) {
+ } catch (error) {
if (error instanceof ServiceError) {
-
}
next(error);
}
@@ -238,8 +241,8 @@ export default class AuthenticationController extends BaseController{
/**
* Reset password handler
- * @param {Request} req
- * @param {Response} res
+ * @param {Request} req
+ * @param {Response} res
*/
async resetPassword(req: Request, res: Response, next: Function) {
const { token } = req.params;
@@ -250,9 +253,9 @@ export default class AuthenticationController extends BaseController{
return res.status(200).send({
type: 'RESET_PASSWORD_SUCCESS',
- message: 'The password has been reset successfully.'
- })
- } catch(error) {
+ message: 'The password has been reset successfully.',
+ });
+ } catch (error) {
next(error);
}
}
@@ -262,7 +265,9 @@ export default class AuthenticationController extends BaseController{
*/
handlerErrors(error, req: Request, res: Response, next: Function) {
if (error instanceof ServiceError) {
- if (['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1) {
+ if (
+ ['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
+ ) {
return res.boom.badRequest(null, {
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
});
@@ -272,7 +277,10 @@ export default class AuthenticationController extends BaseController{
errors: [{ type: 'USER_INACTIVE', code: 200 }],
});
}
- if (error.errorType === 'TOKEN_INVALID' || error.errorType === 'TOKEN_EXPIRED') {
+ if (
+ error.errorType === 'TOKEN_INVALID' ||
+ error.errorType === 'TOKEN_EXPIRED'
+ ) {
return res.boom.badRequest(null, {
errors: [{ type: 'TOKEN_INVALID', code: 300 }],
});
@@ -303,4 +311,4 @@ export default class AuthenticationController extends BaseController{
}
next(error);
}
-};
+}
diff --git a/server/src/api/controllers/Contacts/Contacts.ts b/server/src/api/controllers/Contacts/Contacts.ts
index 5a6d79866..f2c257639 100644
--- a/server/src/api/controllers/Contacts/Contacts.ts
+++ b/server/src/api/controllers/Contacts/Contacts.ts
@@ -304,14 +304,4 @@ export default class ContactsController extends BaseController {
get specificContactSchema(): ValidationChain[] {
return [param('id').exists().isNumeric().toInt()];
}
-
- /**
- * @returns {ValidationChain[]}
- */
- get bulkContactsSchema(): ValidationChain[] {
- return [
- query('ids').isArray({ min: 1 }),
- query('ids.*').isNumeric().toInt(),
- ];
- }
}
diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts
index 31a4f50f7..d47ec60fd 100644
--- a/server/src/api/controllers/Contacts/Vendors.ts
+++ b/server/src/api/controllers/Contacts/Vendors.ts
@@ -219,28 +219,6 @@ export default class VendorsController extends ContactsController {
}
}
- /**
- * Deletes vendors in bulk.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async deleteBulkVendors(req: Request, res: Response, next: NextFunction) {
- const { ids: contactsIds } = req.query;
- const { tenantId, user } = req;
-
- try {
- await this.vendorsService.deleteBulkVendors(tenantId, contactsIds, user)
-
- return res.status(200).send({
- ids: contactsIds,
- message: 'The vendors have been deleted successfully.',
- });
- } catch (error) {
- next(error);
- }
- }
-
/**
* Retrieve vendors datatable list.
* @param {Request} req
diff --git a/server/src/api/controllers/Currencies.ts b/server/src/api/controllers/Currencies.ts
index 1c8417936..f878ceea7 100644
--- a/server/src/api/controllers/Currencies.ts
+++ b/server/src/api/controllers/Currencies.ts
@@ -49,13 +49,17 @@ export default class CurrenciesController extends BaseController {
get currencyDTOSchemaValidation(): ValidationChain[] {
return [
- check('currency_name').exists().trim().escape(),
- check('currency_code').exists().trim().escape(),
+ check('currency_name').exists().trim(),
+ check('currency_code').exists().trim(),
+ check('currency_sign').exists().trim(),
];
}
get currencyEditDTOSchemaValidation(): ValidationChain[] {
- return [check('currency_name').exists().trim().escape()];
+ return [
+ check('currency_name').exists().trim(),
+ check('currency_sign').exists().trim(),
+ ];
}
get currencyIdParamSchema(): ValidationChain[] {
@@ -84,7 +88,10 @@ export default class CurrenciesController extends BaseController {
try {
const currencies = await this.currenciesService.listCurrencies(tenantId);
- return res.status(200).send({ currencies: [...currencies] });
+
+ return res.status(200).send({
+ currencies: this.transfromToResponse(currencies),
+ });
} catch (error) {
next(error);
}
@@ -142,7 +149,7 @@ export default class CurrenciesController extends BaseController {
async editCurrency(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { id: currencyId } = req.params;
- const { body: editCurrencyDTO } = req;
+ const editCurrencyDTO = this.matchedBodyData(req);
try {
const currency = await this.currenciesService.editCurrency(
@@ -180,7 +187,22 @@ export default class CurrenciesController extends BaseController {
}
if (error.errorType === 'currency_code_exists') {
return res.boom.badRequest(null, {
- errors: [{ type: 'CURRENCY_CODE_EXISTS', code: 200 }],
+ errors: [{
+ type: 'CURRENCY_CODE_EXISTS',
+ message: 'The given currency code is already exists.',
+ code: 200,
+ }],
+ });
+ }
+ if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
+ return res.boom.badRequest(null, {
+ errors: [
+ {
+ type: 'CANNOT_DELETE_BASE_CURRENCY',
+ code: 300,
+ message: 'Cannot delete the base currency.',
+ },
+ ],
});
}
}
diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts
index 1cbcd2eb4..c30dca97e 100644
--- a/server/src/api/controllers/Expenses.ts
+++ b/server/src/api/controllers/Expenses.ts
@@ -30,12 +30,6 @@ export default class ExpensesController extends BaseController {
asyncMiddleware(this.newExpense.bind(this)),
this.catchServiceErrors
);
- router.post(
- '/publish',
- [...this.bulkSelectSchema],
- this.bulkPublishExpenses.bind(this),
- this.catchServiceErrors
- );
router.post(
'/:id/publish',
[...this.expenseParamSchema],
@@ -57,13 +51,6 @@ export default class ExpensesController extends BaseController {
asyncMiddleware(this.deleteExpense.bind(this)),
this.catchServiceErrors
);
- router.delete(
- '/',
- [...this.bulkSelectSchema],
- this.validationResult,
- asyncMiddleware(this.bulkDeleteExpenses.bind(this)),
- this.catchServiceErrors
- );
router.get(
'/',
[...this.expensesListSchema],
@@ -134,13 +121,6 @@ export default class ExpensesController extends BaseController {
return [param('id').exists().isNumeric().toInt()];
}
- get bulkSelectSchema() {
- return [
- query('ids').isArray({ min: 1 }),
- query('ids.*').isNumeric().toInt(),
- ];
- }
-
get expensesListSchema() {
return [
query('custom_view_id').optional().isNumeric().toInt(),
@@ -250,63 +230,6 @@ export default class ExpensesController extends BaseController {
}
}
- /**
- * Deletes the expenses in bulk.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async bulkDeleteExpenses(req: Request, res: Response, next: NextFunction) {
- const { tenantId, user } = req;
- const { ids: expensesIds } = req.query;
-
- try {
- await this.expensesService.deleteBulkExpenses(
- tenantId,
- expensesIds,
- user
- );
- return res.status(200).send({
- ids: expensesIds,
- message: 'The expenses have been deleted successfully.',
- });
- } catch (error) {
- next(error);
- }
- }
-
- /**
- * Publishes the given expenses in bulk.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async bulkPublishExpenses(req: Request, res: Response, next: NextFunction) {
- const { tenantId, user } = req;
- const { ids: expensesIds } = req.query;
-
- try {
- const {
- meta: { alreadyPublished, published, total },
- } = await this.expensesService.publishBulkExpenses(
- tenantId,
- expensesIds,
- user
- );
- return res.status(200).send({
- ids: expensesIds,
- message: 'The expenses have been published successfully.',
- meta: {
- alreadyPublished,
- published,
- total,
- },
- });
- } catch (error) {
- next(error);
- }
- }
-
/**
* Retrieve expneses list.
* @param {Request} req
diff --git a/server/src/api/controllers/InviteUsers.ts b/server/src/api/controllers/InviteUsers.ts
index 1a78e41c1..0b22269bd 100644
--- a/server/src/api/controllers/InviteUsers.ts
+++ b/server/src/api/controllers/InviteUsers.ts
@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi';
-import { Router, Request, Response } from 'express';
+import { Router, Request, Response, NextFunction } from 'express';
import { check, body, param } from 'express-validator';
import { IInviteUserInput } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
@@ -25,6 +25,15 @@ export default class InviteUsersController extends BaseController {
asyncMiddleware(this.sendInvite.bind(this)),
this.handleServicesError
);
+ router.post(
+ '/resend/:userId',
+ [
+ param('userId').exists().isNumeric().toInt()
+ ],
+ this.validationResult,
+ this.asyncMiddleware(this.resendInvite.bind(this)),
+ this.handleServicesError
+ );
return router;
}
@@ -67,9 +76,9 @@ export default class InviteUsersController extends BaseController {
/**
* Invite a user to the authorized user organization.
- * @param {Request} req -
- * @param {Response} res -
- * @param {NextFunction} next -
+ * @param {Request} req - Request object.
+ * @param {Response} res - Response object.
+ * @param {NextFunction} next - Next function.
*/
async sendInvite(req: Request, res: Response, next: Function) {
const { email } = req.body;
@@ -90,7 +99,29 @@ export default class InviteUsersController extends BaseController {
} catch (error) {
next(error);
}
- return res.status(200).send();
+ }
+
+ /**
+ * Resend the user invite.
+ * @param {Request} req - Request object.
+ * @param {Response} res - Response object.
+ * @param {NextFunction} next - Next function.
+ */
+ async resendInvite(req: Request, res: Response, next: NextFunction) {
+ const { tenantId, user } = req;
+ const { userId } = req.params;
+
+ try {
+ await this.inviteUsersService.resendInvite(tenantId, userId, user);
+
+ return res.status(200).send({
+ type: 'success',
+ code: 'INVITE.RESEND.SUCCESSFULLY',
+ message: 'The invite has been sent to the given email.',
+ });
+ } catch (error) {
+ next(error);
+ }
}
/**
@@ -151,38 +182,59 @@ export default class InviteUsersController extends BaseController {
if (error instanceof ServiceError) {
if (error.errorType === 'EMAIL_EXISTS') {
return res.status(400).send({
- errors: [{
- type: 'EMAIL.ALREADY.EXISTS',
- code: 100,
- message: 'Email already exists in the users.'
- }],
+ errors: [
+ {
+ type: 'EMAIL.ALREADY.EXISTS',
+ code: 100,
+ message: 'Email already exists in the users.',
+ },
+ ],
});
}
if (error.errorType === 'EMAIL_ALREADY_INVITED') {
return res.status(400).send({
- errors: [{
- type: 'EMAIL.ALREADY.INVITED',
- code: 200,
- message: 'Email already invited.',
- }],
+ errors: [
+ {
+ type: 'EMAIL.ALREADY.INVITED',
+ code: 200,
+ message: 'Email already invited.',
+ },
+ ],
});
}
if (error.errorType === 'INVITE_TOKEN_INVALID') {
return res.status(400).send({
- errors: [{
- type: 'INVITE.TOKEN.INVALID',
- code: 300,
- message: 'Invite token is invalid, please try another one.',
- }],
+ errors: [
+ {
+ type: 'INVITE.TOKEN.INVALID',
+ code: 300,
+ message: 'Invite token is invalid, please try another one.',
+ },
+ ],
});
}
if (error.errorType === 'PHONE_NUMBER_EXISTS') {
return res.status(400).send({
- errors: [{
- type: 'PHONE_NUMBER.EXISTS',
- code: 400,
- message: 'Phone number is already invited, please try another unique one.'
- }],
+ errors: [
+ {
+ type: 'PHONE_NUMBER.EXISTS',
+ code: 400,
+ message:
+ 'Phone number is already invited, please try another unique one.',
+ },
+ ],
+ });
+ }
+ if (error.errorType === 'USER_RECENTLY_INVITED') {
+ return res.status(400).send({
+ errors: [
+ {
+ type: 'USER_RECENTLY_INVITED',
+ code: 500,
+ message:
+ 'This person was recently invited. No need to invite them again just yet.',
+ },
+ ],
});
}
}
diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts
index 22aa6b20a..73b6acf8e 100644
--- a/server/src/api/controllers/ItemCategories.ts
+++ b/server/src/api/controllers/ItemCategories.ts
@@ -40,13 +40,6 @@ export default class ItemsCategoriesController extends BaseController {
asyncMiddleware(this.newCategory.bind(this)),
this.handlerServiceError
);
- router.delete(
- '/',
- [...this.categoriesBulkValidationSchema],
- this.validationResult,
- asyncMiddleware(this.bulkDeleteCategories.bind(this)),
- this.handlerServiceError
- );
router.delete(
'/:id',
[...this.specificCategoryValidationSchema],
@@ -103,16 +96,6 @@ export default class ItemsCategoriesController extends BaseController {
];
}
- /**
- * Validate items categories bulk actions.
- */
- get categoriesBulkValidationSchema() {
- return [
- query('ids').isArray({ min: 1 }),
- query('ids.*').isNumeric().toInt(),
- ];
- }
-
/**
* Validate items categories schema.
*/
@@ -263,31 +246,6 @@ export default class ItemsCategoriesController extends BaseController {
}
}
- /**
- * Bulk delete the given item categories.
- * @param {Request} req -
- * @param {Response} res -
- * @return {Response}
- */
- async bulkDeleteCategories(req: Request, res: Response, next: NextFunction) {
- const itemCategoriesIds = req.query.ids;
- const { tenantId, user } = req;
-
- try {
- await this.itemCategoriesService.deleteItemCategories(
- tenantId,
- itemCategoriesIds,
- user
- );
- return res.status(200).send({
- ids: itemCategoriesIds,
- message: 'The item categories have been deleted successfully.',
- });
- } catch (error) {
- next(error);
- }
- }
-
/**
* Handles service error.
* @param {Error} error
diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts
index 697addee5..67df445ef 100644
--- a/server/src/api/controllers/Items.ts
+++ b/server/src/api/controllers/Items.ts
@@ -172,17 +172,6 @@ export default class ItemsController extends BaseController {
return [param('id').exists().isNumeric().toInt()];
}
- /**
- * Bulk select validation schema.
- * @return {ValidationChain[]}
- */
- get validateBulkSelectSchema(): ValidationChain[] {
- return [
- query('ids').isArray({ min: 1 }),
- query('ids.*').isNumeric().toInt(),
- ];
- }
-
/**
* Validate list query schema
*/
diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts
index 9b38ed1b4..eec84227a 100644
--- a/server/src/api/controllers/ManualJournals.ts
+++ b/server/src/api/controllers/ManualJournals.ts
@@ -35,13 +35,6 @@ export default class ManualJournalsController extends BaseController {
asyncMiddleware(this.getManualJournal.bind(this)),
this.catchServiceErrors.bind(this)
);
- router.post(
- '/publish',
- [...this.manualJournalIdsSchema],
- this.validationResult,
- asyncMiddleware(this.publishManualJournals.bind(this)),
- this.catchServiceErrors.bind(this)
- );
router.post(
'/:id/publish',
[...this.manualJournalParamSchema],
@@ -63,13 +56,6 @@ export default class ManualJournalsController extends BaseController {
asyncMiddleware(this.deleteManualJournal.bind(this)),
this.catchServiceErrors.bind(this)
);
- router.delete(
- '/',
- [...this.manualJournalIdsSchema],
- this.validationResult,
- asyncMiddleware(this.deleteBulkManualJournals.bind(this)),
- this.catchServiceErrors.bind(this)
- );
router.post(
'/',
[...this.manualJournalValidationSchema],
@@ -87,16 +73,6 @@ export default class ManualJournalsController extends BaseController {
return [param('id').exists().isNumeric().toInt()];
}
- /**
- * Manual journal bulk ids validation schema.
- */
- get manualJournalIdsSchema() {
- return [
- query('ids').isArray({ min: 1 }),
- query('ids.*').isNumeric().toInt(),
- ];
- }
-
/**
* Manual journal DTO schema.
*/
@@ -277,34 +253,6 @@ export default class ManualJournalsController extends BaseController {
}
}
- /**
- * Publish the given manual journals in bulk.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async publishManualJournals(req: Request, res: Response, next: NextFunction) {
- const { tenantId } = req;
- const { ids: manualJournalsIds } = req.query;
-
- try {
- const {
- meta: { alreadyPublished, published, total },
- } = await this.manualJournalsService.publishManualJournals(
- tenantId,
- manualJournalsIds
- );
-
- return res.status(200).send({
- ids: manualJournalsIds,
- message: 'The manual journals have been published successfully.',
- meta: { alreadyPublished, published, total },
- });
- } catch (error) {
- next(error);
- }
- }
-
/**
* Delete the given manual journal.
* @param {Request} req
@@ -330,35 +278,6 @@ export default class ManualJournalsController extends BaseController {
}
}
- /**
- * Deletes manual journals in bulk.
- * @param {Request} req
- * @param {Response} res
- * @param {NextFunction} next
- */
- async deleteBulkManualJournals(
- req: Request,
- res: Response,
- next: NextFunction
- ) {
- const { tenantId } = req;
- const { ids: manualJournalsIds } = req.query;
-
- try {
- await this.manualJournalsService.deleteManualJournals(
- tenantId,
- manualJournalsIds
- );
-
- return res.status(200).send({
- ids: manualJournalsIds,
- message: 'Manual journal have been delete successfully.',
- });
- } catch (error) {
- next(error);
- }
- }
-
/**
* Retrieve manual journals list.
* @param {Request} req
diff --git a/server/src/database/migrations/20200419171451_create_currencies_table.js b/server/src/database/migrations/20200419171451_create_currencies_table.js
index c2d847140..4d06717b9 100644
--- a/server/src/database/migrations/20200419171451_create_currencies_table.js
+++ b/server/src/database/migrations/20200419171451_create_currencies_table.js
@@ -4,6 +4,7 @@ exports.up = function(knex) {
table.increments();
table.string('currency_name').index();
table.string('currency_code', 4).index();
+ table.string('currency_sign').index();
table.timestamps();
}).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/interfaces/Currency.ts b/server/src/interfaces/Currency.ts
index 112d62ac5..2bcfc5620 100644
--- a/server/src/interfaces/Currency.ts
+++ b/server/src/interfaces/Currency.ts
@@ -3,14 +3,17 @@
export interface ICurrencyDTO {
currencyName: string,
currencyCode: string,
+ currencySign: string,
};
export interface ICurrencyEditDTO {
currencyName: string,
+ currencySign: string,
}
export interface ICurrency {
id: number,
currencyName: string,
currencyCode: string,
+ currencySign: string,
createdAt: Date,
updatedAt: Date,
};
diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts
index c3bbe9946..0d1a11a1f 100644
--- a/server/src/interfaces/Expenses.ts
+++ b/server/src/interfaces/Expenses.ts
@@ -84,24 +84,6 @@ export interface IExpensesService {
authorizedUser: ISystemUser
): Promise;
- deleteBulkExpenses(
- tenantId: number,
- expensesIds: number[],
- authorizedUser: ISystemUser
- ): Promise;
-
- publishBulkExpenses(
- tenantId: number,
- expensesIds: number[],
- authorizedUser: ISystemUser
- ): Promise<{
- meta: {
- alreadyPublished: number;
- published: number;
- total: number,
- },
- }>;
-
getExpensesList(
tenantId: number,
expensesFilter: IExpensesFilter
diff --git a/server/src/interfaces/User.ts b/server/src/interfaces/User.ts
index 162d1b879..7df463af5 100644
--- a/server/src/interfaces/User.ts
+++ b/server/src/interfaces/User.ts
@@ -1,45 +1,67 @@
import { Model } from 'objection';
export interface ISystemUser extends Model {
- id: number,
- firstName: string,
- lastName: string,
- active: boolean,
- password: string,
- email: string,
- phoneNumber: string,
+ id: number;
+ firstName: string;
+ lastName: string;
+ active: boolean;
+ password: string;
+ email: string;
+ phoneNumber: string;
- roleId: number,
- tenantId: number,
+ roleId: number;
+ tenantId: number;
- inviteAcceptAt: Date,
- lastLoginAt: Date,
- deletedAt: Date,
+ inviteAcceptAt: Date;
+ lastLoginAt: Date;
+ deletedAt: Date;
- createdAt: Date,
- updatedAt: Date,
+ createdAt: Date;
+ updatedAt: Date;
}
export interface ISystemUserDTO {
- firstName: string,
- lastName: string,
- password: string,
- phoneNumber: string,
- active: boolean,
- email: string,
+ firstName: string;
+ lastName: string;
+ password: string;
+ phoneNumber: string;
+ active: boolean;
+ email: string;
}
export interface IInviteUserInput {
- firstName: string,
- lastName: string,
- phoneNumber: string,
- password: string,
-};
+ firstName: string;
+ lastName: string;
+ phoneNumber: string;
+ password: string;
+}
export interface IUserInvite {
- id: number,
- email: string,
- token: string,
- tenantId: number,
- createdAt?: Date,
-}
\ No newline at end of file
+ id: number;
+ email: string;
+ token: string;
+ tenantId: number;
+ userId: number;
+ createdAt?: Date;
+}
+
+export interface IInviteUserService {
+ acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise;
+ resendInvite(
+ tenantId: number,
+ userId: number,
+ authorizedUser: ISystemUser
+ ): Promise<{
+ invite: IUserInvite;
+ }>;
+ sendInvite(
+ tenantId: number,
+ email: string,
+ authorizedUser: ISystemUser
+ ): Promise<{
+ invite: IUserInvite;
+ }>;
+ checkInvite(
+ token: string
+ ): Promise<{ inviteToken: IUserInvite; orgName: object }>;
+}
diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts
index f7d6856bc..9770f767b 100644
--- a/server/src/services/Authentication/index.ts
+++ b/server/src/services/Authentication/index.ts
@@ -177,7 +177,8 @@ export default class AuthenticationService implements IAuthenticationService {
...omit(registerDTO, 'country'),
active: true,
password: hashedPassword,
- tenant_id: tenant.id,
+ tenantId: tenant.id,
+ inviteAcceptedAt: moment().format('YYYY-MM-DD'),
});
// Triggers `onRegister` event.
this.eventDispatcher.dispatch(events.auth.register, {
diff --git a/server/src/services/Currencies/CurrenciesService.ts b/server/src/services/Currencies/CurrenciesService.ts
index 18e7702ed..adb94cfa5 100644
--- a/server/src/services/Currencies/CurrenciesService.ts
+++ b/server/src/services/Currencies/CurrenciesService.ts
@@ -16,7 +16,8 @@ import TenancyService from 'services/Tenancy/TenancyService';
const ERRORS = {
CURRENCY_NOT_FOUND: 'currency_not_found',
CURRENCY_CODE_EXISTS: 'currency_code_exists',
- BASE_CURRENCY_INVALID: 'BASE_CURRENCY_INVALID'
+ BASE_CURRENCY_INVALID: 'BASE_CURRENCY_INVALID',
+ CANNOT_DELETE_BASE_CURRENCY: 'CANNOT_DELETE_BASE_CURRENCY'
};
@Service()
@@ -131,6 +132,7 @@ export default class CurrenciesService implements ICurrenciesService {
tenantId,
currencyDTO,
});
+ // Validate currency code uniquiness.
await this.validateCurrencyCodeUniquiness(
tenantId,
currencyDTO.currencyCode
@@ -174,6 +176,22 @@ export default class CurrenciesService implements ICurrenciesService {
return currency;
}
+ /**
+ * Validate cannot delete base currency.
+ * @param {number} tenantId
+ * @param {string} currencyCode
+ */
+ validateCannotDeleteBaseCurrency(tenantId: number, currencyCode: string) {
+ const settings = this.tenancy.settings(tenantId);
+ const baseCurrency = settings.get({
+ group: 'organization',
+ key: 'base_currency',
+ });
+ if (baseCurrency === currencyCode) {
+ throw new ServiceError(ERRORS.CANNOT_DELETE_BASE_CURRENCY);
+ }
+ }
+
/**
* Delete the given currency code.
* @param {number} tenantId
@@ -192,6 +210,9 @@ export default class CurrenciesService implements ICurrenciesService {
await this.getCurrencyByCodeOrThrowError(tenantId, currencyCode);
+ // Validate currency code not equals base currency.
+ await this.validateCannotDeleteBaseCurrency(tenantId, currencyCode);
+
await Currency.query().where('currency_code', currencyCode).delete();
this.logger.info('[currencies] the currency deleted successfully.', {
tenantId,
@@ -207,10 +228,20 @@ export default class CurrenciesService implements ICurrenciesService {
public async listCurrencies(tenantId: number): Promise {
const { Currency } = this.tenancy.models(tenantId);
+ const settings = this.tenancy.settings(tenantId);
+ const baseCurrency = settings.get({
+ group: 'organization',
+ key: 'base_currency',
+ });
+
const currencies = await Currency.query().onBuild((query) => {
query.orderBy('createdAt', 'ASC');
});
- return currencies;
+ const formattedCurrencies = currencies.map((currency) => ({
+ isBaseCurrency: baseCurrency === currency.currencyCode,
+ ...currency,
+ }));
+ return formattedCurrencies;
}
/**
diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts
index 659cdbc35..13c239952 100644
--- a/server/src/services/Expenses/ExpensesService.ts
+++ b/server/src/services/Expenses/ExpensesService.ts
@@ -570,48 +570,6 @@ export default class ExpensesService implements IExpensesService {
});
}
- /**
- * Deletes the given expenses in bulk.
- * @param {number} tenantId
- * @param {number[]} expensesIds
- * @param {ISystemUser} authorizedUser
- */
- public async deleteBulkExpenses(
- tenantId: number,
- expensesIds: number[],
- authorizedUser: ISystemUser
- ) {
- const {
- expenseRepository,
- expenseEntryRepository,
- } = this.tenancy.repositories(tenantId);
-
- // Retrieve olds expenses.
- const oldExpenses = await this.getExpensesOrThrowError(
- tenantId,
- expensesIds
- );
-
- this.logger.info('[expense] trying to delete the given expenses.', {
- tenantId,
- expensesIds,
- });
- await expenseEntryRepository.deleteWhereIn('expenseId', expensesIds);
- await expenseRepository.deleteWhereIdIn(expensesIds);
-
- this.logger.info('[expense] the given expenses deleted successfully.', {
- tenantId,
- expensesIds,
- });
- // Triggers `onExpenseBulkDeleted` event.
- this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, {
- tenantId,
- expensesIds,
- oldExpenses,
- authorizedUser,
- });
- }
-
/**
* Filters the not published expenses.
* @param {IExpense[]} expenses -
@@ -629,74 +587,6 @@ export default class ExpensesService implements IExpensesService {
return expenses.filter((expense) => expense.publishedAt);
}
- /**
- * Deletes the given expenses in bulk.
- * @param {number} tenantId
- * @param {number[]} expensesIds
- * @param {ISystemUser} authorizedUser
- */
- public async publishBulkExpenses(
- tenantId: number,
- expensesIds: number[],
- authorizedUser: ISystemUser
- ): Promise<{
- meta: {
- alreadyPublished: number;
- published: number;
- total: number,
- },
- }> {
- const oldExpenses = await this.getExpensesOrThrowError(
- tenantId,
- expensesIds
- );
- const { expenseRepository } = this.tenancy.repositories(tenantId);
-
- // Filters the not published expenses.
- const notPublishedExpenses = this.getNonePublishedExpenses(oldExpenses);
-
- // Filters the published expenses.
- const publishedExpenses = this.getPublishedExpenses(oldExpenses);
-
- // Mappes the not-published expenses to get id.
- const notPublishedExpensesIds = map(notPublishedExpenses, 'id');
-
- if (notPublishedExpensesIds.length > 0) {
- this.logger.info('[expense] trying to publish the given expenses.', {
- tenantId,
- expensesIds,
- });
- await expenseRepository.whereIdInPublish(notPublishedExpensesIds);
-
- this.logger.info(
- '[expense] the given expenses ids published successfully.',
- { tenantId, expensesIds }
- );
- }
- // Retrieve the new expenses after modification.
- const expenses = await expenseRepository.findWhereIn(
- 'id',
- expensesIds,
- 'categories'
- );
- // Triggers `onExpenseBulkDeleted` event.
- this.eventDispatcher.dispatch(events.expenses.onBulkPublished, {
- tenantId,
- expensesIds,
- oldExpenses,
- expenses,
- authorizedUser,
- });
-
- return {
- meta: {
- alreadyPublished: publishedExpenses.length,
- published: notPublishedExpenses.length,
- total: oldExpenses.length,
- },
- };
- }
-
/**
* Retrieve expenses datatable lsit.
* @param {number} tenantId
diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts
index 7b4ef5526..5fb30b244 100644
--- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts
+++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts
@@ -95,8 +95,7 @@ export default class BalanceSheetStatementService
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
+ group: 'organization', key: 'base_currency',
});
const filter = {
diff --git a/server/src/services/InviteUsers/constants.ts b/server/src/services/InviteUsers/constants.ts
new file mode 100644
index 000000000..1e0516917
--- /dev/null
+++ b/server/src/services/InviteUsers/constants.ts
@@ -0,0 +1,11 @@
+
+
+export const ERRORS = {
+ EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED',
+ INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID',
+ PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
+ USER_NOT_FOUND: 'USER_NOT_FOUND',
+ EMAIL_EXISTS: 'EMAIL_EXISTS',
+ EMAIL_NOT_EXISTS: 'EMAIL_NOT_EXISTS',
+ USER_RECENTLY_INVITED: 'USER_RECENTLY_INVITED',
+};
\ No newline at end of file
diff --git a/server/src/services/InviteUsers/index.ts b/server/src/services/InviteUsers/index.ts
index d903f6334..6fbf79d23 100644
--- a/server/src/services/InviteUsers/index.ts
+++ b/server/src/services/InviteUsers/index.ts
@@ -12,17 +12,17 @@ import { hashPassword } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages';
import events from 'subscribers/events';
-import { ISystemUser, IInviteUserInput, IUserInvite } from 'interfaces';
+import {
+ ISystemUser,
+ IInviteUserInput,
+ IUserInvite,
+ IInviteUserService,
+} from 'interfaces';
import TenantsManagerService from 'services/Tenancy/TenantsManager';
+import { ERRORS } from './constants';
-const ERRORS = {
- EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED',
- INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID',
- PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
- EMAIL_EXISTS: 'EMAIL_EXISTS'
-};
@Service()
-export default class InviteUserService {
+export default class InviteUserService implements IInviteUserService {
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@@ -41,60 +41,6 @@ export default class InviteUserService {
@Inject()
tenantsManager: TenantsManagerService;
- /**
- * Accept the received invite.
- * @param {string} token
- * @param {IInviteUserInput} inviteUserInput
- * @throws {ServiceErrors}
- * @returns {Promise}
- */
- async acceptInvite(
- token: string,
- inviteUserInput: IInviteUserInput
- ): Promise {
- const { systemUserRepository } = this.sysRepositories;
-
- // Retrieve the invite token or throw not found error.
- const inviteToken = await this.getInviteOrThrowError(token);
-
- // Validates the user phone number.
- await this.validateUserPhoneNumber(inviteUserInput);
-
- this.logger.info('[aceept_invite] trying to hash the user password.');
- const hashedPassword = await hashPassword(inviteUserInput.password);
-
- this.logger.info('[accept_invite] trying to update user details.');
- const user = await systemUserRepository.findOneByEmail(inviteToken.email);
-
- // Sets the invited user details after invite accepting.
- const systemUserOper = systemUserRepository.create(
- {
- ...inviteUserInput,
- email: inviteToken.email,
- tenantId: inviteToken.tenantId,
- active: 1,
- inviteAcceptedAt: moment().format('YYYY-MM-DD'),
- password: hashedPassword,
- },
- );
-
- this.logger.info('[accept_invite] trying to delete the given token.');
- const deleteInviteTokenOper = Invite.query()
- .where('token', inviteToken.token)
- .delete();
-
- // Await all async operations.
- const [systemUser] = await Promise.all([
- systemUserOper,
- deleteInviteTokenOper,
- ]);
- // Triggers `onUserAcceptInvite` event.
- this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
- inviteToken,
- user: systemUser,
- });
- }
-
/**
* Sends invite mail to the given email from the given tenant and user.
* @param {number} tenantId -
@@ -110,27 +56,120 @@ export default class InviteUserService {
): Promise<{
invite: IUserInvite;
}> {
- // Throw error in case user email exists.
- await this.throwErrorIfUserEmailExists(email);
+ const { systemUserRepository } = this.sysRepositories;
- // Throws service error in case the user already invited.
- await this.throwErrorIfUserInvited(email);
+ // Validates the given email not exists on the storage.
+ await this.validateUserEmailNotExists(email);
- this.logger.info('[send_invite] trying to store invite token.');
+ this.logger.info('[invite] trying to store user with email and tenant.', {
+ email,
+ });
+ const user = await systemUserRepository.create({
+ email,
+ tenantId,
+ active: 1,
+ });
+
+ this.logger.info('[invite] trying to store invite token.', { email });
const invite = await Invite.query().insert({
email,
- tenant_id: authorizedUser.tenantId,
+ tenantId: authorizedUser.tenantId,
+ userId: user.id,
token: uniqid(),
});
- this.logger.info(
- '[send_invite] trying to store user with email and tenant.'
- );
// Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
invite,
authorizedUser,
tenantId,
+ user,
+ });
+ return { invite };
+ }
+
+ /**
+ * Accept the received invite.
+ * @param {string} token
+ * @param {IInviteUserInput} inviteUserInput
+ * @throws {ServiceErrors}
+ * @returns {Promise}
+ */
+ public async acceptInvite(
+ token: string,
+ inviteUserInput: IInviteUserInput
+ ): Promise {
+ const { systemUserRepository } = this.sysRepositories;
+
+ // Retrieve the invite token or throw not found error.
+ const inviteToken = await this.getInviteTokenOrThrowError(token);
+
+ // Validates the user phone number.
+ await this.validateUserPhoneNumberNotExists(inviteUserInput.phoneNumber);
+
+ this.logger.info('[invite] trying to hash the user password.');
+ const hashedPassword = await hashPassword(inviteUserInput.password);
+
+ this.logger.info('[invite] trying to update user details.');
+ const user = await systemUserRepository.findOneByEmail(inviteToken.email);
+
+ // Sets the invited user details after invite accepting.
+ const systemUser = await systemUserRepository.update(
+ {
+ ...inviteUserInput,
+ inviteAcceptedAt: moment().format('YYYY-MM-DD'),
+ password: hashedPassword,
+ },
+ { id: inviteToken.userId }
+ );
+ // Clear invite token by the given user id.
+ await this.clearInviteTokensByUserId(inviteToken.userId);
+
+ // Triggers `onUserAcceptInvite` event.
+ this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
+ inviteToken,
+ user: systemUser,
+ });
+ }
+
+ /**
+ * Re-send user invite.
+ * @param tenantId
+ * @param {string} email
+ * @return {Promise<{ invite: IUserInvite }>}
+ */
+ public async resendInvite(
+ tenantId: number,
+ userId: number,
+ authorizedUser: ISystemUser
+ ): Promise<{
+ invite: IUserInvite;
+ }> {
+ // Retrieve the user by id or throw not found service error.
+ const user = this.getUserByIdOrThrowError(userId);
+
+ // Validate invite user active
+ await this.validateInviteUserNotActive(tenantId, userId);
+
+ // Clear all invite tokens of the given user id.
+ await this.clearInviteTokensByUserId(userId);
+
+ this.logger.info('[invite] trying to store invite token.', {
+ userId,
+ tenantId,
+ });
+ const invite = await Invite.query().insert({
+ email: user.email,
+ tenantId,
+ userId,
+ token: uniqid(),
+ });
+ // Triggers `onUserSendInvite` event.
+ this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
+ invite,
+ authorizedUser,
+ tenantId,
+ user,
});
return { invite };
}
@@ -143,7 +182,7 @@ export default class InviteUserService {
public async checkInvite(
token: string
): Promise<{ inviteToken: IUserInvite; orgName: object }> {
- const inviteToken = await this.getInviteOrThrowError(token);
+ const inviteToken = await this.getInviteTokenOrThrowError(token);
// Find the tenant that associated to the given token.
const tenant = await Tenant.query().findById(inviteToken.tenantId);
@@ -166,13 +205,48 @@ export default class InviteUserService {
return { inviteToken, orgName };
}
+ /**
+ * Validate the given user has no active invite token.
+ * @param {number} tenantId
+ * @param {number} userId - User id.
+ */
+ private async validateInviteUserNotActive(tenantId: number, userId: number) {
+ // Retrieve the invite token or throw not found error.
+ const inviteTokens = await Invite.query()
+ .modify('notExpired')
+ .where('user_id', userId);
+
+ // Throw the error if the one invite tokens is still active.
+ if (inviteTokens.length > 0) {
+ this.logger.info('[invite] email is already invited.', {
+ userId,
+ tenantId,
+ });
+ throw new ServiceError(ERRORS.USER_RECENTLY_INVITED);
+ }
+ }
+
+ /**
+ * Retrieve the given user by id or throw not found service error.
+ * @param {number} userId - User id.
+ */
+ private async getUserByIdOrThrowError(userId: number) {
+ const { systemUserRepository } = this.sysRepositories;
+ const user = await systemUserRepository.findOneById(userId);
+
+ // Throw if the user not found.
+ if (!user) {
+ throw new ServiceError(ERRORS.USER_NOT_FOUND);
+ }
+ return user;
+ }
+
/**
* Throws error in case the given user email not exists on the storage.
* @param {string} email
+ * @throws {ServiceError}
*/
- private async throwErrorIfUserEmailExists(
- email: string
- ): Promise {
+ private async validateUserEmailNotExists(email: string): Promise {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.findOneByEmail(email);
@@ -181,31 +255,21 @@ export default class InviteUserService {
}
}
- /**
- * Throws service error if the user already invited.
- * @param {string} email -
- */
- private async throwErrorIfUserInvited(
- email: string,
- ): Promise {
- const inviteToken = await Invite.query().findOne('email', email);
-
- if (inviteToken) {
- throw new ServiceError(ERRORS.EMAIL_ALREADY_INVITED);
- }
- }
-
/**
* Retrieve invite model from the given token or throw error.
* @param {string} token - Then given token string.
* @throws {ServiceError}
* @returns {Invite}
*/
- private async getInviteOrThrowError(token: string): Promise {
- const inviteToken = await Invite.query().findOne('token', token);
+ private async getInviteTokenOrThrowError(
+ token: string
+ ): Promise {
+ const inviteToken = await Invite.query()
+ .modify('notExpired')
+ .findOne('token', token);
if (!inviteToken) {
- this.logger.info('[aceept_invite] the invite token is invalid.');
+ this.logger.info('[invite] the invite token is invalid.');
throw new ServiceError(ERRORS.INVITE_TOKEN_INVALID);
}
return inviteToken;
@@ -215,15 +279,24 @@ export default class InviteUserService {
* Validate the given user email and phone number uniquine.
* @param {IInviteUserInput} inviteUserInput
*/
- private async validateUserPhoneNumber(
- inviteUserInput: IInviteUserInput
+ private async validateUserPhoneNumberNotExists(
+ phoneNumber: string
): Promise {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.findOneByPhoneNumber(
- inviteUserInput.phoneNumber
+ phoneNumber
);
if (foundUser) {
throw new ServiceError(ERRORS.PHONE_NUMBER_EXISTS);
}
}
+
+ /**
+ * Clear invite tokens of the given user id.
+ * @param {number} userId - User id.
+ */
+ private async clearInviteTokensByUserId(userId: number) {
+ this.logger.info('[invite] trying to delete the given token.');
+ await Invite.query().where('user_id', userId).delete();
+ }
}
diff --git a/server/src/services/Users/UsersService.ts b/server/src/services/Users/UsersService.ts
index 22e623689..0d3bf4eef 100644
--- a/server/src/services/Users/UsersService.ts
+++ b/server/src/services/Users/UsersService.ts
@@ -113,7 +113,7 @@ export default class UsersService {
// Throw serivce error if the user is already activated.
this.throwErrorIfUserActive(user);
- await systemUserRepository.activateUser(userId);
+ await systemUserRepository.activateById(userId);
}
/**
diff --git a/server/src/system/migrations/20200422225247_create_user_invites_table.js b/server/src/system/migrations/20200422225247_create_user_invites_table.js
index abb723b20..42028ccc7 100644
--- a/server/src/system/migrations/20200422225247_create_user_invites_table.js
+++ b/server/src/system/migrations/20200422225247_create_user_invites_table.js
@@ -5,6 +5,7 @@ exports.up = function(knex) {
table.string('email').index();
table.string('token').unique().index();
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
+ table.integer('user_id').unsigned().index().references('id').inTable('users');
table.datetime('created_at');
});
};
diff --git a/server/src/system/models/Invite.js b/server/src/system/models/Invite.js
index bb226f88b..2de707735 100644
--- a/server/src/system/models/Invite.js
+++ b/server/src/system/models/Invite.js
@@ -1,4 +1,5 @@
import SystemModel from 'system/models/SystemModel';
+import moment from 'moment';
export default class UserInvite extends SystemModel {
/**
@@ -14,4 +15,16 @@ export default class UserInvite extends SystemModel {
get timestamps() {
return ['createdAt'];
}
+
+ /**
+ * Model modifiers.
+ */
+ static get modifiers() {
+ return {
+ notExpired(query) {
+ const comp = moment().subtract(24, 'hours').toMySqlDateTime();
+ query.where('created_at', '>=', comp);
+ }
+ }
+ }
}
diff --git a/server/src/system/models/SystemUser.js b/server/src/system/models/SystemUser.js
index a1f5bed74..a314240e2 100644
--- a/server/src/system/models/SystemUser.js
+++ b/server/src/system/models/SystemUser.js
@@ -29,7 +29,21 @@ export default class SystemUser extends SystemModel {
* Virtual attributes.
*/
static get virtualAttributes() {
- return ['fullName'];
+ return ['fullName', 'isDeleted', 'isInviteAccepted'];
+ }
+
+ /**
+ *
+ */
+ get isDeleted() {
+ return !!this.deletedAt;
+ }
+
+ /**
+ *
+ */
+ get isInviteAccepted() {
+ return !!this.inviteAcceptedAt;
}
/**