diff --git a/server/package.json b/server/package.json index 3a7aa61a6..953825d3c 100644 --- a/server/package.json +++ b/server/package.json @@ -41,6 +41,7 @@ "express-validator": "^6.2.0", "helmet": "^3.21.0", "i18n": "^0.8.5", + "is-my-json-valid": "^2.20.5", "jsonwebtoken": "^8.5.1", "knex": "^0.20.3", "knex-cleaner": "^1.3.0", diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index 8ae7d2561..eb9851823 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -1,30 +1,21 @@ import { Router, Request, Response, NextFunction } from 'express'; -import { check, validationResult, param, query } from 'express-validator'; -import { difference } from 'lodash'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import JournalPoster from 'services/Accounting/JournalPoster'; -import { - mapViewRolesToConditionals, - mapFilterRolesToDynamicFilter, -} from 'lib/ViewRolesBuilder'; -import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterViews, - DynamicFilterFilterRoles, -} from 'lib/DynamicFilter'; -import BaseController from './BaseController'; -import { IAccountDTO, IAccount } from 'interfaces'; -import { ServiceError } from 'exceptions'; -import AccountsService from 'services/Accounts/AccountsService'; +import { check, param, query } from 'express-validator'; import { Service, Inject } from 'typedi'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseController from 'api/controllers/BaseController'; +import AccountsService from 'services/Accounts/AccountsService'; +import { IAccountDTO, IAccountsFilter } from 'interfaces'; +import { ServiceError } from 'exceptions'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class AccountsController extends BaseController{ - @Inject() accountsService: AccountsService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor method. */ @@ -39,13 +30,15 @@ export default class AccountsController extends BaseController{ '/:id/activate', [ ...this.accountParamSchema, ], - asyncMiddleware(this.activateAccount.bind(this)) + asyncMiddleware(this.activateAccount.bind(this)), + this.catchServiceErrors, ); router.post( '/:id/inactivate', [ ...this.accountParamSchema, ], - asyncMiddleware(this.inactivateAccount.bind(this)) + asyncMiddleware(this.inactivateAccount.bind(this)), + this.catchServiceErrors, ); router.post( '/:id', [ @@ -53,47 +46,48 @@ export default class AccountsController extends BaseController{ ...this.accountParamSchema, ], this.validationResult, - asyncMiddleware(this.editAccount.bind(this)) + asyncMiddleware(this.editAccount.bind(this)), + this.catchServiceErrors, ); router.post( '/', [ ...this.accountDTOSchema, ], this.validationResult, - asyncMiddleware(this.newAccount.bind(this)) + asyncMiddleware(this.newAccount.bind(this)), + this.catchServiceErrors, ); router.get( '/:id', [ ...this.accountParamSchema, ], this.validationResult, - asyncMiddleware(this.getAccount.bind(this)) + asyncMiddleware(this.getAccount.bind(this)), + this.catchServiceErrors, + ); + router.get( + '/', [ + ...this.accountsListSchema, + ], + this.validationResult, + asyncMiddleware(this.getAccountsList.bind(this)), + this.dynamicListService.handlerErrorsToResponse, + this.catchServiceErrors, ); - // // router.get( - // // '/', [ - // // ...this.accountsListSchema - // // ], - // // asyncMiddleware(this.getAccountsList.handler) - // // ); - router.delete( '/:id', [ ...this.accountParamSchema ], this.validationResult, - asyncMiddleware(this.deleteAccount.bind(this)) + asyncMiddleware(this.deleteAccount.bind(this)), + this.catchServiceErrors, ); router.delete( '/', this.bulkDeleteSchema, - asyncMiddleware(this.deleteBulkAccounts.bind(this)) + asyncMiddleware(this.deleteBulkAccounts.bind(this)), + this.catchServiceErrors, ); - // router.post( - // '/:id/transfer_account/:toAccount', - // this.transferToAnotherAccount.validation, - // asyncMiddleware(this.transferToAnotherAccount.handler) - // ); - return router; } @@ -142,11 +136,7 @@ export default class AccountsController extends BaseController{ */ get accountsListSchema() { return [ - query('display_type').optional().isIn(['tree', 'flat']), - query('account_types').optional().isArray(), - query('account_types.*').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(), - query('stringified_filter_roles').optional().isJSON(), query('column_sort_by').optional(), @@ -176,10 +166,7 @@ export default class AccountsController extends BaseController{ return res.status(200).send({ id: account.id }); } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + next(error); } } @@ -198,10 +185,7 @@ export default class AccountsController extends BaseController{ const account = await this.accountsService.editAccount(tenantId, accountId, accountDTO); return res.status(200).send({ id: account.id }); } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + next(error); } } @@ -220,10 +204,7 @@ export default class AccountsController extends BaseController{ return res.status(200).send({ account }); } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + next(error); } } @@ -240,14 +221,8 @@ export default class AccountsController extends BaseController{ try { await this.accountsService.deleteAccount(tenantId, accountId); return res.status(200).send({ id: accountId }); - } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - - - } - next(); + next(error); } } @@ -265,10 +240,7 @@ export default class AccountsController extends BaseController{ await this.accountsService.activateAccount(tenantId, accountId, true); return res.status(200).send({ id: accountId }); } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + next(error); } } @@ -287,10 +259,7 @@ export default class AccountsController extends BaseController{ return res.status(200).send({ id: accountId }); } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + next(error); } } @@ -310,10 +279,7 @@ export default class AccountsController extends BaseController{ await this.accountsService.activateAccounts(tenantId, accountsIds, isActive) return res.status(200).send({ ids: accountsIds }); } catch (error) { - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + next(error); } } @@ -332,211 +298,117 @@ export default class AccountsController extends BaseController{ return res.status(200).send({ ids: accountsIds }); } catch (error) { - console.log(error); + next(error); + } + } - if (error instanceof ServiceError) { - this.transformServiceErrorToResponse(res, error); - } - next(); + /** + * Retrieve accounts datatable list. + * @param {Request} req + * @param {Response} res + */ + async getAccountsList(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const filter: IAccountsFilter = { + filterRoles: [], + sortOrder: 'asc', + columnSortBy: 'name', + ...this.matchedQueryData(req), + }; + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); + } + try { + const accounts = await this.accountsService.getAccountsList(tenantId, filter); + return res.status(200).send({ accounts }); + } catch (error) { + next(error); } } /** * Transforms service errors to response. + * @param {Error} + * @param {Request} req * @param {Response} res * @param {ServiceError} error */ - transformServiceErrorToResponse(res: Response, error: ServiceError) { - console.log(error.errorType); - if (error.errorType === 'account_not_found') { - return res.boom.notFound( - 'The given account not found.', { - errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }] } - ); - } - if (error.errorType === 'account_name_not_unqiue') { - return res.boom.badRequest( - 'The given account not unique.', - { errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }], } - ); - } - if (error.errorType === 'account_type_not_found') { - return res.boom.badRequest( - 'The given account type not found.', { - errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] } - ); - } - if (error.errorType === 'account_type_not_allowed_to_changed') { - return res.boom.badRequest( - 'Not allowed to change account type of the account.', - { errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }] } - ); - } - if (error.errorType === 'parent_account_not_found') { - return res.boom.badRequest( - 'The parent account not found.', - { errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }] }, - ); - } - if (error.errorType === 'parent_has_different_type') { - return res.boom.badRequest( - 'The parent account has different type.', - { errors: [{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 }] } - ); - } - if (error.errorType === 'account_code_not_unique') { - return res.boom.badRequest( - 'The given account code is not unique.', - { errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] } - ); - } - if (error.errorType === 'account_has_children') { - return res.boom.badRequest( - 'You could not delete account has children.', - { errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 700 }] } - ); - } - if (error.errorType === 'account_has_associated_transactions') { - return res.boom.badRequest( - 'You could not delete account has associated transactions.', - { errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 }] } - ); - } - if (error.errorType === 'account_predefined') { - return res.boom.badRequest( - 'You could not delete predefined account', - { errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }] } - ); - } - if (error.errorType === 'accounts_not_found') { - return res.boom.notFound( - 'Some of the given accounts not found.', - { errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }] }, - ); - } - if (error.errorType === 'predefined_accounts') { - return res.boom.badRequest( - 'Some of the given accounts are predefined.', - { errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] } - ); + catchServiceErrors(error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'account_not_found') { + return res.boom.notFound( + 'The given account not found.', + { errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }] } + ); + } + if (error.errorType === 'account_name_not_unqiue') { + return res.boom.badRequest( + 'The given account not unique.', + { errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }], } + ); + } + if (error.errorType === 'account_type_not_found') { + return res.boom.badRequest( + 'The given account type not found.', { + errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] + } + ); + } + if (error.errorType === 'account_type_not_allowed_to_changed') { + return res.boom.badRequest( + 'Not allowed to change account type of the account.', + { errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }] } + ); + } + if (error.errorType === 'parent_account_not_found') { + return res.boom.badRequest( + 'The parent account not found.', + { errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }] }, + ); + } + if (error.errorType === 'parent_has_different_type') { + return res.boom.badRequest( + 'The parent account has different type.', + { errors: [{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 }] } + ); + } + if (error.errorType === 'account_code_not_unique') { + return res.boom.badRequest( + 'The given account code is not unique.', + { errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] } + ); + } + if (error.errorType === 'account_has_children') { + return res.boom.badRequest( + 'You could not delete account has children.', + { errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 700 }] } + ); + } + if (error.errorType === 'account_has_associated_transactions') { + return res.boom.badRequest( + 'You could not delete account has associated transactions.', + { errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 }] } + ); + } + if (error.errorType === 'account_predefined') { + return res.boom.badRequest( + 'You could not delete predefined account', + { errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }] } + ); + } + if (error.errorType === 'accounts_not_found') { + return res.boom.notFound( + 'Some of the given accounts not found.', + { errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }] }, + ); + } + if (error.errorType === 'predefined_accounts') { + return res.boom.badRequest( + 'Some of the given accounts are predefined.', + { errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] } + ); + } } + next(error) } - - - // /** - // * Retrieve accounts list. - // */ - // getAccountsList(req, res) { - // const validationErrors = validationResult(req); - - // if (!validationErrors.isEmpty()) { - // return res.boom.badData(null, { - // code: 'validation_error', - // ...validationErrors, - // }); - // } - // const filter = { - // display_type: 'flat', - // account_types: [], - // filter_roles: [], - // sort_order: 'asc', - // ...req.query, - // }; - // if (filter.stringified_filter_roles) { - // filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - // } - // const { Resource, Account, View } = req.models; - // const errorReasons = []; - - // const accountsResource = await Resource.query() - // .remember() - // .where('name', 'accounts') - // .withGraphFetched('fields') - // .first(); - - // if (!accountsResource) { - // return res.status(400).send({ - // errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }], - // }); - // } - // const resourceFieldsKeys = accountsResource.fields.map((c) => c.key); - - // const view = await View.query().onBuild((builder) => { - // if (filter.custom_view_id) { - // builder.where('id', filter.custom_view_id); - // } else { - // builder.where('favourite', true); - // } - // // builder.where('resource_id', accountsResource.id); - // builder.withGraphFetched('roles.field'); - // builder.withGraphFetched('columns'); - // builder.first(); - - // builder.remember(); - // }); - // const dynamicFilter = new DynamicFilter(Account.tableName); - - // if (filter.column_sort_by) { - // if (resourceFieldsKeys.indexOf(filter.column_sort_by) === -1) { - // errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); - // } - // const sortByFilter = new DynamicFilterSortBy( - // filter.column_sort_by, - // filter.sort_order - // ); - // dynamicFilter.setFilter(sortByFilter); - // } - // // View roles. - // if (view && view.roles.length > 0) { - // const viewFilter = new DynamicFilterViews( - // mapViewRolesToConditionals(view.roles), - // view.rolesLogicExpression - // ); - // if (!viewFilter.validateFilterRoles()) { - // errorReasons.push({ - // type: 'VIEW.LOGIC.EXPRESSION.INVALID', - // code: 400, - // }); - // } - // dynamicFilter.setFilter(viewFilter); - // } - // // Filter roles. - // if (filter.filter_roles.length > 0) { - // // Validate the accounts resource fields. - // const filterRoles = new DynamicFilterFilterRoles( - // mapFilterRolesToDynamicFilter(filter.filter_roles), - // accountsResource.fields - // ); - // dynamicFilter.setFilter(filterRoles); - - // if (filterRoles.validateFilterRoles().length > 0) { - // errorReasons.push({ - // type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', - // code: 500, - // }); - // } - // } - // if (errorReasons.length > 0) { - // return res.status(400).send({ errors: errorReasons }); - // } - - // const accounts = await Account.query().onBuild((builder) => { - // builder.modify('filterAccountTypes', filter.account_types); - // builder.withGraphFetched('type'); - // builder.withGraphFetched('balance'); - - // dynamicFilter.buildQuery()(builder); - // }); - // return res.status(200).send({ - // accounts: - // filter.display_type === 'tree' - // ? Account.toNestedArray(accounts) - // : accounts, - // ...(view - // ? { - // customViewId: view.id, - // } - // : {}), - // }); - // } }; diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts index 73b5ced36..519c12bea 100644 --- a/server/src/api/controllers/BaseController.ts +++ b/server/src/api/controllers/BaseController.ts @@ -1,17 +1,28 @@ -import { Response, Request } from 'express'; +import { Response, Request, NextFunction } from 'express'; import { matchedData, validationResult } from "express-validator"; import { camelCase, omit } from "lodash"; import { mapKeysDeep } from 'utils' export default class BaseController { + private dataToCamelCase(data) { + return mapKeysDeep(data, (v, k) => camelCase(k)); + } + matchedBodyData(req: Request, options: any = {}) { const data = matchedData(req, { locations: ['body'], includeOptionals: true, ...omit(options, ['locations']), // override any propery except locations. }); - return mapKeysDeep(data, (v, k) => camelCase(k)); + return this.dataToCamelCase(data); + } + + matchedQueryData(req: Request) { + const data = matchedData(req, { + locations: ['query'], + }); + return this.dataToCamelCase(data); } validationResult(req: Request, res: Response, next: NextFunction) { diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index 9e49ad7ea..e97683327 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -1,10 +1,10 @@ import { Request, Response, Router, NextFunction } from 'express'; import { Service, Inject } from 'typedi'; -import { check } from 'express-validator'; +import { check, query } from 'express-validator'; import ContactsController from 'api/controllers/Contacts/Contacts'; import VendorsService from 'services/Contacts/VendorsService'; import { ServiceError } from 'exceptions'; -import { IVendorNewDTO, IVendorEditDTO } from 'interfaces'; +import { IVendorNewDTO, IVendorEditDTO, IVendorsFilter } from 'interfaces'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; @Service() @@ -52,6 +52,12 @@ export default class VendorsController extends ContactsController { this.validationResult, asyncMiddleware(this.getVendor.bind(this)) ); + router.get('/', [ + ...this.vendorsListSchema, + ], + this.validationResult, + asyncMiddleware(this.getVendorsList.bind(this)), + ); return router; } @@ -64,6 +70,22 @@ export default class VendorsController extends ContactsController { ]; } + /** + * Vendors datatable list validation schema. + */ + get vendorsListSchema() { + return [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ]; + } + /** * Creates a new vendor. * @param {Request} req @@ -192,4 +214,24 @@ export default class VendorsController extends ContactsController { next(error); } } + + /** + * Retrieve vendors datatable list. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getVendorsList(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const vendorsFilter: IVendorsFilter = { + filterRoles: [], + ...this.matchedBodyData(req), + }; + try { + const vendors = await this.vendorsService.getVendorsList(tenantId, vendorsFilter); + return res.status(200).send({ vendors }); + } catch (error) { + + } + } } \ No newline at end of file diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index f0f3d4a8c..c2afcce26 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -6,12 +6,16 @@ import BaseController from "api/controllers/BaseController"; import ExpensesService from "services/Expenses/ExpensesService"; import { IExpenseDTO } from 'interfaces'; import { ServiceError } from "exceptions"; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class ExpensesController extends BaseController { @Inject() expensesService: ExpensesService; + @Inject() + dynamicListService: DynamicListingService; + /** * Express router. */ @@ -23,19 +27,22 @@ export default class ExpensesController extends BaseController { ...this.expenseDTOSchema, ], this.validationResult, - asyncMiddleware(this.newExpense.bind(this)) + asyncMiddleware(this.newExpense.bind(this)), + this.catchServiceErrors, ); router.post('/publish', [ ...this.bulkSelectSchema, ], - this.bulkPublishExpenses.bind(this) + this.bulkPublishExpenses.bind(this), + this.catchServiceErrors, ); router.post( '/:id/publish', [ ...this.expenseParamSchema, ], this.validationResult, - asyncMiddleware(this.publishExpense.bind(this)) + asyncMiddleware(this.publishExpense.bind(this)), + this.catchServiceErrors, ); router.post( '/:id', [ @@ -44,19 +51,28 @@ export default class ExpensesController extends BaseController { ], this.validationResult, asyncMiddleware(this.editExpense.bind(this)), + this.catchServiceErrors, ); router.delete( '/:id', [ ...this.expenseParamSchema, ], this.validationResult, - asyncMiddleware(this.deleteExpense.bind(this)) + asyncMiddleware(this.deleteExpense.bind(this)), + this.catchServiceErrors, ); router.delete('/', [ ...this.bulkSelectSchema, ], this.validationResult, - asyncMiddleware(this.bulkDeleteExpenses.bind(this)) + asyncMiddleware(this.bulkDeleteExpenses.bind(this)), + this.catchServiceErrors, + ); + router.get( + '/', + asyncMiddleware(this.getExpensesList.bind(this)), + this.dynamicListService.handlerErrorsToResponse, + this.catchServiceErrors, ); return router; } @@ -118,9 +134,6 @@ export default class ExpensesController extends BaseController { const expense = await this.expensesService.newExpense(tenantId, expenseDTO, user); return res.status(200).send({ id: expense.id }); } catch (error) { - if (error instanceof ServiceError) { - this.serviceErrorsTransformer(res, error); - } next(error); } } @@ -140,9 +153,6 @@ export default class ExpensesController extends BaseController { await this.expensesService.editExpense(tenantId, expenseId, expenseDTO, user); return res.status(200).send({ id: expenseId }); } catch (error) { - if (error instanceof ServiceError) { - this.serviceErrorsTransformer(res, error); - } next(error) } } @@ -161,9 +171,6 @@ export default class ExpensesController extends BaseController { await this.expensesService.deleteExpense(tenantId, expenseId) return res.status(200).send({ id: expenseId }); } catch (error) { - if (error instanceof ServiceError) { - this.serviceErrorsTransformer(res, error); - } next(error) } } @@ -182,9 +189,6 @@ export default class ExpensesController extends BaseController { await this.expensesService.publishExpense(tenantId, expenseId) return res.status(200).send({ }); } catch (error) { - if (error instanceof ServiceError) { - this.serviceErrorsTransformer(req, error); - } next(error); } } @@ -203,9 +207,6 @@ export default class ExpensesController extends BaseController { await this.expensesService.deleteBulkExpenses(tenantId, expensesIds); return res.status(200).send({ ids: expensesIds }); } catch (error) { - if (error instanceof ServiceError) { - this.serviceErrorsTransformer(req, error); - } next(error); } } @@ -218,9 +219,23 @@ export default class ExpensesController extends BaseController { await this.expensesService.publishBulkExpenses(tenantId,); return res.status(200).send({}); } catch (error) { - if (error instanceof ServiceError) { - this.serviceErrorsTransformer(req, error); - } + next(error); + } + } + + + async getExpensesList(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const filter = { + filterRoles: [], + sortOrder: 'asc', + ...this.matchedQueryData(req), + }; + + try { + const expenses = await this.expensesService.getExpensesList(tenantId, filter); + return res.status(200).send({ expenses }); + } catch (error) { next(error); } } @@ -230,36 +245,39 @@ export default class ExpensesController extends BaseController { * @param {Response} res * @param {ServiceError} error */ - serviceErrorsTransformer(res, error: ServiceError) { - if (error.errorType === 'expense_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'EXPENSE_NOT_FOUND' }], - }); - } - if (error.errorType === 'total_amount_equals_zero') { - return res.boom.badRequest(null, { - errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO' }], - }); - } - if (error.errorType === 'payment_account_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', }], - }); - } - if (error.errorType === 'some_expenses_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 200 }] - }) - } - if (error.errorType === 'payment_account_has_invalid_type') { - return res.boom.badRequest(null, { - errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE' }], - }); - } - if (error.errorType === 'expenses_account_has_invalid_type') { - return res.boom.badRequest(null, { - errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE' }] - }); + catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'expense_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'EXPENSE_NOT_FOUND' }], + }); + } + if (error.errorType === 'total_amount_equals_zero') { + return res.boom.badRequest(null, { + errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO' }], + }); + } + if (error.errorType === 'payment_account_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', }], + }); + } + if (error.errorType === 'some_expenses_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 200 }] + }) + } + if (error.errorType === 'payment_account_has_invalid_type') { + return res.boom.badRequest(null, { + errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE' }], + }); + } + if (error.errorType === 'expenses_account_has_invalid_type') { + return res.boom.badRequest(null, { + errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE' }] + }); + } } + next(error); } } \ No newline at end of file diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index f56a93073..90245f6a0 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -8,14 +8,6 @@ import { difference } from 'lodash'; import { Service } from 'typedi'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import validateMiddleware from 'api/middleware/validateMiddleware'; -import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterFilterRoles, -} from 'lib/DynamicFilter'; -import { - mapFilterRolesToDynamicFilter, -} from 'lib/ViewRolesBuilder'; import { IItemCategory, IItemCategoryOTD } from 'interfaces'; import BaseController from 'api/controllers/BaseController'; @@ -336,67 +328,7 @@ export default class ItemsCategoriesController extends BaseController { * @return {Response} */ async getList(req: Request, res: Response) { - const { Resource, ItemCategory } = req.models; - const categoriesResource = await Resource.query() - .where('name', 'items_categories') - .withGraphFetched('fields') - .first(); - - if (!categoriesResource) { - return res.status(400).send({ - errors: [{ type: 'ITEMS.CATEGORIES.RESOURCE.NOT.FOUND', code: 200 }], - }); - } - const filter = { - column_sort_order: '', - sort_order: '', - filter_roles: [], - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const errorReasons = []; - const resourceFieldsKeys = categoriesResource.fields.map((c) => c.key); - const dynamicFilter = new DynamicFilter(ItemCategory.tableName); - - // Dynamic filter with filter roles. - if (filter.filter_roles.length > 0) { - // Validate the accounts resource fields. - const filterRoles = new DynamicFilterFilterRoles( - mapFilterRolesToDynamicFilter(filter.filter_roles), - categoriesResource.fields, - ); - categoriesResource.setFilter(filterRoles); - - if (filterRoles.validateFilterRoles().length > 0) { - errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 }); - } - } - // Dynamic filter with column sort order. - if (filter.column_sort_order) { - if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) { - errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); - } - const sortByFilter = new DynamicFilterSortBy( - filter.column_sort_order, - filter.sort_order, - ); - dynamicFilter.setFilter(sortByFilter); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const categories = await ItemCategory.query().onBuild((builder) => { - dynamicFilter.buildQuery()(builder); - - builder.select([ - '*', - ItemCategory.relatedQuery('items').count().as('count'), - ]); - }); - - return res.status(200).send({ categories }); + } /** diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 092a53489..9fdb7b1ab 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -1,18 +1,20 @@ import { Inject, Service } from 'typedi'; -import { Router, Request, Response } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, ValidationChain, matchedData } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import validateMiddleware from 'api/middleware/validateMiddleware'; import ItemsService from 'services/Items/ItemsService'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing'; +import BaseController from 'api/controllers/BaseController'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() -export default class ItemsController { +export default class ItemsController extends BaseController { @Inject() itemsService: ItemsService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor. */ @@ -62,7 +64,8 @@ export default class ItemsController { '/', this.validateListQuerySchema, validateMiddleware, - asyncMiddleware(this.listItems.bind(this)), + asyncMiddleware(this.getItemsList.bind(this)), + this.dynamicListService.handlerErrorsToResponse, ); return router; } @@ -131,7 +134,7 @@ export default class ItemsController { */ get validateListQuerySchema() { return [ - query('column_sort_order').optional().isIn(['created_at', 'name', 'amount', 'sku']), + query('column_sort_order').optional().trim().escape(), query('sort_order').optional().isIn(['desc', 'asc']), query('page').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(), @@ -353,79 +356,25 @@ export default class ItemsController { } /** - * Listing items with pagination metadata. + * Retrieve items datatable list. * @param {Request} req * @param {Response} res */ - async listItems(req: Request, res: Response) { + async getItemsList(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, + filterRoles: [], + sortOrder: 'asc', + ...this.matchedQueryData(req), }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } - const { Resource, Item, View } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'items') - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'ITEMS.RESOURCE.NOT_FOUND', code: 200 }], - }); + try { + const items = await this.itemsService.getItemsList(tenantId, filter); + return res.status(200).send({ items }); + } catch (error) { + next(error); } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addModelClass(Item); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - listingBuilder.addView(viewMeta); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const items = await Item.query().onBuild((builder: any) => { - builder.withGraphFetched('costAccount'); - builder.withGraphFetched('sellAccount'); - builder.withGraphFetched('inventoryAccount'); - builder.withGraphFetched('category'); - - dynamicListing.buildQuery()(builder); - return builder; - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - items: { - ...items, - ...(viewMeta - ? { - viewMeta: { - custom_view_id: viewMeta.id, - view_columns: viewMeta.columns, - } - } - : {}), - }, - }); - } + } } \ No newline at end of file diff --git a/server/src/api/controllers/Organization.ts b/server/src/api/controllers/Organization.ts index 9ff614029..b43126018 100644 --- a/server/src/api/controllers/Organization.ts +++ b/server/src/api/controllers/Organization.ts @@ -68,16 +68,15 @@ export default class OrganizationController extends BaseController{ }); } } - console.log(error); next(error); } } /** * Seeds initial data to tenant database. - * @param req - * @param res - * @param next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async seed(req: Request, res: Response, next: Function) { const { organizationId } = req.tenant; diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index aa8d6cbef..9f7618323 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -9,9 +9,6 @@ import BillsService from 'services/Purchases/Bills'; import BaseController from 'api/controllers/BaseController'; import ItemsService from 'services/Items/ItemsService'; import TenancyService from 'services/Tenancy/TenancyService'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; -import { dynamicListingErrorsToResponse } from 'services/DynamicListing/HasDynamicListing'; @Service() export default class BillsController extends BaseController { @@ -330,71 +327,6 @@ export default class BillsController extends BaseController { * @return {Response} */ async listingBills(req: Request, res: Response) { - const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { Bill, View, Resource } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'bills') - .withGraphFetched('fields') - .first(); - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'BILLS_RESOURCE_NOT_FOUND', code: 200 }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addModelClass(Bill); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - listingBuilder.addView(viewMeta); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const bills = await Bill.query() - .onBuild((builder) => { - dynamicListing.buildQuery()(builder); - builder.withGraphFetched('vendor'); - return builder; - }) - .pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - bills: { - ...bills, - ...(viewMeta - ? { - view_meta: { - customViewId: viewMeta.id, - }, - } - : {}), - }, - }); } } diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index c9f395fc0..bd80c6903 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -1,5 +1,5 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; import { Service, Inject } from 'typedi'; import { check, param, query, ValidationChain, matchedData } from 'express-validator'; import { difference } from 'lodash'; @@ -8,9 +8,6 @@ import validateMiddleware from 'api/middleware/validateMiddleware'; import BaseController from 'api/controllers/BaseController'; import BillPaymentsService from 'services/Purchases/BillPayments'; import AccountsService from 'services/Accounts/AccountsService'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; -import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing'; /** * Bills payments controller. @@ -384,70 +381,6 @@ export default class BillsPayments extends BaseController { * @return {Response} */ async getBillsPayments(req: Request, res: Response) { - const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { BillPayment, View, Resource } = req.models; - const resource = await Resource.query() - .where('name', 'bill_payments') - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'BILL.PAYMENTS.RESOURCE.NOT_FOUND', code: 200 }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addModelClass(BillPayment); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - listingBuilder.addView(viewMeta); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const billPayments = await BillPayment.query().onBuild((builder) => { - dynamicListing.buildQuery()(builder); - builder.withGraphFetched('vendor'); - builder.withGraphFetched('paymentAccount'); - return builder; - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - bill_payments: { - ...billPayments, - ...(viewMeta - ? { - view_meta: { - customViewId: viewMeta.id, - }, - } - : {}), - }, - }); } } \ No newline at end of file diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 51302fc37..d2693a174 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -9,9 +9,6 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; import PaymentReceiveService from 'services/Sales/PaymentsReceives'; import SaleInvoiceService from 'services/Sales/SalesInvoices'; import AccountsService from 'services/Accounts/AccountsService'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing'; /** * Payments receives controller. @@ -406,71 +403,6 @@ export default class PaymentReceivesController extends BaseController { * @return {Response} */ async getPaymentReceiveList(req: Request, res: Response) { - const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { Resource, PaymentReceive, View, Bill } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'payment_receives') - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'PAYMENT_RECEIVES_RESOURCE_NOT_FOUND', code: 200 }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addModelClass(Bill); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - listingBuilder.addView(viewMeta); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const paymentReceives = await PaymentReceive.query().onBuild((builder) => { - builder.withGraphFetched('customer'); - builder.withGraphFetched('depositAccount'); - - dynamicListing.buildQuery()(builder); - return builder; - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - payment_receives: { - ...paymentReceives, - ...(viewMeta - ? { - viewMeta: { - customViewId: viewMeta.id, - } - } - : {}), - }, - }); + } } diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 514a5311e..9b397071c 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -7,8 +7,6 @@ import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import ItemsService from 'services/Items/ItemsService'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; @Service() export default class SalesEstimatesController extends BaseController { @@ -301,69 +299,6 @@ export default class SalesEstimatesController extends BaseController { * @param {Response} res */ async getEstimates(req: Request, res: Response) { - const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { SaleEstimate, Resource, View } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'sales_estimates') - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addView(viewMeta); - listingBuilder.addModelClass(SaleEstimate); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - - const salesEstimates = await SaleEstimate.query().onBuild((builder) => { - dynamicListing.buildQuery()(builder); - builder.withGraphFetched('customer'); - return builder; - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - sales_estimates: { - ...salesEstimates, - ...(viewMeta ? { - viewMeta: { - custom_view_id: viewMeta.id, - }, - } : {}), - }, - - }); + } }; diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 1230fa329..5b3c8a19a 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -7,9 +7,6 @@ import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleInvoiceService from 'services/Sales/SalesInvoices'; import ItemsService from 'services/Items/ItemsService'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import { dynamicListingErrorsToResponse } from 'services/DynamicListing/hasDynamicListing'; import { ISaleInvoiceOTD } from 'interfaces'; @Service() @@ -415,69 +412,6 @@ export default class SaleInvoicesController { * @param {Function} next */ async getSalesInvoices(req, res) { - const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { SaleInvoice, View, Resource } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'sales_invoices') - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'SALES_INVOICES_RESOURCE_NOT_FOUND', code: 200 }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addModelClass(SaleInvoice); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - listingBuilder.addView(viewMeta); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const salesInvoices = await SaleInvoice.query().onBuild((builder) => { - builder.withGraphFetched('entries'); - builder.withGraphFetched('customer'); - dynamicListing.buildQuery()(builder); - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - sales_invoices: { - ...salesInvoices, - ...(viewMeta - ? { - view_meta: { - customViewId: viewMeta.id, - } - } - : {}), - }, - }); + } } diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index 25d286cb6..7d5e7c8e8 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -6,11 +6,6 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; import AccountsService from 'services/Accounts/AccountsService'; import ItemsService from 'services/Items/ItemsService'; import SaleReceiptService from 'services/Sales/SalesReceipts'; -import DynamicListingBuilder from 'services/DynamicListing/DynamicListingBuilder'; -import DynamicListing from 'services/DynamicListing/DynamicListing'; -import { - dynamicListingErrorsToResponse -} from 'services/DynamicListing/HasDynamicListing'; @Service() export default class SalesReceiptsController { @@ -302,65 +297,6 @@ export default class SalesReceiptsController { * @param {Response} res */ async listingSalesReceipts(req: Request, res: Response) { - const filter = { - filter_roles: [], - sort_order: 'asc', - page: 1, - page_size: 10, - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { SaleReceipt, Resource, View } = req.models; - const resource = await Resource.query() - .remember() - .where('name', 'sales_receipts') - .withGraphFetched('fields') - .first(); - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }], - }); - } - const viewMeta = await View.query() - .modify('allMetadata') - .modify('specificOrFavourite', filter.custom_view_id) - .where('resource_id', resource.id) - .first(); - - const listingBuilder = new DynamicListingBuilder(); - const errorReasons = []; - - listingBuilder.addView(viewMeta); - listingBuilder.addModelClass(SaleReceipt); - listingBuilder.addCustomViewId(filter.custom_view_id); - listingBuilder.addFilterRoles(filter.filter_roles); - listingBuilder.addSortBy(filter.sort_by, filter.sort_order); - - const dynamicListing = new DynamicListing(listingBuilder); - - if (dynamicListing instanceof Error) { - const errors = dynamicListingErrorsToResponse(dynamicListing); - errorReasons.push(...errors); - } - const salesReceipts = await SaleReceipt.query().onBuild((builder) => { - builder.withGraphFetched('customer'); - builder.withGraphFetched('depositAccount'); - builder.withGraphFetched('entries'); - dynamicListing.buildQuery()(builder); - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - sales_receipts: { - ...salesReceipts, - ...(viewMeta ? { - view_meta: { - customViewId: viewMeta.id, - } - } : {}), - }, - }); } }; diff --git a/server/src/api/controllers/Subscription/Licenses.ts b/server/src/api/controllers/Subscription/Licenses.ts index ea9a3e1a0..781c8710f 100644 --- a/server/src/api/controllers/Subscription/Licenses.ts +++ b/server/src/api/controllers/Subscription/Licenses.ts @@ -178,7 +178,6 @@ export default class LicensesController extends BaseController { message: 'The licenses have been generated successfully.' }); } catch (error) { - console.log(error); next(error); } } diff --git a/server/src/api/controllers/Users.ts b/server/src/api/controllers/Users.ts index ec223be12..35e4ae2af 100644 --- a/server/src/api/controllers/Users.ts +++ b/server/src/api/controllers/Users.ts @@ -134,7 +134,6 @@ export default class UsersController extends BaseController{ await this.usersService.deleteUser(tenantId, id); return res.status(200).send({ id }); } catch (error) { - if (error instanceof ServiceError) { if (error.errorType === 'user_not_found') { return res.boom.notFound(null, { @@ -142,7 +141,7 @@ export default class UsersController extends BaseController{ }); } } - next(); + next(error); } } @@ -160,8 +159,6 @@ export default class UsersController extends BaseController{ const user = await this.usersService.getUser(tenantId, userId); return res.status(200).send({ user }); } catch (error) { - console.log(error); - if (error instanceof ServiceError) { if (error.errorType === 'user_not_found') { return res.boom.notFound(null, { @@ -169,7 +166,7 @@ export default class UsersController extends BaseController{ }); } } - next(); + next(error); } } @@ -186,7 +183,7 @@ export default class UsersController extends BaseController{ return res.status(200).send({ users }); } catch (error) { - next(); + next(error); } } @@ -204,8 +201,6 @@ export default class UsersController extends BaseController{ await this.usersService.activateUser(tenantId, userId); return res.status(200).send({ id: userId }); } catch(error) { - console.log(error); - if (error instanceof ServiceError) { if (error.errorType === 'user_not_found') { return res.status(404).send({ @@ -218,7 +213,7 @@ export default class UsersController extends BaseController{ }); } } - next(); + next(error); } } @@ -248,7 +243,7 @@ export default class UsersController extends BaseController{ }); } } - next(); + next(error); } } }; \ No newline at end of file diff --git a/server/src/api/middleware/SettingsMiddleware.ts b/server/src/api/middleware/SettingsMiddleware.ts index 39f4437fb..408c20c5b 100644 --- a/server/src/api/middleware/SettingsMiddleware.ts +++ b/server/src/api/middleware/SettingsMiddleware.ts @@ -6,8 +6,6 @@ export default async (req: Request, res: Response, next: NextFunction) => { const { tenantId } = req.user; const { knex } = req; - console.log(knex); - const Logger = Container.get('logger'); const tenantContainer = Container.of(`tenant-${tenantId}`); diff --git a/server/src/data/ResourceFieldsKeys.js b/server/src/data/ResourceFieldsKeys.js index ab737010a..8f46e8a5f 100644 --- a/server/src/data/ResourceFieldsKeys.js +++ b/server/src/data/ResourceFieldsKeys.js @@ -37,7 +37,7 @@ export default { 'type': { column: 'account_type_id', relation: 'account_types.id', - relationColumn: 'account_types.id', + relationColumn: 'account_types.key', }, 'description': { column: 'description', @@ -58,7 +58,8 @@ export default { column: 'active', }, balance: { - column: 'balance', + column: 'amount', + columnType: 'number' }, currency: { column: 'currency_code', diff --git a/server/src/database/migrations/20190423085240_create_resources_table.js b/server/src/database/migrations/20190423085240_create_resources_table.js deleted file mode 100644 index 1f9b99d40..000000000 --- a/server/src/database/migrations/20190423085240_create_resources_table.js +++ /dev/null @@ -1,11 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('resources', (table) => { - table.increments(); - table.string('name'); - }); -}; - -exports.down = function (knex) { - return knex.schema.dropTable('resources'); -}; diff --git a/server/src/database/migrations/20190822214905_create_resource_fields_table.js b/server/src/database/migrations/20190822214905_create_resource_fields_table.js deleted file mode 100644 index 4087dde75..000000000 --- a/server/src/database/migrations/20190822214905_create_resource_fields_table.js +++ /dev/null @@ -1,21 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('resource_fields', (table) => { - table.increments(); - table.string('label_name'); - table.string('key'); - table.string('data_type'); - table.string('help_text'); - table.string('default'); - table.boolean('active'); - table.boolean('predefined'); - table.boolean('builtin').defaultTo(false); - table.boolean('columnable'); - table.integer('index'); - table.string('data_resource'); - table.json('options'); - table.integer('resource_id').unsigned(); - }).raw('ALTER TABLE `RESOURCE_FIELDS` AUTO_INCREMENT = 1000'); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('resource_fields'); diff --git a/server/src/database/migrations/20190822214905_create_views_columns.js b/server/src/database/migrations/20190822214905_create_views_columns.js index 1fddbaca1..5b902fbf3 100644 --- a/server/src/database/migrations/20190822214905_create_views_columns.js +++ b/server/src/database/migrations/20190822214905_create_views_columns.js @@ -3,7 +3,7 @@ exports.up = function (knex) { return knex.schema.createTable('view_has_columns', (table) => { table.increments(); table.integer('view_id').unsigned(); - table.integer('field_id').unsigned(); + table.string('field_key'); table.integer('index').unsigned(); }).raw('ALTER TABLE `ITEMS_CATEGORIES` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20190822214905_create_views_roles_table.js b/server/src/database/migrations/20190822214905_create_views_roles_table.js new file mode 100644 index 000000000..f96a15cbe --- /dev/null +++ b/server/src/database/migrations/20190822214905_create_views_roles_table.js @@ -0,0 +1,13 @@ + +exports.up = function (knex) { + return knex.schema.createTable('view_roles', (table) => { + table.increments(); + table.integer('index'); + table.string('field_key'); + table.string('comparator'); + table.string('value'); + table.integer('view_id').unsigned(); + }).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000'); +}; + +exports.down = (knex) => knex.schema.dropTableIfExists('view_roles'); \ No newline at end of file diff --git a/server/src/database/migrations/20190822214905_create_views_table.js b/server/src/database/migrations/20190822214905_create_views_table.js index 26deaef69..e238d7ccb 100644 --- a/server/src/database/migrations/20190822214905_create_views_table.js +++ b/server/src/database/migrations/20190822214905_create_views_table.js @@ -4,7 +4,7 @@ exports.up = function (knex) { table.increments(); table.string('name'); table.boolean('predefined'); - table.integer('resource_id').unsigned().references('id').inTable('resources'); + table.string('resource_model'); table.boolean('favourite'); table.string('roles_logic_expression'); table.timestamps(); diff --git a/server/src/database/migrations/20200125173323_create_resource_custom_fields_metadata_table.js b/server/src/database/migrations/20200125173323_create_resource_custom_fields_metadata_table.js deleted file mode 100644 index b1731a3ea..000000000 --- a/server/src/database/migrations/20200125173323_create_resource_custom_fields_metadata_table.js +++ /dev/null @@ -1,14 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('resource_custom_fields_metadata', (table) => { - table.increments(); - table.integer('resource_id').unsigned(); - table.integer('resource_item_id').unsigned(); - table.string('key'); - table.string('value'); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('resource_custom_fields_metadata'); -}; diff --git a/server/src/database/seeds/core/20200810121807_seed_views.js b/server/src/database/seeds/core/20200810121807_seed_views.js new file mode 100644 index 000000000..87d383888 --- /dev/null +++ b/server/src/database/seeds/core/20200810121807_seed_views.js @@ -0,0 +1,37 @@ + +exports.up = (knex) => { + // Deletes ALL existing entries + return knex('views').del() + .then(() => { + // Inserts seed entries + return knex('views').insert([ + // Accounts + { id: 15, name: 'Inactive', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 1, name: 'Assets', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 3, name: 'Equity', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 4, name: 'Income', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 5, name: 'Expenses', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + + // Items + // { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true }, + // { id: 7, name: 'Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true }, + // { id: 8, name: 'Non-Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true }, + + // // Manual Journals + // { id: 9, name: 'Journal', roles_logic_expression: '1', resource_id: 4, predefined: true }, + // { id: 10, name: 'Credit', roles_logic_expression: '1', resource_id: 4, predefined: true }, + // { id: 11, name: 'Reconciliation', roles_logic_expression: '1', resource_id: 4, predefined: true }, + + // // Expenses + // { id: 12, name: 'Interest', roles_logic_expression: '1', resource_id: 3, predefined: false, }, + // { id: 13, name: 'Depreciation', roles_logic_expression: '1', resource_id: 3, predefined: false, }, + // { id: 14, name: 'Payroll', roles_logic_expression: '1', resource_id: '3', predefined: false }, + ]); + }); +}; + + +exports.down = (knex) => { + +}; \ No newline at end of file diff --git a/server/src/database/seeds/core/20200810121808_seed_views_roles.js b/server/src/database/seeds/core/20200810121808_seed_views_roles.js new file mode 100644 index 000000000..f8ad2a64f --- /dev/null +++ b/server/src/database/seeds/core/20200810121808_seed_views_roles.js @@ -0,0 +1,34 @@ + +exports.up = (knex) => { + // Deletes ALL existing entries + return knex('view_roles').del() + .then(() => { + // Inserts seed entries + return knex('view_roles').insert([ + // Accounts + { id: 1, field_key: 'type', index: 1, comparator: 'equals', value: 'asset', view_id: 1 }, + { id: 2, field_key: 'type', index: 1, comparator: 'equals', value: 'liability', view_id: 2 }, + { id: 3, field_key: 'type', index: 1, comparator: 'equals', value: 'equity', view_id: 3 }, + { id: 4, field_key: 'type', index: 1, comparator: 'equals', value: 'income', view_id: 4 }, + { id: 5, field_key: 'type', index: 1, comparator: 'equals', value: 'expense', view_id: 5 }, + { id: 12, field_key: 'active', index: 1, comparator: 'is', value: 1, view_id: 15 }, + + // Items. + // { id: 6, field_id: 12, index: 1, comparator: 'equals', value: 'service', view_id: 6 }, + // { id: 7, field_id: 12, index: 1, comparator: 'equals', value: 'inventory', view_id: 7 }, + // { id: 8, field_id: 12, index: 1, comparator: 'equals', value: 'non-inventory', view_id: 8 }, + + // // Manual Journals. + // { id: 9, field_id: 26, index: 1, comparator: 'equals', value: 'Journal', view_id: 9 }, + // { id: 10, field_id: 26, index: 1, comparator: 'equals', value: 'CreditNote', view_id: 10 }, + // { id: 11, field_id: 26, index: 1, comparator: 'equals', value: 'Reconciliation', view_id: 11 }, + + // Expenses + // { id: 12, field_id: index: } + ]); + }); +}; + +exports.down = (knex) => { + +} \ No newline at end of file diff --git a/server/src/database/seeds/seed_accounts.js b/server/src/database/seeds/seed_accounts.js deleted file mode 100644 index 9189dc4b0..000000000 --- a/server/src/database/seeds/seed_accounts.js +++ /dev/null @@ -1,177 +0,0 @@ - -exports.seed = (knex) => { - console.log(knex.tenantId); - - // Deletes ALL existing entries - return knex('accounts').del() - .then(() => { - // Inserts seed entries - return knex('accounts').insert([ - { - id: 1, - name: 'Petty Cash', - slug: 'petty-cash', - account_type_id: 2, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 2, - name: 'Bank', - slug: 'bank', - account_type_id: 2, - parent_account_id: null, - code: '2000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 3, - name: 'Other Income', - slug: 'other-income', - account_type_id: 7, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 4, - name: 'Interest Income', - slug: 'interest-income', - account_type_id: 7, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 5, - name: 'Opening Balance', - slug: 'opening-balance', - account_type_id: 5, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 6, - name: 'Depreciation Expense', - slug: 'depreciation-expense', - account_type_id: 6, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 7, - name: 'Interest Expense', - slug: 'interest-expense', - account_type_id: 6, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 8, - name: 'Payroll Expenses', - slug: 'payroll-expenses', - account_type_id: 6, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 9, - name: 'Other Expenses', - slug: 'other-expenses', - account_type_id: 6, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 10, - name: 'Accounts Receivable', - slug: 'accounts-receivable', - account_type_id: 8, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 11, - name: 'Accounts Payable', - slug: 'accounts-payable', - account_type_id: 9, - parent_account_id: null, - code: '1000', - description: '', - active: 1, - index: 1, - predefined: 1, - }, - { - id: 12, - name: 'Cost of Goods Sold (COGS)', - slug: 'cost-of-goods-sold', - account_type_id: 12, - predefined: 1, - parent_account_id: null, - index: 1, - active: 1, - description: 1, - }, - { - id: 13, - name: 'Inventory Asset', - slug: 'inventory-asset', - account_type_id: 14, - predefined: 1, - parent_account_id: null, - index: 1, - active: 1, - description: '', - }, - { - id: 14, - name: 'Sales of Product Income', - slug: 'sales-of-product-income', - account_type_id: 7, - predefined: 1, - parent_account_id: null, - index: 1, - active: 1, - description: '', - } - ]); - }); -}; diff --git a/server/src/database/seeds/seed_accounts_fields.js b/server/src/database/seeds/seed_accounts_fields.js deleted file mode 100644 index f89f58699..000000000 --- a/server/src/database/seeds/seed_accounts_fields.js +++ /dev/null @@ -1,67 +0,0 @@ -exports.seed = function (knex) { - // Deletes ALL existing entries - return knex('resource_fields') - .del() - .then(() => { - // Inserts seed entries - return knex('resource_fields').insert([ - { - id: 1, - label_name: 'Name', - key: 'name', - data_type: '', - active: 1, - predefined: 1, - }, - { - id: 2, - label_name: 'Code', - key: 'code', - data_type: '', - active: 1, - predefined: 1, - }, - { - id: 3, - label_name: 'Account Type', - key: 'type', - data_type: '', - active: 1, - predefined: 1, - data_resource_id: 8, - }, - { - id: 4, - label_name: 'Description', - key: 'description', - data_type: '', - active: 1, - predefined: 1, - }, - { - id: 5, - label_name: 'Account Normal', - key: 'normal', - data_type: 'string', - active: 1, - predefined: 1, - }, - { - id: 6, - label_name: 'Root Account Type', - key: 'root_type', - data_type: 'string', - active: 1, - predefined: 1, - }, - { - id: 7, - label_name: 'Active', - key: 'active', - data_type: 'boolean', - active: 1, - predefined: 1, - }, - ]); - }); -}; diff --git a/server/src/database/seeds/seed_resources.js b/server/src/database/seeds/seed_resources.js deleted file mode 100644 index 00d10878b..000000000 --- a/server/src/database/seeds/seed_resources.js +++ /dev/null @@ -1,25 +0,0 @@ - -exports.seed = (knex) => { - // Deletes ALL existing entries - return knex('resources').del() - .then(() => { - // Inserts seed entries - return knex('resources').insert([ - { id: 1, name: 'accounts' }, - { id: 8, name: 'accounts_types' }, - { id: 2, name: 'items' }, - { id: 3, name: 'expenses' }, - { id: 4, name: 'manual_journals' }, - { id: 5, name: 'items_categories' }, - { id: 6, name: 'customers' }, - { id: 7, name: 'vendors' }, - { id: 9, name: 'sales_estimates' }, - { id: 10, name: 'sales_receipts' }, - { id: 11, name: 'sales_invoices' }, - { id: 12, name: 'sales_payment_receives' }, - { id: 13, name: 'bills' }, - { id: 14, name: 'bill_payments' }, - { id: 16, name: 'payment_receives' }, - ]); - }); -}; diff --git a/server/src/database/seeds/seed_resources_fields.js b/server/src/database/seeds/seed_resources_fields.js deleted file mode 100644 index a5f8b89cd..000000000 --- a/server/src/database/seeds/seed_resources_fields.js +++ /dev/null @@ -1,424 +0,0 @@ - -exports.seed = (knex) => { - return knex('resource_fields').del() - .then(() => { - return knex('resource_fields').insert([ - // Accounts - { - id: 1, - resource_id: 1, - label_name: 'Account Name', - key: 'name', - data_type: 'textbox', - predefined: 1, - columnable: true, - }, - { - id: 2, - resource_id: 1, - label_name: 'Code', - key: 'code', - data_type: 'textbox', - predefined: 1, - columnable: true, - }, - { - id: 3, - resource_id: 1, - label_name: 'Type', - key: 'type', - data_type: 'options', - predefined: 1, - columnable: true, - data_resource: 'accounts_types', - }, - { - id: 5, - resource_id: 1, - label_name: 'Description', - data_type: 'textarea', - key: 'description', - predefined: 1, - columnable: true, - }, - { - id: 6, - resource_id: 1, - label_name: 'Root type', - data_type: 'textbox', - key: 'root_type', - predefined: 1, - columnable: true, - }, - { - id: 16, - resource_id: 1, - label_name: 'Created at', - data_type: 'date', - key: 'created_at', - predefined: 1, - columnable: true, - }, - { - id: 17, - resource_id: 1, - data_type: 'boolean', - label_name: 'Active', - key: 'active', - predefined: 1, - columnable: true, - }, - { - id: 31, - resource_id: 1, - data_type: 'numeric', - label_name: 'Balance', - key: 'balance', - predefined: 1, - columnable: true, - }, - { - id: 32, - resource_id: 1, - data_type: 'options', - label_name: 'Currency', - key: 'currency', - predefined: 1, - columnable: true, - data_resource: 'currencies', - }, - { - id: 33, - resource_id: 1, - data_type: 'options', - label_name: 'Normal', - key: 'normal', - predefined: 1, - columnable: true, - options: JSON.stringify([ - { key: 'credit', label: 'Credit' }, - { key: 'debit', label: 'Debit' }, - ]) - }, - - // Manual Journals - { - id: 18, - resource_id: 4, - data_type: 'date', - label_name: 'Date', - key: 'date', - predefined: 1, - columnable: true, - }, - { - id: 19, - resource_id: 4, - data_type: 'date', - label_name: 'Created At', - key: 'created_at', - predefined: 1, - columnable: true, - }, - { - id: 20, - resource_id: 4, - data_type: 'textbox', - label_name: 'Journal Number', - key: 'journal_number', - predefined: 1, - columnable: true, - }, - { - id: 21, - resource_id: 4, - data_type: 'boolean', - label_name: 'Active', - key: 'status', - predefined: 1, - columnable: true, - }, - { - id: 22, - resource_id: 4, - data_type: 'textbox', - label_name: 'Reference', - key: 'reference', - predefined: 1, - columnable: true, - }, - { - id: 23, - resource_id: 4, - data_type: 'textbox', - label_name: 'Description', - key: 'description', - predefined: 1, - columnable: true, - }, - { - id: 24, - resource_id: 4, - data_type: 'numeric', - label_name: 'Amount', - key: 'amount', - predefined: 1, - columnable: true, - }, - { - id: 25, - resource_id: 4, - data_type: 'optons', - label_name: 'User', - key: 'user', - predefined: 1, - columnable: true, - data_resource: 'users', - }, - { - id: 26, - resource_id: 4, - data_type: 'textbox', - label_name: 'Journal Type', - key: 'journal_type', - predefined: 1, - columnable: true, - }, - - // Expenses - { - id: 7, - resource_id: 3, - label_name: 'Payment Date', - key: 'payment_date', - data_type: 'date', - predefined: 1, - columnable: true, - }, - { - id: 9, - resource_id: 3, - key: 'payment_account', - label_name: 'Payment Account', - data_type: 'options', - predefined: 1, - columnable: true, - data_resource: 'accounts', - }, - { - id: 10, - resource_id: 3, - key: 'total_amount', - label_name: 'Amount', - data_type: 'numeric', - predefined: 1, - columnable: true, - }, - { - id: 27, - resource_id: 3, - label_name: 'Reference No.', - key: 'reference_no', - data_type: 'textbox', - predefined: 1, - columnable: true, - }, - { - id: 28, - resource_id: 3, - key: 'description', - label_name: 'Description', - data_type: 'textbox', - predefined: 1, - columnable: true, - }, - { - id: 29, - resource_id: 3, - key: 'published', - label_name: 'Published', - data_type: 'checkbox', - predefined: 1, - columnable: true, - }, - { - id: 30, - resource_id: 3, - key: 'user', - data_type: 'options', - label_name: 'User', - predefined: 1, - columnable: true, - data_resource: 'users', - }, - - // Items - { - id: 11, - resource_id: 2, - label_name: 'Name', - key: 'name', - data_type: 'textbox', - predefined: 1, - columnable: true, - }, - { - id: 12, - resource_id: 2, - label_name: 'Type', - key: 'type', - data_type: 'textbox', - predefined: 1, - columnable: true, - }, - - - // Sales Estimates - { - label_name: 'Customer name', - key: 'customer_name', - }, - { - label_name: 'Amount', - key: 'amount', - }, - { - label_name: 'Estimate number', - key: 'estimate_number', - }, - { - label_name: 'Estimate date', - key: 'estimate_date', - }, - { - label_name: 'Expiration date', - key: 'expiration_date', - }, - { - label_name: 'Reference', - key: 'reference', - }, - { - label_name: 'Terms and conditions', - key: 'terms_conditions', - }, - { - label_name: 'Note', - key: 'note', - }, - - // Sales invoices - // { - // label_name: 'Customer name', - // ley: 'customer_name', - // }, - // { - // label_name: 'Amount', - // ley: 'amount', - // }, - // { - // label_name: 'Invoice number', - // ley: 'invoice_no', - // }, - // { - // label_name: 'Invoice date', - // ley: 'invoice_date', - // }, - // { - // label_name: 'Reference', - // ley: 'reference', - // }, - // { - // label_name: 'Payment amount', - // ley: 'payment_amount', - // }, - // { - // label_name: 'Invoice message', - // ley: 'invoice_no', - // }, - // { - // label_name: 'Terms and conditions', - // key: 'terms_conditions', - // }, - - // // Sales receipts - // { - // label_name: 'Deposit account', - // key: 'deposit_account', - // }, - // { - // label_name: 'Customer name', - // key: 'customer_name', - // }, - // { - // label_name: 'Receipt date', - // key: 'receipt_date', - // }, - // { - // label_name: 'Reference No', - // key: 'reference', - // }, - // { - // label_name: 'Receipt message', - // key: 'receipt_message', - // }, - // { - // label_name: 'Sent to email', - // key: 'email_send_to', - // }, - - // // Payment Receives - // { - // label_name: 'Customer name', - // key: 'customer_name', - // }, - // { - // label_name: 'Payment date', - // key: 'payment_date', - // }, - // { - // label_name: 'Amount', - // key: 'amount', - // }, - // { - // label_name: 'Reference No', - // key: 'reference', - // }, - // { - // label_name: 'Deposit account', - // key: 'deposit_account', - // }, - // { - // label_name: 'Payment receive no.', - // key: 'payment_receive_no', - // }, - - // // Purchases bills. - // { - // label_name: 'Bill number', - // key: 'bill_number' - // }, - - // { - // label_name: 'Bill date', - // key: 'bill_date' - // }, - // { - // label_name: 'Amount', - // key: 'amount' - // }, - // { - // label_name: 'Vendor name', - // key: 'vendor_name' - // }, - // { - // label_name: 'Due date', - // key: 'due_date' - // }, - // { - // label_name: 'Note', - // key: 'note' - // }, - ]); - }); -}; diff --git a/server/src/database/seeds/seed_users.js b/server/src/database/seeds/seed_users.js deleted file mode 100644 index 58bbf28d8..000000000 --- a/server/src/database/seeds/seed_users.js +++ /dev/null @@ -1,18 +0,0 @@ - -exports.seed = (knex) => { - return knex('users').del() - .then(() => { - return knex('users').insert([ - { - first_name: 'Ahmed', - last_name: 'Mohamed', - email: 'admin@admin.com', - phone_number: '0920000000', - password: '$2b$10$LGSMrezP8IHBb/cNMlc1ZOKA59Fc9rY0IEk2u.iuF/y6yS2YlGP7i', // test - active: 1, - language: 'ar', - created_at: new Date(), - }, - ]); - }); -}; diff --git a/server/src/database/seeds/seed_views.js b/server/src/database/seeds/seed_views.js deleted file mode 100644 index f46042ddc..000000000 --- a/server/src/database/seeds/seed_views.js +++ /dev/null @@ -1,32 +0,0 @@ - -exports.seed = (knex) => { - // Deletes ALL existing entries - return knex('views').del() - .then(() => { - // Inserts seed entries - return knex('views').insert([ - // Accounts - { id: 15, name: 'Inactive', roles_logic_expression: '1', resource_id: 1, predefined: true }, - { id: 1, name: 'Assets', roles_logic_expression: '1', resource_id: 1, predefined: true }, - { id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_id: 1, predefined: true }, - { id: 3, name: 'Equity', roles_logic_expression: '1', resource_id: 1, predefined: true }, - { id: 4, name: 'Income', roles_logic_expression: '1', resource_id: 1, predefined: true }, - { id: 5, name: 'Expenses', roles_logic_expression: '1', resource_id: 1, predefined: true }, - - // Items - { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true }, - { id: 7, name: 'Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true }, - { id: 8, name: 'Non-Inventory', roles_logic_expression: '1', resource_id: 2, predefined: true }, - - // Manual Journals - { id: 9, name: 'Journal', roles_logic_expression: '1', resource_id: 4, predefined: true }, - { id: 10, name: 'Credit', roles_logic_expression: '1', resource_id: 4, predefined: true }, - { id: 11, name: 'Reconciliation', roles_logic_expression: '1', resource_id: 4, predefined: true }, - - // Expenses - { id: 12, name: 'Interest', roles_logic_expression: '1', resource_id: 3, predefined: false, }, - { id: 13, name: 'Depreciation', roles_logic_expression: '1', resource_id: 3, predefined: false, }, - { id: 14, name: 'Payroll', roles_logic_expression: '1', resource_id: '3', predefined: false }, - ]); - }); -}; diff --git a/server/src/database/seeds/seed_views_roles.js b/server/src/database/seeds/seed_views_roles.js deleted file mode 100644 index 7441e7cbe..000000000 --- a/server/src/database/seeds/seed_views_roles.js +++ /dev/null @@ -1,30 +0,0 @@ - -exports.seed = (knex) => { - // Deletes ALL existing entries - return knex('view_roles').del() - .then(() => { - // Inserts seed entries - return knex('view_roles').insert([ - // Accounts - { id: 1, field_id: 6, index: 1, comparator: 'equals', value: 'asset', view_id: 1 }, - { id: 2, field_id: 6, index: 1, comparator: 'equals', value: 'liability', view_id: 2 }, - { id: 3, field_id: 6, index: 1, comparator: 'equals', value: 'equity', view_id: 3 }, - { id: 4, field_id: 6, index: 1, comparator: 'equals', value: 'income', view_id: 4 }, - { id: 5, field_id: 6, index: 1, comparator: 'equals', value: 'expense', view_id: 5 }, - { id: 12, field_id: 17, index: 1, comparator: 'is', value: 1, view_id: 15 }, - - // Items. - { id: 6, field_id: 12, index: 1, comparator: 'equals', value: 'service', view_id: 6 }, - { id: 7, field_id: 12, index: 1, comparator: 'equals', value: 'inventory', view_id: 7 }, - { id: 8, field_id: 12, index: 1, comparator: 'equals', value: 'non-inventory', view_id: 8 }, - - // Manual Journals. - { id: 9, field_id: 26, index: 1, comparator: 'equals', value: 'Journal', view_id: 9 }, - { id: 10, field_id: 26, index: 1, comparator: 'equals', value: 'CreditNote', view_id: 10 }, - { id: 11, field_id: 26, index: 1, comparator: 'equals', value: 'Reconciliation', view_id: 11 }, - - // Expenses - // { id: 12, field_id: index: } - ]); - }); -}; diff --git a/server/src/interfaces/Account.ts b/server/src/interfaces/Account.ts index 64bfb6749..751c349b8 100644 --- a/server/src/interfaces/Account.ts +++ b/server/src/interfaces/Account.ts @@ -1,4 +1,4 @@ - +import { IDynamicListFilterDTO } from 'interfaces/DynamicFilter'; export interface IAccountDTO { name: string, @@ -12,4 +12,8 @@ export interface IAccount { code: string, description: string, accountTypeNumber: number, -}; \ No newline at end of file +}; + +export interface IAccountsFilter extends IDynamicListFilterDTO { + stringifiedFilterRoles?: string, +}; diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts index af9b8b0b3..e0183cd8e 100644 --- a/server/src/interfaces/Contact.ts +++ b/server/src/interfaces/Contact.ts @@ -1,5 +1,8 @@ // Contact Interfaces. + +import { IDynamicListFilter } from "./DynamicFilter"; + // ---------------------------------- export interface IContactAddress { billingAddress1: string, @@ -167,4 +170,16 @@ export interface IVendorEditDTO extends IContactAddressDTO { note?: string, active?: boolean, +}; + +export interface IVendorsFilter extends IDynamicListFilter { + stringifiedFilterRoles?: string, + page?: number, + pageSize?: number, +}; + +export interface ICustomerFilter extends IDynamicListFilter { + stringifiedFilterRoles?: string, + page?: number, + pageSize?: number, }; \ No newline at end of file diff --git a/server/src/interfaces/DynamicFilter.ts b/server/src/interfaces/DynamicFilter.ts new file mode 100644 index 000000000..0453e9c97 --- /dev/null +++ b/server/src/interfaces/DynamicFilter.ts @@ -0,0 +1,20 @@ + +export interface IDynamicFilter { + setTableName(tableName: string): void; + buildQuery(): void; +} + +export interface IFilterRole { + fieldKey: string, + value: string, + condition?: string, + index?: number, + comparator?: string, +}; + +export interface IDynamicListFilterDTO { + customViewId?: number, + filterRoles?: IFilterRole[], + columnSortBy: string, + sortOrder: string, +} \ No newline at end of file diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index 52a0d1b0c..e7fa44cce 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -1,7 +1,17 @@ - +import { IDynamicListFilter } from 'interfaces/DynamicFilter'; export interface IItem{ id: number, name: string, type: string, -} \ No newline at end of file +} + +export interface IItemsService { + +} + +export interface IItemsFilter extends IDynamicListFilter { + stringifiedFilterRoles?: string, + page?: number, + pageSize?: number, +}; \ No newline at end of file diff --git a/server/src/interfaces/Resource.ts b/server/src/interfaces/Resource.ts new file mode 100644 index 000000000..d193a94c7 --- /dev/null +++ b/server/src/interfaces/Resource.ts @@ -0,0 +1,22 @@ + + +export interface IResource { + id: number, + key: string, +} + +export interface IResourceField { + labelName: string, + key: string, + dataType: string, + helpText?: string | null, + default?: string, + predefined: boolean, + active: boolean, + builtin: boolean, + columnable: boolean, + index: number, + dataResource: string, + resourceId: number, + options: any; +} \ No newline at end of file diff --git a/server/src/interfaces/View.ts b/server/src/interfaces/View.ts new file mode 100644 index 000000000..9c308b0a7 --- /dev/null +++ b/server/src/interfaces/View.ts @@ -0,0 +1,24 @@ + +export interface IView { + id: number, + name: string, + predefined: boolean, + resourceId: number, + favourite: boolean, + rolesLogicRxpression: string, +}; + +export interface IViewRole { + id: number, + fieldId: number, + index: number, + comparator: string, + value: string, + viewId: number, +}; + +export interface IViewHasColumn { + viewId: number, + fieldId: number, + index: number, +} \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index ddd1b086a..20f8db9f1 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -1,153 +1,22 @@ -import { IInventoryTransaction, IInventoryLotCost } from './InventoryTransaction'; -import { - IBillPaymentEntry, - IBillPayment, - IBillPaymentOTD, -} from './BillPayment'; -import { IInventoryCostMethod } from './InventoryCostMethod'; -import { IItemEntry } from './ItemEntry'; -import { IItem } from './Item'; -import { ILicense, ILicensesFilter } from './License'; -import { IItemCategory, IItemCategoryOTD } from './ItemCategory'; -import { - IPaymentModel, - ILicensePaymentModel, - IPaymentMethod, - ILicensePaymentMethod, - IPaymentContext, -} from './Payment'; -import { - ISaleInvoice, - ISaleInvoiceOTD, -} from './SaleInvoice'; -import { - IPaymentReceive, - IPaymentReceiveOTD, -} from './PaymentReceive'; -import { - ISaleEstimate, - ISaleEstimateOTD, -} from './SaleEstimate'; -import { - IRegisterDTO, -} from './Register'; -import { - ISystemUser, - ISystemUserDTO, - IInviteUserInput, -} from './User'; -import { - IMetadata, - IMetaQuery, - IMetableStore, - IMetableStoreStorage, -} from './Metable'; -import { - IOptionDTO, - IOptionsDTO, -} from './Options'; -import { - IAccount, - IAccountDTO, -} from './Account'; -import { - IJournalEntry, - IJournalPoster, - TEntryType, - IAccountChange, - IAccountsChange, -} from './Journal'; -import { - IContactAddress, - IContact, - IContactNewDTO, - IContactEditDTO, - ICustomer, - ICustomerNewDTO, - ICustomerEditDTO, -} from './Contact'; -import { - IExpense, - IExpenseCategory, - IExpenseDTO, - IExpenseCategoryDTO, - IExpensesService, -} from './Expenses'; -import { - ITenant, - ITenantDBManager, - ITenantManager, - ISystemService, -} from './Tenancy'; - -export { - IAccount, - IAccountDTO, - - IBillPaymentEntry, - IBillPayment, - IBillPaymentOTD, - - IInventoryTransaction, - IInventoryLotCost, - IInventoryCostMethod, - IItemEntry, - IItem, - ILicense, - ILicensesFilter, - IItemCategory, - IItemCategoryOTD, - - IPaymentModel, - IPaymentMethod, - IPaymentContext, - ILicensePaymentModel, - ILicensePaymentMethod, - - ISaleInvoice, - ISaleInvoiceOTD, - - ISaleEstimate, - ISaleEstimateOTD, - - IPaymentReceive, - IPaymentReceiveOTD, - - IRegisterDTO, - ISystemUser, - ISystemUserDTO, - IInviteUserInput, - - IMetadata, - IMetaQuery, - IMetableStore, - IMetableStoreStorage, - - IOptionDTO, - IOptionsDTO, - - IJournalEntry, - IJournalPoster, - TEntryType, - IAccountChange, - IAccountsChange, - - IContactAddress, - IContact, - IContactNewDTO, - IContactEditDTO, - ICustomer, - ICustomerNewDTO, - ICustomerEditDTO, - - IExpense, - IExpenseCategory, - IExpenseDTO, - IExpenseCategoryDTO, - IExpensesService, - - ITenant, - ITenantDBManager, - ITenantManager, - ISystemService, -}; \ No newline at end of file +export * from './InventoryTransaction'; +export * from './BillPayment'; +export * from './InventoryCostMethod'; +export * from './ItemEntry'; +export * from './Item'; +export * from './License'; +export * from './ItemCategory'; +export * from './Payment'; +export * from './SaleInvoice'; +export * from './PaymentReceive'; +export * from './SaleEstimate'; +export * from './Register'; +export * from './User'; +export * from './Metable'; +export * from './Options'; +export * from './Account'; +export * from './DynamicFilter'; +export * from './Journal'; +export * from './Contact'; +export * from './Expenses'; +export * from './Tenancy'; +export * from './View'; diff --git a/server/src/jobs/ComputeItemCost.ts b/server/src/jobs/ComputeItemCost.ts index 9d9458a73..d025dd859 100644 --- a/server/src/jobs/ComputeItemCost.ts +++ b/server/src/jobs/ComputeItemCost.ts @@ -32,7 +32,6 @@ export default class ComputeItemCostJob { Logger.info(`Compute item cost - completed: ${job.attrs.data}`); done(); } catch(e) { - console.log(e); Logger.info(`Compute item cost: ${job.attrs.data}, error: ${e}`); done(e); } diff --git a/server/src/jobs/SendLicenseEmail.ts b/server/src/jobs/SendLicenseEmail.ts index 41410a680..6de2e2366 100644 --- a/server/src/jobs/SendLicenseEmail.ts +++ b/server/src/jobs/SendLicenseEmail.ts @@ -14,7 +14,6 @@ export default class SendLicenseViaEmailJob { Logger.debug(`Send license via email - completed: ${job.attrs.data}`); done(); } catch(e) { - console.log(e); Logger.error(`Send license via email: ${job.attrs.data}, error: ${e}`); done(e); } diff --git a/server/src/jobs/SendLicensePhone.ts b/server/src/jobs/SendLicensePhone.ts index 359087848..797a427fa 100644 --- a/server/src/jobs/SendLicensePhone.ts +++ b/server/src/jobs/SendLicensePhone.ts @@ -15,7 +15,6 @@ export default class SendLicenseViaPhoneJob { Logger.debug(`Send license via phone number - completed: ${job.attrs.data}`); done(); } catch(e) { - console.log(e); Logger.error(`Send license via phone number: ${job.attrs.data}, error: ${e}`); done(e); } diff --git a/server/src/jobs/writeInvoicesJEntries.ts b/server/src/jobs/writeInvoicesJEntries.ts index 5648819aa..46d832182 100644 --- a/server/src/jobs/writeInvoicesJEntries.ts +++ b/server/src/jobs/writeInvoicesJEntries.ts @@ -14,7 +14,6 @@ export default class WriteInvoicesJournalEntries { Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`); done(); } catch(e) { - console.log(e); Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`); done(e); } diff --git a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.js b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts similarity index 53% rename from server/src/lib/DynamicFilter/DynamicFilterFilterRoles.js rename to server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts index 88a870a61..4a8366a8d 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.js +++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts @@ -3,6 +3,7 @@ import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbst import { buildFilterQuery, } from 'lib/ViewRolesBuilder'; +import { IFilterRole } from 'interfaces'; export default class FilterRoles extends DynamicFilterRoleAbstructor { /** @@ -10,31 +11,17 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { * @param {Array} filterRoles - * @param {Array} resourceFields - */ - constructor(filterRoles, resourceFields) { + constructor(filterRoles: IFilterRole[]) { super(); - - this.filterRoles = filterRoles.map((role, index) => ({ - ...role, - index: index + 1, - columnKey: role.field_key, - condition: role.comparator === 'AND' ? '&&' : '||', - })); - this.resourceFields = resourceFields; + this.filterRoles = filterRoles; } - validateFilterRoles() { - const filterFieldsKeys = this.filterRoles.map((r) => r.field_key); - const resourceFieldsKeys = this.resourceFields.map((r) => r.key); - - return difference(filterFieldsKeys, resourceFieldsKeys); - } - - // @private - buildLogicExpression() { + private buildLogicExpression(): string { let expression = ''; this.filterRoles.forEach((role, index) => { - expression += (index === 0) - ? `${role.index} ` : `${role.condition} ${role.index} `; + expression += (index === 0) ? + `${role.index} ` : + `${role.condition} ${role.index} `; }); return expression.trim(); } diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js deleted file mode 100644 index fcb553867..000000000 --- a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js +++ /dev/null @@ -1,29 +0,0 @@ - -export default class DynamicFilterAbstructor { - constructor() { - this.filterRoles = []; - this.tableName = ''; - } - - setTableName(tableName) { - this.tableName = tableName; - } - - /** - * @interface - */ - // eslint-disable-next-line class-methods-use-this - buildLogicExpression() {} - - /** - * @interface - */ - // eslint-disable-next-line class-methods-use-this - validateFilterRoles() {} - - /** - * @interface - */ - // eslint-disable-next-line class-methods-use-this - buildQuery() {} -} \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts new file mode 100644 index 000000000..5ccfe111b --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts @@ -0,0 +1,10 @@ +import { IFilterRole, IDynamicFilter } from "interfaces"; + +export default class DynamicFilterAbstructor implements IDynamicFilter { + filterRoles: IFilterRole[] = []; + tableName: string; + + setTableName(tableName) { + this.tableName = tableName; + } +} \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.js b/server/src/lib/DynamicFilter/DynamicFilterSortBy.js deleted file mode 100644 index a463045f8..000000000 --- a/server/src/lib/DynamicFilter/DynamicFilterSortBy.js +++ /dev/null @@ -1,32 +0,0 @@ -import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; -import { getRoleFieldColumn } from 'lib/ViewRolesBuilder'; - -export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { - constructor(sortByFieldKey, sortDirection) { - super(); - - this.filterRoles = { - columnKey: sortByFieldKey, - value: sortDirection, - comparator: 'sort_by', - }; - } - - /** - * Builds database query of sort by column on the given direction. - */ - buildQuery() { - const { columnKey = null, value = null } = this.filterRoles; - - return (builder) => { - const fieldRelation = getRoleFieldColumn(this.tableName, columnKey); - const comparatorColumn = - fieldRelation.relationColumn || - `${this.tableName}.${fieldRelation.column}`; - - if (columnKey) { - builder.orderBy(`${comparatorColumn}`, value.toLowerCase()); - } - }; - } -} diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts new file mode 100644 index 000000000..32b35f555 --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts @@ -0,0 +1,40 @@ +import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; +import { getRoleFieldColumn, validateFieldKeyExistance } from 'lib/ViewRolesBuilder'; + +export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { + sortRole: { fieldKey: string, order: string } = {}; + + /** + * Constructor method. + * @param {string} sortByFieldKey + * @param {string} sortDirection + */ + constructor(sortByFieldKey: string, sortDirection: string) { + super(); + + this.sortRole = { + fieldKey: sortByFieldKey, + order: sortDirection, + }; + } + + validate() { + validateFieldKeyExistance(this.tableName, this.sortRole.fieldKey); + } + + /** + * Builds database query of sort by column on the given direction. + */ + buildQuery() { + return (builder) => { + const fieldRelation = getRoleFieldColumn(this.tableName, this.sortRole.fieldKey); + const comparatorColumn = + fieldRelation.relationColumn || + `${this.tableName}.${fieldRelation.column}`; + + if (this.sortRole.fieldKey) { + builder.orderBy(`${comparatorColumn}`, this.sortRole.order); + } + }; + } +} diff --git a/server/src/lib/DynamicFilter/DynamicFilterViews.js b/server/src/lib/DynamicFilter/DynamicFilterViews.ts similarity index 77% rename from server/src/lib/DynamicFilter/DynamicFilterViews.js rename to server/src/lib/DynamicFilter/DynamicFilterViews.ts index bbc3a2cce..b02a86aa1 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterViews.js +++ b/server/src/lib/DynamicFilter/DynamicFilterViews.ts @@ -1,3 +1,4 @@ +import { IFilterRole } from 'interfaces'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import { validateViewRoles, @@ -5,20 +6,20 @@ import { } from 'lib/ViewRolesBuilder'; export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { + logicExpression: string; + /** * Constructor method. - * @param {*} filterRoles - - * @param {*} logicExpression - + * @param {*} filterRoles - Filter roles. + * @param {*} logicExpression - Logic expression. */ - constructor(filterRoles, logicExpression) { + constructor(filterRoles: IFilterRole[], logicExpression: string) { super(); this.filterRoles = filterRoles; this.logicExpression = logicExpression .replace('AND', '&&') .replace('OR', '||'); - - this.tableName = ''; } /** @@ -31,7 +32,7 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { /** * Validates filter roles. */ - validateFilterRoles() { + validate() { return validateViewRoles(this.filterRoles, this.logicExpression); } diff --git a/server/src/lib/ViewRolesBuilder/index.js b/server/src/lib/ViewRolesBuilder/index.ts similarity index 65% rename from server/src/lib/ViewRolesBuilder/index.js rename to server/src/lib/ViewRolesBuilder/index.ts index 4b1e10d06..39af35931 100644 --- a/server/src/lib/ViewRolesBuilder/index.js +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -1,20 +1,46 @@ -import { difference } from 'lodash'; +import { difference, filter } from 'lodash'; import moment from 'moment'; import { Lexer } from 'lib/LogicEvaluation/Lexer'; import Parser from 'lib/LogicEvaluation/Parser'; import QueryParser from 'lib/LogicEvaluation/QueryParser'; import resourceFieldsKeys from 'data/ResourceFieldsKeys'; +import { IFilterRole } from 'interfaces'; -// const role = { -// compatotor: String, -// value: String, -// columnKey: String, -// columnSlug: String, -// index: Number, -// } +const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { + switch (role.comparator) { + case 'equals': + case 'equal': + default: + return (builder) => { + builder.where(comparatorColumn, '=', role.value); + }; + case 'not_equals': + case 'not_equal': + return (builder) => { + builder.whereNot(comparatorColumn, role.value); + }; + case 'bigger_than': + case 'bigger': + return (builder) => { + builder.where(comparatorColumn, '>', role.value); + }; + case 'bigger_or_equals': + return (builder) => { + builder.where(comparatorColumn, '>=', role.value); + }; + case 'smaller_than': + case 'smaller': + return (builder) => { + builder.where(comparatorColumn, '<', role.value); + }; + case 'smaller_or_equals': + return (builder) => { + builder.where(comparatorColumn, '<=', role.value); + }; + } +}; - -const textRoleQueryBuilder = (role, comparatorColumn) => { +const textRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { switch (role.comparator) { case 'equals': default: @@ -39,7 +65,7 @@ const textRoleQueryBuilder = (role, comparatorColumn) => { } }; -const dateQueryBuilder = (role, comparatorColumn) => { +const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { switch(role.comparator) { case 'after': case 'before': @@ -81,11 +107,11 @@ const dateQueryBuilder = (role, comparatorColumn) => { /** * Get field column metadata and its relation with other tables. * @param {String} tableName - Table name of target column. - * @param {String} columnKey - Target column key that stored in resource field. + * @param {String} fieldKey - Target column key that stored in resource field. */ -export function getRoleFieldColumn(tableName, columnKey) { +export function getRoleFieldColumn(tableName: string, fieldKey: string) { const tableFields = resourceFieldsKeys[tableName]; - return (tableFields[columnKey]) ? tableFields[columnKey] : null; + return (tableFields[fieldKey]) ? tableFields[fieldKey] : null; } /** @@ -93,13 +119,15 @@ export function getRoleFieldColumn(tableName, columnKey) { * @param {String} tableName - * @param {Object} role - */ -export function buildRoleQuery(tableName, role) { - const fieldRelation = getRoleFieldColumn(tableName, role.columnKey); +export function buildRoleQuery(tableName: string, role: IFilterRole) { + const fieldRelation = getRoleFieldColumn(tableName, role.fieldKey); const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`; switch (fieldRelation.columnType) { + case 'number': + return numberRoleQueryBuilder(role, comparatorColumn); case 'date': - return dateQueryBuilder(role, comparatorColumn); + return dateQueryBuilder(role, comparatorColumn); case 'text': case 'varchar': default: @@ -112,7 +140,7 @@ export function buildRoleQuery(tableName, role) { * @param {String} column - * @return {String} - join relation table. */ -export const getTableFromRelationColumn = (column) => { +export const getTableFromRelationColumn = (column: string) => { const splitedColumn = column.split('.'); return (splitedColumn.length > 0) ? splitedColumn[0] : ''; }; @@ -122,10 +150,10 @@ export const getTableFromRelationColumn = (column) => { * @param {String} tableName - * @param {Array} roles - */ -export function buildFilterRolesJoins(tableName, roles) { +export function buildFilterRolesJoins(tableName: string, roles: IFilterRole[]) { return (builder) => { roles.forEach((role) => { - const fieldColumn = getRoleFieldColumn(tableName, role.columnKey); + const fieldColumn = getRoleFieldColumn(tableName, role.fieldKey); if (fieldColumn.relation) { const joinTable = getTableFromRelationColumn(fieldColumn.relation); @@ -135,7 +163,7 @@ export function buildFilterRolesJoins(tableName, roles) { }; } -export function buildSortColumnJoin(tableName, sortColumnKey) { +export function buildSortColumnJoin(tableName: string, sortColumnKey: string) { return (builder) => { const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey); @@ -152,7 +180,7 @@ export function buildSortColumnJoin(tableName, sortColumnKey) { * @param {Array} roles - * @return {Function} */ -export function buildFilterRolesQuery(tableName, roles, logicExpression = '') { +export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], logicExpression: string = '') { const rolesIndexSet = {}; roles.forEach((role) => { @@ -176,32 +204,12 @@ export function buildFilterRolesQuery(tableName, roles, logicExpression = '') { * @param {Array} roles - * @param {String} logicExpression - */ -export const buildFilterQuery = (tableName, roles, logicExpression) => { +export const buildFilterQuery = (tableName: string, roles, logicExpression: string) => { return (builder) => { buildFilterRolesQuery(tableName, roles, logicExpression)(builder); }; }; -/** - * Validates the view logic expression. - * @param {String} logicExpression - - * @param {Array} indexes - - */ -export function validateFilterLogicExpression(logicExpression, indexes) { - const logicExpIndexes = logicExpression.match(/\d+/g) || []; - return !difference(logicExpIndexes.map(Number), indexes).length; -} - -/** - * Validates view roles. - * @param {Array} roles - - * @param {String} logicExpression - - * @return {Boolean} - */ -export function validateViewRoles(roles, logicExpression) { - return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); -} - /** * Mapes the view roles to view conditionals. * @param {Array} viewRoles - @@ -211,9 +219,10 @@ export function mapViewRolesToConditionals(viewRoles) { return viewRoles.map((viewRole) => ({ comparator: viewRole.comparator, value: viewRole.value, + index: viewRole.index, + columnKey: viewRole.field.key, slug: viewRole.field.slug, - index: viewRole.index, })); } @@ -231,7 +240,7 @@ export function mapFilterRolesToDynamicFilter(roles) { * @param {String} columnKey - * @param {String} sortDirection - */ -export function buildSortColumnQuery(tableName, columnKey, sortDirection) { +export function buildSortColumnQuery(tableName: string, columnKey: string, sortDirection: string) { const fieldRelation = getRoleFieldColumn(tableName, columnKey); const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`; @@ -239,4 +248,27 @@ export function buildSortColumnQuery(tableName, columnKey, sortDirection) { builder.orderBy(sortColumn, sortDirection); buildSortColumnJoin(tableName, columnKey)(builder); }; +} + +export function validateFilterLogicExpression(logicExpression: string, indexes) { + const logicExpIndexes = logicExpression.match(/\d+/g) || []; + const diff = !difference(logicExpIndexes.map(Number), indexes).length; + +} + +export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) { + return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); +} + +export function validateFieldKeyExistance(tableName: string, fieldKey: string) { + if (!resourceFieldsKeys?.[tableName]?.[fieldKey]) + return fieldKey; + else + return false; +} + +export function validateFilterRolesFieldsExistance(tableName, filterRoles: IFilterRole[]) { + return filterRoles.filter((filterRole: IFilterRole) => { + return validateFieldKeyExistance(tableName, filterRole.fieldKey); + }); } \ No newline at end of file diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 4db503ad8..24bf17db7 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -28,11 +28,8 @@ import SaleEstimateEntry from 'models/SaleEstimateEntry'; import PaymentReceive from 'models/PaymentReceive'; import PaymentReceiveEntry from 'models/PaymentReceiveEntry'; import Option from 'models/Option'; -import Resource from 'models/Resource'; import InventoryCostLotTracker from 'models/InventoryCostLotTracker'; import InventoryTransaction from 'models/InventoryTransaction'; -import ResourceField from 'models/ResourceField'; -import ResourceFieldMetadata from 'models/ResourceFieldMetadata'; import ManualJournal from 'models/ManualJournal'; import Media from 'models/Media'; import MediaLink from 'models/MediaLink'; @@ -67,11 +64,8 @@ export default (knex) => { SaleEstimateEntry, PaymentReceive, PaymentReceiveEntry, - Resource, InventoryTransaction, InventoryCostLotTracker, - ResourceField, - ResourceFieldMetadata, Media, MediaLink, Contact, diff --git a/server/src/loaders/tenantRepositories.ts b/server/src/loaders/tenantRepositories.ts index 756a99278..628832103 100644 --- a/server/src/loaders/tenantRepositories.ts +++ b/server/src/loaders/tenantRepositories.ts @@ -3,6 +3,8 @@ import AccountTypeRepository from 'repositories/AccountTypeRepository'; import VendorRepository from 'repositories/VendorRepository'; import CustomerRepository from 'repositories/CustomerRepository'; import ExpenseRepository from 'repositories/ExpenseRepository'; +import ViewRepository from 'repositories/ViewRepository'; +import ViewRoleRepository from 'repositories/ViewRoleRepository'; export default (tenantId: number) => { return { @@ -11,5 +13,7 @@ export default (tenantId: number) => { customerRepository: new CustomerRepository(tenantId), vendorRepository: new VendorRepository(tenantId), expenseRepository: new ExpenseRepository(tenantId), + viewRepository: new ViewRepository(tenantId), + viewRoleRepository: new ViewRoleRepository(tenantId), }; }; \ No newline at end of file diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js deleted file mode 100644 index 123bde984..000000000 --- a/server/src/models/Resource.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Model } from 'objection'; -import TenantModel from 'models/TenantModel'; - -export default class Resource extends TenantModel { - /** - * Table name. - */ - static get tableName() { - return 'resources'; - } - - /** - * Timestamp columns. - */ - static get hasTimestamps() { - return false; - } - - /** - * Relationship mapping. - */ - static get relationMappings() { - const View = require('models/View'); - const ResourceField = require('models/ResourceField'); - - return { - /** - * Resource model may has many views. - */ - views: { - relation: Model.HasManyRelation, - modelClass: this.relationBindKnex(View.default), - join: { - from: 'resources.id', - to: 'views.resourceId', - }, - }, - - /** - * Resource model may has many fields. - */ - fields: { - relation: Model.HasManyRelation, - modelClass: this.relationBindKnex(ResourceField.default), - join: { - from: 'resources.id', - to: 'resource_fields.resourceId', - }, - }, - }; - } -} diff --git a/server/src/models/ResourceField.js b/server/src/models/ResourceField.js deleted file mode 100644 index 76ef71ed7..000000000 --- a/server/src/models/ResourceField.js +++ /dev/null @@ -1,70 +0,0 @@ -import { snakeCase } from 'lodash'; -import { Model } from 'objection'; -import path from 'path'; -import TenantModel from 'models/TenantModel'; - -export default class ResourceField extends TenantModel { - /** - * Table name. - */ - static get tableName() { - return 'resource_fields'; - } - - static get jsonAttributes() { - return ['options']; - } - - /** - * Model modifiers. - */ - static get modifiers() { - return { - whereNotPredefined(query) { - query.whereNot('predefined', true); - }, - }; - } - - /** - * Timestamp columns. - */ - static get hasTimestamps() { - return false; - } - - /** - * Virtual attributes. - */ - static get virtualAttributes() { - return ['key']; - } - - /** - * Resource field key. - */ - key() { - return snakeCase(this.labelName); - } - - /** - * Relationship mapping. - */ - static get relationMappings() { - const Resource = require('models/Resource'); - - return { - /** - * Resource field may belongs to resource model. - */ - resource: { - relation: Model.BelongsToOneRelation, - modelClass: this.relationBindKnex(Resource.default), - join: { - from: 'resource_fields.resourceId', - to: 'resources.id', - }, - }, - }; - } -} diff --git a/server/src/models/ResourceFieldMetadata.js b/server/src/models/ResourceFieldMetadata.js deleted file mode 100644 index 6a9eb152a..000000000 --- a/server/src/models/ResourceFieldMetadata.js +++ /dev/null @@ -1,10 +0,0 @@ -import TenantModel from 'models/TenantModel'; - -export default class ResourceFieldMetadata extends TenantModel { - /** - * Table name. - */ - static get tableName() { - return 'resource_custom_fields_metadata'; - } -} diff --git a/server/src/models/View.js b/server/src/models/View.js index 27e2cb7d0..4eb330084 100644 --- a/server/src/models/View.js +++ b/server/src/models/View.js @@ -12,7 +12,7 @@ export default class View extends TenantModel { /** * Model timestamps. */ - static get timestamps() { + get timestamps() { return ['createdAt', 'updatedAt']; } @@ -40,23 +40,10 @@ export default class View extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const Resource = require('models/Resource'); const ViewColumn = require('models/ViewColumn'); const ViewRole = require('models/ViewRole'); return { - /** - * View model belongs to resource model. - */ - resource: { - relation: Model.BelongsToOneRelation, - modelClass: this.relationBindKnex(Resource.default), - join: { - from: 'views.resourceId', - to: 'resources.id', - }, - }, - /** * View model may has many columns. */ diff --git a/server/src/models/ViewColumn.js b/server/src/models/ViewColumn.js index c216602c8..d3dc38e49 100644 --- a/server/src/models/ViewColumn.js +++ b/server/src/models/ViewColumn.js @@ -13,20 +13,9 @@ export default class ViewColumn extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const ResourceField = require('models/ResourceField'); return { - /** - * View role model may belongs to resource field model. - */ - field: { - relation: Model.BelongsToOneRelation, - modelClass: this.relationBindKnex(ResourceField.default), - join: { - from: 'view_has_columns.fieldId', - to: 'resource_fields.id', - }, - }, + }; } } diff --git a/server/src/models/ViewRole.js b/server/src/models/ViewRole.js index b7f1b37f9..aa9283777 100644 --- a/server/src/models/ViewRole.js +++ b/server/src/models/ViewRole.js @@ -27,7 +27,6 @@ export default class ViewRole extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const ResourceField = require('models/ResourceField'); const View = require('models/View'); return { @@ -42,18 +41,6 @@ export default class ViewRole extends TenantModel { to: 'views.id', }, }, - - /** - * View role model may belongs to resource field model. - */ - field: { - relation: Model.BelongsToOneRelation, - modelClass: this.relationBindKnex(ResourceField.default), - join: { - from: 'view_roles.fieldId', - to: 'resource_fields.id', - }, - }, }; } } diff --git a/server/src/models/index.js b/server/src/models/index.js index cc025cbb0..bd91c577d 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -15,7 +15,6 @@ import PaymentReceiveEntry from './PaymentReceiveEntry'; import Bill from './Bill'; import BillPayment from './BillPayment'; import BillPaymentEntry from './BillPaymentEntry'; -import Resource from './Resource'; import View from './View'; import ItemEntry from './ItemEntry'; import InventoryTransaction from './InventoryTransaction'; @@ -39,7 +38,6 @@ export { Bill, BillPayment, BillPaymentEntry, - Resource, View, ItemEntry, InventoryTransaction, diff --git a/server/src/repositories/ResourceRepository.js b/server/src/repositories/ResourceRepository.js deleted file mode 100644 index 33e4a131f..000000000 --- a/server/src/repositories/ResourceRepository.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Resource } from 'models'; -import BaseModelRepository from 'repositories/BaseModelRepository'; - -export default class ResourceRepository extends BaseModelRepository{ - - static async isExistsByName(name) { - const resourceNames = Array.isArray(name) ? name : [name]; - const foundResources = await Resource.tenant().query().whereIn('name', resourceNames); - - return foundResources.length > 0; - } -} \ No newline at end of file diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts new file mode 100644 index 000000000..5c31253b5 --- /dev/null +++ b/server/src/repositories/ViewRepository.ts @@ -0,0 +1,40 @@ +import { View } from 'models'; +import TenantRepository from 'repositories/TenantRepository'; + +export default class ViewRepository extends TenantRepository { + models: any; + cache: any; + repositories: any; + + /** + * Constructor method. + * @param {number} tenantId - The given tenant id. + */ + constructor( + tenantId: number, + ) { + super(tenantId); + + this.models = this.tenancy.models(tenantId); + this.cache = this.tenancy.cache(tenantId); + this.repositories = this.tenancy.cache(tenantId); + } + + /** + * Retrieve view model by the given id. + * @param {number} id - + */ + getById(id: number) { + const { View } = this.models; + return this.cache.get(`customView.id.${id}`, () => { + return View.query().findById(id); + }); + } + + allByResource() { + const resourceId = 1; + return this.cache.get(`customView.resource.id.${resourceId}`, () => { + return View.query().where('resource_id', resourceId); + }); + } +} \ No newline at end of file diff --git a/server/src/repositories/ViewRoleRepository.ts b/server/src/repositories/ViewRoleRepository.ts new file mode 100644 index 000000000..bbd254f4b --- /dev/null +++ b/server/src/repositories/ViewRoleRepository.ts @@ -0,0 +1,29 @@ +import { omit } from 'lodash'; +import TenantRepository from 'repositories/TenantRepository'; + +export default class ViewRoleRepository extends TenantRepository { + models: any; + cache: any; + repositories: any; + + /** + * Constructor method. + * @param {number} tenantId - The given tenant id. + */ + constructor( + tenantId: number, + ) { + super(tenantId); + + this.models = this.tenancy.models(tenantId); + this.cache = this.tenancy.cache(tenantId); + this.repositories = this.tenancy.cache(tenantId); + } + + allByView(viewId: number) { + const { ViewRole } = this.models; + return this.cache.get(`viewRole.view.${viewId}`, async () => { + return ViewRole.query().where('view_id', viewId); + }); + } +} \ No newline at end of file diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 32c0cf7d7..d36ea7f3a 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -2,15 +2,18 @@ import { Inject, Service } from 'typedi'; import { kebabCase } from 'lodash' import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; -import { IAccountDTO, IAccount } from 'interfaces'; +import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces'; import { difference } from 'lodash'; -import { Account } from 'src/models'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class AccountsService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + @Inject('logger') logger: any; @@ -429,4 +432,22 @@ export default class AccountsService { }) this.logger.info('[account] account have been activated successfully.', { tenantId, accountId }); } + + /** + * Retrieve accounts datatable list. + * @param {number} tenantId + * @param {IAccountsFilter} accountsFilter + */ + async getAccountsList(tenantId: number, filter: IAccountsFilter) { + const { Account } = this.tenancy.models(tenantId); + + const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter); + + this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter }); + const accounts = await Account.query().onBuild((builder) => { + builder.withGraphFetched('type'); + dynamicList.buildQuery()(builder); + }); + return accounts; + } } diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 34c914799..2f49e315d 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -1,14 +1,16 @@ import { Inject, Service } from 'typedi'; -import { difference } from 'lodash'; +import { difference, rest } from 'lodash'; import JournalPoster from "services/Accounting/JournalPoster"; import JournalCommands from "services/Accounting/JournalCommands"; import ContactsService from 'services/Contacts/ContactsService'; import { IVendorNewDTO, IVendorEditDTO, - IVendor + IVendor, + IVendorsFilter } from 'interfaces'; import { ServiceError } from 'exceptions'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; @Service() @@ -19,6 +21,9 @@ export default class VendorsService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + /** * Converts vendor to contact DTO. * @param {IVendorNewDTO|IVendorEditDTO} vendorDTO @@ -186,4 +191,20 @@ export default class VendorsService { throw new ServiceError('some_vendors_have_bills'); } } + + /** + * Retrieve vendors datatable list. + * @param {number} tenantId - Tenant id. + * @param {IVendorsFilter} vendorsFilter - Vendors filter. + */ + async getVendorsList(tenantId: number, vendorsFilter: IVendorsFilter) { + const { Vendor } = this.tenancy.models(tenantId); + + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Vendor, vendorsFilter); + + const vendors = await Vendor.query().onBuild((builder) => { + dynamicFilter.buildQuery()(builder); + }); + return vendors; + } } diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts new file mode 100644 index 000000000..4d036f1ae --- /dev/null +++ b/server/src/services/DynamicListing/DynamicListService.ts @@ -0,0 +1,167 @@ +import { Service, Inject } from "typedi"; +import validator from 'is-my-json-valid'; +import { Router, Request, Response, NextFunction } from 'express'; +import { ServiceError } from 'exceptions'; +import { + DynamicFilter, + DynamicFilterSortBy, + DynamicFilterViews, + DynamicFilterFilterRoles, +} from 'lib/DynamicFilter'; +import { + validateFieldKeyExistance, + validateFilterRolesFieldsExistance, +} from 'lib/ViewRolesBuilder'; + +import TenancyService from 'services/Tenancy/TenancyService'; +import { IDynamicListFilterDTO, IFilterRole } from 'interfaces'; + +const ERRORS = { + VIEW_NOT_FOUND: 'view_not_found', + SORT_COLUMN_NOT_FOUND: 'sort_column_not_found', + FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found', +}; + +@Service() +export default class DynamicListService { + @Inject() + tenancy: TenancyService; + + /** + * Middleware to catch services errors + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'sort_column_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'SORT.COLUMN.NOT.FOUND', code: 200 }], + }); + } + if (error.errorType === 'view_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }] + }) + } + if (error.errorType === 'filter_roles_fields_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'FILTER.ROLES.FIELDS.NOT.FOUND', code: 300 }], + }); + } + if (error.errorType === 'stringified_filter_roles_invalid') { + return res.boom.badRequest(null, { + errors: [{ type: 'STRINGIFIED_FILTER_ROLES_INVALID', code: 400 }], + }); + } + } + next(error); + } + + /** + * Retreive custom view or throws error not found. + * @param {number} tenantId + * @param {number} viewId + * @return {Promise} + */ + async getCustomViewOrThrowError(tenantId: number, viewId: number) { + const { viewRepository } = this.tenancy.repositories(tenantId); + const view = await viewRepository.getById(viewId); + + if (!view || view.resourceModel !== 'Account') { + throw new ServiceError(ERRORS.VIEW_NOT_FOUND); + } + return view; + } + + /** + * Validates the sort column whether exists. + * @param {IModel} model + * @param {string} columnSortBy - Sort column + * @throws {ServiceError} + */ + validateSortColumnExistance(model: any, columnSortBy: string) { + const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy); + + if (notExistsField) { + throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND); + } + } + + /** + * Validates existance the fields of filter roles. + * @param {IModel} model + * @param {IFilterRole[]} filterRoles + * @throws {ServiceError} + */ + validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) { + const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles); + + if (invalidFieldsKeys.length > 0) { + throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); + } + } + + /** + * Validates filter roles schema. + * @param {IFilterRole[]} filterRoles + */ + validateFilterRolesSchema(filterRoles: IFilterRole[]) { + const validate = validator({ + required: true, + type: 'object', + properties: { + fieldKey: { required: true, type: 'string' }, + value: { required: true, type: 'string' }, + }, + }); + const invalidFields = filterRoles.filter((filterRole) => { + const isValid = validate(filterRole); + return isValid ? false : true; + }); + if (invalidFields.length > 0) { + throw new ServiceError('stringified_filter_roles_invalid'); + } + } + + /** + * Dynamic listing. + * @param {number} tenantId + * @param {IModel} model + * @param {IAccountsFilter} filter + */ + async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) { + const { viewRoleRepository } = this.tenancy.repositories(tenantId); + const dynamicFilter = new DynamicFilter(model.tableName); + + // Custom view filter roles. + if (filter.customViewId) { + const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId); + const viewRoles = await viewRoleRepository.allByView(view.id); + + const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression); + dynamicFilter.setFilter(viewFilter); + } + // Sort by the given column. + if (filter.columnSortBy) { + this.validateSortColumnExistance(model, filter.columnSortBy);; + + const sortByFilter = new DynamicFilterSortBy( + filter.columnSortBy, filter.sortOrder + ); + dynamicFilter.setFilter(sortByFilter); + } + // Filter roles. + if (filter.filterRoles.length > 0) { + this.validateFilterRolesSchema(filter.filterRoles); + this.validateRolesFieldsExistance(model, filter.filterRoles); + + // Validate the accounts resource fields. + const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles); + dynamicFilter.setFilter(filterRoles); + } + return dynamicFilter; + } +} \ No newline at end of file diff --git a/server/src/services/DynamicListing/DynamicListing.js b/server/src/services/DynamicListing/DynamicListing.js deleted file mode 100644 index b81ab1456..000000000 --- a/server/src/services/DynamicListing/DynamicListing.js +++ /dev/null @@ -1,75 +0,0 @@ - -import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterViews, - DynamicFilterFilterRoles, -} from 'lib/DynamicFilter'; -import { - mapViewRolesToConditionals, - mapFilterRolesToDynamicFilter, -} from 'lib/ViewRolesBuilder'; - -export const DYNAMIC_LISTING_ERRORS = { - LOGIC_INVALID: 'VIEW.LOGIC.EXPRESSION.INVALID', - RESOURCE_HAS_NO_FIELDS: 'RESOURCE.HAS.NO.GIVEN.FIELDS', -}; - -export default class DynamicListing { - - /** - * Constructor method. - * @param {DynamicListingBuilder} dynamicListingBuilder - * @return {DynamicListing|Error} - */ - constructor(dynamicListingBuilder) { - this.listingBuilder = dynamicListingBuilder; - this.dynamicFilter = new DynamicFilter(this.listingBuilder.modelClass.tableName); - return this.init(); - } - - /** - * Initialize the dynamic listing. - */ - init() { - // Initialize the column sort by. - if (this.listingBuilder.columnSortBy) { - const sortByFilter = new DynamicFilterSortBy( - filter.column_sort_by, - filter.sort_order - ); - this.dynamicFilter.setFilter(sortByFilter); - } - // Initialize the view filter roles. - if (this.listingBuilder.view && this.listingBuilder.view.roles.length > 0) { - const viewFilter = new DynamicFilterViews( - mapViewRolesToConditionals(this.listingBuilder.view.roles), - this.listingBuilder.view.rolesLogicExpression - ); - if (!viewFilter.validateFilterRoles()) { - return new Error(DYNAMIC_LISTING_ERRORS.LOGIC_INVALID); - } - this.dynamicFilter.setFilter(viewFilter); - } - // Initialize the dynamic filter roles. - if (this.listingBuilder.filterRoles.length > 0) { - const filterRoles = new DynamicFilterFilterRoles( - mapFilterRolesToDynamicFilter(filter.filter_roles), - accountsResource.fields - ); - this.dynamicFilter.setFilter(filterRoles); - - if (filterRoles.validateFilterRoles().length > 0) { - return new Error(DYNAMIC_LISTING_ERRORS.RESOURCE_HAS_NO_FIELDS); - } - } - return this; - } - - /** - * Build query. - */ - buildQuery(){ - return this.dynamicFilter.buildQuery(); - } -} \ No newline at end of file diff --git a/server/src/services/DynamicListing/DynamicListingBuilder.js b/server/src/services/DynamicListing/DynamicListingBuilder.js deleted file mode 100644 index 4674cfee5..000000000 --- a/server/src/services/DynamicListing/DynamicListingBuilder.js +++ /dev/null @@ -1,25 +0,0 @@ - - -export default class DynamicListingBuilder { - - addModelClass(modelClass) { - this.modelClass = modelClass; - } - - addCustomViewId(customViewId) { - this.customViewId = customViewId; - } - - addFilterRoles (filterRoles) { - this.filterRoles = filterRoles; - } - - addSortBy(sortBy, sortOrder) { - this.sortBy = sortBy; - this.sortOrder = sortOrder; - } - - addView(view) { - this.view = view; - } -} \ No newline at end of file diff --git a/server/src/services/DynamicListing/HasDynamicListing.js b/server/src/services/DynamicListing/HasDynamicListing.js deleted file mode 100644 index 286fb0ab9..000000000 --- a/server/src/services/DynamicListing/HasDynamicListing.js +++ /dev/null @@ -1,22 +0,0 @@ -import { DYNAMIC_LISTING_ERRORS } from 'services/DynamicListing/DynamicListing'; - -export const dynamicListingErrorsToResponse = (error) => { - let _errors; - - if (error.message === DYNAMIC_LISTING_ERRORS.LOGIC_INVALID) { - _errors.push({ - type: DYNAMIC_LISTING_ERRORS.LOGIC_INVALID, - code: 200, - }); - } - if ( - error.message === - DYNAMIC_LISTING_ERRORS.RESOURCE_HAS_NO_FIELDS - ) { - _errors.push({ - type: DYNAMIC_LISTING_ERRORS.RESOURCE_HAS_NO_FIELDS, - code: 300, - }); - } - return _errors; -}; \ No newline at end of file diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 1d29cec6b..5b8b08527 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -7,6 +7,7 @@ import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import JournalCommands from 'services/Accounting/JournalCommands'; import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; const ERRORS = { EXPENSE_NOT_FOUND: 'expense_not_found', @@ -23,6 +24,9 @@ export default class ExpensesService implements IExpensesService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + @Inject('logger') logger: any; @@ -429,4 +433,24 @@ export default class ExpensesService implements IExpensesService { this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds }); } + + /** + * Retrieve expenses datatable lsit. + * @param {number} tenantId + * @param {IExpensesFilter} expensesFilter + * @return {IExpense[]} + */ + async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) { + const { Expense } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter); + + this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter }); + const expenses = await Expense.query().onBuild((builder) => { + builder.withGraphFetched('paymentAccount'); + builder.withGraphFetched('user'); + + dynamicFilter.buildQuery()(builder); + }); + return expenses; + } } \ No newline at end of file diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 9961a3ab3..469721aee 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -1,5 +1,7 @@ import { difference } from "lodash"; import { Service, Inject } from "typedi"; +import { IItemsFilter } from 'interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; @Service() @@ -7,6 +9,9 @@ export default class ItemsService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + async newItem(tenantId: number, item: any) { const { Item } = this.tenancy.models(tenantId); const storedItem = await Item.query() @@ -71,4 +76,24 @@ export default class ItemsService { writeItemInventoryOpeningQuantity(tenantId: number, itemId: number, openingQuantity: number, averageCost: number) { } + + /** + * Retrieve items datatable list. + * @param {number} tenantId + * @param {IItemsFilter} itemsFilter + */ + async getItemsList(tenantId: number, itemsFilter: IItemsFilter) { + const { Item } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter); + + const items = await Item.query().onBuild((builder) => { + builder.withGraphFetched('inventoryAccount'); + builder.withGraphFetched('sellAccount'); + builder.withGraphFetched('costAccount'); + builder.withGraphFetched('category'); + + dynamicFilter.buildQuery()(builder); + }); + return items; + } } \ No newline at end of file diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index 915ebcf4b..8c3b2479d 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -71,7 +71,7 @@ export default class HasTenancyService { repositories(tenantId: number) { return this.singletonService(tenantId, 'repositories', () => { return tenantRepositoriesLoader(tenantId); - }) + }); } /**