From 9186076676995ef13a2dce52445902b87ae21a58 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Thu, 29 Jul 2021 08:46:41 +0200 Subject: [PATCH] feat: optimize dynamic list service. feat: inactive mode for accounts, items, customers and vendors services. --- server/src/api/controllers/Accounts.ts | 58 ++-- .../src/api/controllers/Contacts/Customers.ts | 8 +- .../src/api/controllers/Contacts/Vendors.ts | 9 +- server/src/api/controllers/Expenses.ts | 4 - .../FinancialStatements/JournalSheet.ts | 9 +- server/src/api/controllers/ItemCategories.ts | 2 +- server/src/api/controllers/Items.ts | 9 +- server/src/api/controllers/ManualJournals.ts | 4 - server/src/api/controllers/Purchases/Bills.ts | 6 +- server/src/api/controllers/Resources.ts | 100 ++++-- .../api/controllers/Sales/PaymentReceives.ts | 4 - .../api/controllers/Sales/SalesEstimates.ts | 147 +++++---- .../api/controllers/Sales/SalesInvoices.ts | 12 +- .../api/controllers/Sales/SalesReceipts.ts | 4 - server/src/interfaces/Account.ts | 1 + server/src/interfaces/Bill.ts | 2 + server/src/interfaces/DynamicFilter.ts | 13 +- server/src/interfaces/Item.ts | 1 + server/src/interfaces/JournalReport.ts | 4 +- server/src/interfaces/Model.ts | 79 ++++- server/src/lib/DynamicFilter/DynamicFilter.ts | 73 +++-- .../DynamicFilter/DynamicFilterAbstructor.ts | 37 +++ .../DynamicFilter/DynamicFilterFilterRoles.ts | 23 +- .../DynamicFilter/DynamicFilterQueryParser.ts | 61 ++++ .../DynamicFilterRoleAbstructor.ts | 303 +++++++++++++++++- .../lib/DynamicFilter/DynamicFilterSortBy.ts | 34 +- server/src/lib/DynamicFilter/constants.ts | 36 +++ server/src/lib/ViewRolesBuilder/index.ts | 230 +------------ server/src/models/Account.Settings.ts | 63 ++++ server/src/models/Account.js | 144 +++------ server/src/models/Bill.Settings.ts | 79 +++++ server/src/models/Bill.js | 131 +++----- server/src/models/BillPayment.Settings.ts | 61 ++++ server/src/models/BillPayment.js | 79 +---- server/src/models/Customer.Settings.ts | 98 ++++++ server/src/models/Customer.js | 93 ++---- server/src/models/Expense.Settings.ts | 82 +++++ server/src/models/Expense.js | 80 +---- .../models/InventoryAdjustment.Settings.ts | 60 ++++ server/src/models/InventoryAdjustment.js | 54 +--- server/src/models/Item.Settings.ts | 102 ++++++ server/src/models/Item.js | 146 ++------- server/src/models/ItemCategory.Settings.ts | 41 +++ server/src/models/ItemCategory.js | 62 ---- server/src/models/ManualJournal.Settings.ts | 54 ++++ server/src/models/ManualJournal.js | 55 +--- server/src/models/ModelSetting.ts | 54 ++++ server/src/models/PaymentReceive.Settings.ts | 43 +++ server/src/models/PaymentReceive.js | 66 +--- server/src/models/SaleEstimate.Settings.ts | 85 +++++ server/src/models/SaleEstimate.js | 110 +------ server/src/models/SaleInvoice.Settings.ts | 86 +++++ server/src/models/SaleInvoice.js | 166 +++------- server/src/models/SaleReceipt.Settings.ts | 87 +++++ server/src/models/SaleReceipt.js | 101 +----- server/src/models/Vendor.Settings.ts | 89 +++++ server/src/models/Vendor.js | 82 +---- .../src/services/Accounts/AccountsService.ts | 30 +- .../src/services/Contacts/CustomersService.ts | 28 +- .../src/services/Contacts/VendorsService.ts | 28 +- .../DynamicListing/DynamicListAbstruct.ts | 6 + .../DynamicListing/DynamicListCustomView.ts | 52 +++ .../DynamicListing/DynamicListFilterRoles.ts | 103 ++++++ .../DynamicListing/DynamicListService.ts | 218 ++++++------- .../DynamicListing/DynamicListSortBy.ts | 40 +++ .../src/services/DynamicListing/constants.ts | 6 + .../src/services/DynamicListing/validators.ts | 0 .../src/services/Expenses/ExpensesService.ts | 33 +- .../JournalSheet/JournalSheetService.ts | 8 +- .../Inventory/InventoryAdjustmentService.ts | 26 +- .../ItemCategories/ItemCategoriesService.ts | 20 +- server/src/services/Items/ItemsService.ts | 26 +- .../ManualJournals/ManualJournalsService.ts | 31 +- .../Purchases/BillPayments/BillPayments.ts | 29 +- server/src/services/Purchases/Bills.ts | 24 +- .../src/services/Resource/ResourceService.ts | 16 +- .../Sales/PaymentReceives/PaymentsReceives.ts | 31 +- server/src/services/Sales/SalesEstimate.ts | 24 +- server/src/services/Sales/SalesInvoices.ts | 27 +- server/src/services/Sales/SalesReceipts.ts | 22 +- 80 files changed, 2748 insertions(+), 1806 deletions(-) create mode 100644 server/src/lib/DynamicFilter/DynamicFilterAbstructor.ts create mode 100644 server/src/lib/DynamicFilter/DynamicFilterQueryParser.ts create mode 100644 server/src/lib/DynamicFilter/constants.ts create mode 100644 server/src/models/Account.Settings.ts create mode 100644 server/src/models/Bill.Settings.ts create mode 100644 server/src/models/BillPayment.Settings.ts create mode 100644 server/src/models/Customer.Settings.ts create mode 100644 server/src/models/Expense.Settings.ts create mode 100644 server/src/models/InventoryAdjustment.Settings.ts create mode 100644 server/src/models/Item.Settings.ts create mode 100644 server/src/models/ItemCategory.Settings.ts create mode 100644 server/src/models/ManualJournal.Settings.ts create mode 100644 server/src/models/ModelSetting.ts create mode 100644 server/src/models/PaymentReceive.Settings.ts create mode 100644 server/src/models/SaleEstimate.Settings.ts create mode 100644 server/src/models/SaleInvoice.Settings.ts create mode 100644 server/src/models/SaleReceipt.Settings.ts create mode 100644 server/src/models/Vendor.Settings.ts create mode 100644 server/src/services/DynamicListing/DynamicListAbstruct.ts create mode 100644 server/src/services/DynamicListing/DynamicListCustomView.ts create mode 100644 server/src/services/DynamicListing/DynamicListFilterRoles.ts create mode 100644 server/src/services/DynamicListing/DynamicListSortBy.ts create mode 100644 server/src/services/DynamicListing/constants.ts create mode 100644 server/src/services/DynamicListing/validators.ts diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index e48f1f9e3..f89c4d63a 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -29,11 +29,9 @@ export default class AccountsController extends BaseController { router.get( '/transactions', - [ - query('account_id').optional().isInt().toInt(), - ], + [query('account_id').optional().isInt().toInt()], this.asyncMiddleware(this.accountTransactions.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); router.post( '/:id/activate', @@ -136,6 +134,8 @@ export default class AccountsController extends BaseController { query('column_sort_by').optional(), query('sort_order').optional().isIn(['desc', 'asc']), + + query('inactive_mode').optional().isBoolean().toBoolean(), ]; } @@ -213,7 +213,9 @@ export default class AccountsController extends BaseController { tenantId, accountId ); - return res.status(200).send({ account: this.transfromToResponse(account) }); + return res + .status(200) + .send({ account: this.transfromToResponse(account) }); } catch (error) { next(error); } @@ -256,7 +258,7 @@ export default class AccountsController extends BaseController { return res.status(200).send({ id: accountId, - message: 'The account has been activated successfully.' + message: 'The account has been activated successfully.', }); } catch (error) { next(error); @@ -291,22 +293,24 @@ export default class AccountsController extends BaseController { * @param {Response} res * @param {Response} */ - async getAccountsList(req: Request, res: Response, next: NextFunction) { + public async getAccountsList( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; - const filter: IAccountsFilter = { - filterRoles: [], + + // Filter query. + const filter = { sortOrder: 'asc', columnSortBy: 'name', + inactiveMode: false, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } + try { - const { - accounts, - filterMeta, - } = await this.accountsService.getAccountsList(tenantId, filter); + const { accounts, filterMeta } = + await this.accountsService.getAccountsList(tenantId, filter); return res.status(200).send({ accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req), @@ -343,9 +347,9 @@ export default class AccountsController extends BaseController { /** * Retrieve accounts transactions list. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next * @returns {Response} */ async accountTransactions(req: Request, res: Response, next: NextFunction) { @@ -353,10 +357,11 @@ export default class AccountsController extends BaseController { const transactionsFilter = this.matchedQueryData(req); try { - const { transactions } = await this.accountsService.getAccountsTransactions( - tenantId, - transactionsFilter - ); + const { transactions } = + await this.accountsService.getAccountsTransactions( + tenantId, + transactionsFilter + ); return res.status(200).send({ transactions: this.transfromToResponse(transactions), }); @@ -372,7 +377,12 @@ export default class AccountsController extends BaseController { * @param {Response} res * @param {ServiceError} error */ - catchServiceErrors(error, req: Request, res: Response, next: NextFunction) { + private 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.', { diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 97041a348..3b7e5734f 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -120,6 +120,8 @@ export default class CustomersController extends ContactsController { query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), + + query('inactive_mode').optional().isBoolean().toBoolean(), ]; } @@ -264,17 +266,15 @@ export default class CustomersController extends ContactsController { */ async getCustomersList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const filter = { - filterRoles: [], + inactiveMode: false, sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } try { const { diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index d47ec60fd..53ad0e61a 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -100,6 +100,8 @@ export default class VendorsController extends ContactsController { query('page').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(), + + query('inactive_mode').optional().isBoolean().toBoolean(), ]; } @@ -227,8 +229,13 @@ export default class VendorsController extends ContactsController { */ async getVendorsList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const vendorsFilter: IVendorsFilter = { - filterRoles: [], + inactiveMode: false, + sortOrder: 'asc', + columnSortBy: 'created_at', + page: 1, + pageSize: 12, ...this.matchedQueryData(req), }; diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index b6010d706..5740dc316 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -290,16 +290,12 @@ export default class ExpensesController extends BaseController { async getExpensesList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } try { const { expenses, pagination, filterMeta } = diff --git a/server/src/api/controllers/FinancialStatements/JournalSheet.ts b/server/src/api/controllers/FinancialStatements/JournalSheet.ts index 86748f6a4..6c82e7a42 100644 --- a/server/src/api/controllers/FinancialStatements/JournalSheet.ts +++ b/server/src/api/controllers/FinancialStatements/JournalSheet.ts @@ -32,13 +32,8 @@ export default class JournalSheetController extends BaseFinancialReportControlle return [ query('from_date').optional().isISO8601(), query('to_date').optional().isISO8601(), - oneOf( - [ - query('transaction_types').optional().isArray({ min: 1 }), - query('transaction_types.*').optional().isNumeric().toInt(), - ], - [query('transaction_types').optional().trim().escape()] - ), + query('transaction_type').optional().trim().escape(), + query('transaction_id').optional().isInt().toInt(), oneOf( [ query('account_ids').optional().isArray({ min: 1 }), diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index 73b6acf8e..89cdb7a02 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -199,8 +199,8 @@ export default class ItemsCategoriesController extends BaseController { */ async getList(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; + const itemCategoriesFilter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', ...this.matchedQueryData(req), diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index ff30657e3..d4b4432cb 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -185,6 +185,8 @@ export default class ItemsController extends BaseController { query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), + + query('inactive_mode').optional().isBoolean().toBoolean(), ]; } @@ -339,17 +341,16 @@ export default class ItemsController extends BaseController { */ async getItemsList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const filter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, + inactiveMode: false, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } + try { const { items, diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index 288adc3e8..9473ec588 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -288,14 +288,10 @@ export default class ManualJournalsController extends BaseController { const filter = { sortOrder: 'asc', columnSortBy: 'created_at', - filterRoles: [], page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } try { const { manualJournals, diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index c119bcfce..93664aee4 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -1,5 +1,6 @@ import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query } from 'express-validator'; +import * as R from 'ramda'; import { Service, Inject } from 'typedi'; import { IBillDTO, IBillEditDTO } from 'interfaces'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; @@ -300,14 +301,11 @@ export default class BillsController extends BaseController { const filter = { page: 1, pageSize: 12, - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } + try { const { bills, pagination, filterMeta } = await this.billsService.getBills(tenantId, filter); diff --git a/server/src/api/controllers/Resources.ts b/server/src/api/controllers/Resources.ts index 013273db3..a28bb6045 100644 --- a/server/src/api/controllers/Resources.ts +++ b/server/src/api/controllers/Resources.ts @@ -1,16 +1,13 @@ import { Service, Inject } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; -import { - param, - query, -} from 'express-validator'; +import { param, query } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BaseController from './BaseController'; import { ServiceError } from 'exceptions'; import ResourceService from 'services/Resource/ResourceService'; @Service() -export default class ResourceController extends BaseController{ +export default class ResourceController extends BaseController { @Inject() resourcesService: ResourceService; @@ -21,42 +18,76 @@ export default class ResourceController extends BaseController{ const router = Router(); router.get( - '/:resource_model/fields', [ - ...this.resourceModelParamSchema, - ], + '/:resource_model/meta', + [...this.resourceModelParamSchema], + this.asyncMiddleware(this.resourceMeta.bind(this)), + this.handleServiceErrors + ); + + router.get( + '/:resource_model/fields', + [...this.resourceModelParamSchema], this.validationResult, asyncMiddleware(this.resourceFields.bind(this)), this.handleServiceErrors ); router.get( - '/:resource_model/data', [ - ...this.resourceModelParamSchema, - ], + '/:resource_model/data', + [...this.resourceModelParamSchema], this.validationResult, asyncMiddleware(this.resourceData.bind(this)), - this.handleServiceErrors, - ) + this.handleServiceErrors + ); return router; } get resourceModelParamSchema() { - return [ - param('resource_model').exists().trim().escape(), - ]; + return [param('resource_model').exists().trim().escape()]; } + /** + * Retrieve resource model meta. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + * @returns {Response} + */ + private resourceMeta = ( + req: Request, + res: Response, + next: NextFunction + ): Response => { + const { tenantId } = req; + const { resource_model: resourceModel } = req.params; + + try { + const resourceMeta = this.resourcesService.getResourceMeta( + tenantId, + resourceModel + ); + return res + .status(200) + .send({ resource_meta: this.transfromToResponse(resourceMeta) }); + } catch (error) { + next(error); + } + }; + /** * Retrieve resource fields of the given resource. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ resourceFields(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { resource_model: resourceModel } = req.params; try { - const resourceFields = this.resourcesService.getResourceFields(tenantId, resourceModel); + const resourceFields = this.resourcesService.getResourceFields( + tenantId, + resourceModel + ); return res.status(200).send({ resource_fields: this.transfromToResponse(resourceFields), @@ -68,9 +99,9 @@ export default class ResourceController extends BaseController{ /** * Retrieve resource data of the give resource based on the given query. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async resourceData(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; @@ -78,7 +109,11 @@ export default class ResourceController extends BaseController{ const filter = req.query; try { - const resourceData = await this.resourcesService.getResourceData(tenantId, resourceModel, filter); + const resourceData = await this.resourcesService.getResourceData( + tenantId, + resourceModel, + filter + ); return res.status(200).send({ resource_data: this.transfromToResponse(resourceData), @@ -90,12 +125,17 @@ export default class ResourceController extends BaseController{ /** * Handles service errors. - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + private handleServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { return res.status(400).send({ @@ -105,4 +145,4 @@ export default class ResourceController extends BaseController{ } next(error); } -}; +} diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 37c958978..aad0dad9b 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -261,16 +261,12 @@ export default class PaymentReceivesController extends BaseController { async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } try { const { diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 164f5c945..f723980d2 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -2,11 +2,11 @@ import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { Inject, Service } from 'typedi'; import { ISaleEstimateDTO } from 'interfaces'; -import BaseController from 'api/controllers/BaseController' +import BaseController from 'api/controllers/BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; -import { ServiceError } from "exceptions"; +import { ServiceError } from 'exceptions'; @Service() export default class SalesEstimatesController extends BaseController { @@ -23,63 +23,56 @@ export default class SalesEstimatesController extends BaseController { const router = Router(); router.post( - '/', [ - ...this.estimateValidationSchema, - ], + '/', + [...this.estimateValidationSchema], this.validationResult, asyncMiddleware(this.newEstimate.bind(this)), - this.handleServiceErrors, + this.handleServiceErrors ); router.post( '/:id/deliver', - [ - ...this.validateSpecificEstimateSchema, - ], + [...this.validateSpecificEstimateSchema], this.validationResult, asyncMiddleware(this.deliverSaleEstimate.bind(this)), - this.handleServiceErrors, + this.handleServiceErrors ); router.post( '/:id/approve', - [ - this.validateSpecificEstimateSchema, - ], + [this.validateSpecificEstimateSchema], this.validationResult, asyncMiddleware(this.approveSaleEstimate.bind(this)), this.handleServiceErrors ); router.post( '/:id/reject', - [ - this.validateSpecificEstimateSchema, - ], + [this.validateSpecificEstimateSchema], this.validationResult, asyncMiddleware(this.rejectSaleEstimate.bind(this)), - this.handleServiceErrors, - ) + this.handleServiceErrors + ); router.post( - '/:id', [ + '/:id', + [ ...this.validateSpecificEstimateSchema, ...this.estimateValidationSchema, ], this.validationResult, asyncMiddleware(this.editEstimate.bind(this)), - this.handleServiceErrors, + this.handleServiceErrors ); router.delete( - '/:id', [ - this.validateSpecificEstimateSchema, - ], + '/:id', + [this.validateSpecificEstimateSchema], this.validationResult, asyncMiddleware(this.deleteEstimate.bind(this)), - this.handleServiceErrors, + this.handleServiceErrors ); router.get( '/:id', this.validateSpecificEstimateSchema, this.validationResult, asyncMiddleware(this.getEstimate.bind(this)), - this.handleServiceErrors, + this.handleServiceErrors ); router.get( '/', @@ -87,7 +80,7 @@ export default class SalesEstimatesController extends BaseController { this.validationResult, asyncMiddleware(this.getEstimates.bind(this)), this.handleServiceErrors, - this.dynamicListService.handlerErrorsToResponse, + this.dynamicListService.handlerErrorsToResponse ); return router; } @@ -109,8 +102,14 @@ export default class SalesEstimatesController extends BaseController { check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.quantity').exists().isNumeric().toInt(), check('entries.*.rate').exists().isNumeric().toFloat(), - check('entries.*.description').optional({ nullable: true }).trim().escape(), - check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(), + check('entries.*.description') + .optional({ nullable: true }) + .trim() + .escape(), + check('entries.*.discount') + .optional({ nullable: true }) + .isNumeric() + .toFloat(), check('note').optional().trim().escape(), check('terms_conditions').optional().trim().escape(), @@ -122,9 +121,7 @@ export default class SalesEstimatesController extends BaseController { * Specific sale estimate validation schema. */ get validateSpecificEstimateSchema() { - return [ - param('id').exists().isNumeric().toInt(), - ]; + return [param('id').exists().isNumeric().toInt()]; } /** @@ -137,8 +134,8 @@ export default class SalesEstimatesController extends BaseController { query('column_sort_by').optional(), query('sort_order').optional().isIn(['desc', 'asc']), query('page').optional().isNumeric().toInt(), - query('page_size').optional().isNumeric().toInt(), - ] + query('page_size').optional().isNumeric().toInt(), + ]; } /** @@ -152,7 +149,10 @@ export default class SalesEstimatesController extends BaseController { const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); try { - const storedEstimate = await this.saleEstimateService.createEstimate(tenantId, estimateDTO); + const storedEstimate = await this.saleEstimateService.createEstimate( + tenantId, + estimateDTO + ); return res.status(200).send({ id: storedEstimate.id, @@ -165,8 +165,8 @@ export default class SalesEstimatesController extends BaseController { /** * Handle update estimate details with associated entries. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ async editEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; @@ -175,12 +175,16 @@ export default class SalesEstimatesController extends BaseController { try { // Update estimate with associated estimate entries. - await this.saleEstimateService.editEstimate(tenantId, estimateId, estimateDTO); + await this.saleEstimateService.editEstimate( + tenantId, + estimateId, + estimateDTO + ); return res.status(200).send({ id: estimateId, message: 'The sale estimate has been created successfully.', - }); + }); } catch (error) { next(error); } @@ -188,8 +192,8 @@ export default class SalesEstimatesController extends BaseController { /** * Deletes the given estimate with associated entries. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ async deleteEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; @@ -200,7 +204,7 @@ export default class SalesEstimatesController extends BaseController { return res.status(200).send({ id: estimateId, - message: 'The sale estimate has been deleted successfully.' + message: 'The sale estimate has been deleted successfully.', }); } catch (error) { next(error); @@ -209,8 +213,8 @@ export default class SalesEstimatesController extends BaseController { /** * Deliver the given sale estimate. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; @@ -230,9 +234,9 @@ export default class SalesEstimatesController extends BaseController { /** * Marks the sale estimate as approved. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async approveSaleEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; @@ -252,9 +256,9 @@ export default class SalesEstimatesController extends BaseController { /** * Marks the sale estimate as rejected. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; @@ -274,16 +278,19 @@ export default class SalesEstimatesController extends BaseController { /** * Retrieve the given estimate with associated entries. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async getEstimate(req: Request, res: Response, next: NextFunction) { const { id: estimateId } = req.params; const { tenantId } = req; try { - const estimate = await this.saleEstimateService.getEstimate(tenantId, estimateId); + const estimate = await this.saleEstimateService.getEstimate( + tenantId, + estimateId + ); return res.status(200).send({ estimate }); } catch (error) { @@ -293,35 +300,28 @@ export default class SalesEstimatesController extends BaseController { /** * Retrieve estimates with pagination metadata. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res */ async getEstimates(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } try { - const { - salesEstimates, - pagination, - filterMeta - } = await this.saleEstimateService.estimatesList(tenantId, filter); + const { salesEstimates, pagination, filterMeta } = + await this.saleEstimateService.estimatesList(tenantId, filter); return res.status(200).send({ sales_estimates: this.transfromToResponse(salesEstimates), pagination, filter_meta: this.transfromToResponse(filterMeta), - }) + }); } catch (error) { next(error); } @@ -329,12 +329,17 @@ export default class SalesEstimatesController extends BaseController { /** * Handles service errors. - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + private handleServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'ITEMS_NOT_FOUND') { return res.boom.badRequest(null, { @@ -409,4 +414,4 @@ export default class SalesEstimatesController extends BaseController { } next(error); } -}; +} diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 7b8641c36..df04fe5c0 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -284,23 +284,15 @@ export default class SaleInvoicesController extends BaseController { ) { const { tenantId } = req; const filter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } - try { - const { - salesInvoices, - filterMeta, - pagination, - } = await this.saleInvoiceService.salesInvoicesList(tenantId, filter); + const { salesInvoices, filterMeta, pagination } = + await this.saleInvoiceService.salesInvoicesList(tenantId, filter); return res.status(200).send({ sales_invoices: salesInvoices, diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index f8cf3658d..9c621e7c0 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -230,16 +230,12 @@ export default class SalesReceiptsController extends BaseController { async getSalesReceipts(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const filter = { - filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', page: 1, pageSize: 12, ...this.matchedQueryData(req), }; - if (filter.stringifiedFilterRoles) { - filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); - } try { const { diff --git a/server/src/interfaces/Account.ts b/server/src/interfaces/Account.ts index 60cf1dbee..fa978e63b 100644 --- a/server/src/interfaces/Account.ts +++ b/server/src/interfaces/Account.ts @@ -47,6 +47,7 @@ export interface IAccountResponse extends IAccount { export interface IAccountsFilter extends IDynamicListFilterDTO { stringifiedFilterRoles?: string, + onlyInactive: boolean; }; export interface IAccountType { diff --git a/server/src/interfaces/Bill.ts b/server/src/interfaces/Bill.ts index ea68100c3..db22b9a26 100644 --- a/server/src/interfaces/Bill.ts +++ b/server/src/interfaces/Bill.ts @@ -62,6 +62,8 @@ export interface IBill { export interface IBillsFilter extends IDynamicListFilterDTO { stringifiedFilterRoles?: string; + page: number; + pageSize: number; } export interface IBillsService { diff --git a/server/src/interfaces/DynamicFilter.ts b/server/src/interfaces/DynamicFilter.ts index b72971289..74115e92e 100644 --- a/server/src/interfaces/DynamicFilter.ts +++ b/server/src/interfaces/DynamicFilter.ts @@ -1,6 +1,9 @@ +import { IModel, ISortOrder } from "./Model"; + export interface IDynamicFilter { - setTableName(tableName: string): void; + setModel(model: IModel): void; buildQuery(): void; + getResponseMeta(); } export interface IFilterRole { @@ -10,19 +13,19 @@ export interface IFilterRole { index?: number; comparator?: string; } - -export interface IDynamicListFilterDTO { +export interface IDynamicListFilter { customViewId?: number; filterRoles?: IFilterRole[]; - columnSortBy: string; + columnSortBy: ISortOrder; sortOrder: string; + stringifiedFilterRoles: string; } export interface IDynamicListService { dynamicList( tenantId: number, model: any, - filter: IDynamicListFilterDTO + filter: IDynamicListFilter ): Promise; handlerErrorsToResponse(error, req, res, next): void; } diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index a314c07b1..f9b9800d5 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -72,6 +72,7 @@ export interface IItemsFilter extends IDynamicListFilterDTO { stringifiedFilterRoles?: string, page: number, pageSize: number, + inactiveMode: boolean, }; export interface IItemsAutoCompleteFilter { diff --git a/server/src/interfaces/JournalReport.ts b/server/src/interfaces/JournalReport.ts index d7ff7a738..9786e1634 100644 --- a/server/src/interfaces/JournalReport.ts +++ b/server/src/interfaces/JournalReport.ts @@ -7,7 +7,9 @@ export interface IJournalReportQuery { noCents: boolean, divideOn1000: boolean, }, - transactionTypes: string | string[], + transactionType: string, + transactionId: string, + accountsIds: number | number[], fromRange: number, toRange: number, diff --git a/server/src/interfaces/Model.ts b/server/src/interfaces/Model.ts index 91f20c15f..1589a07b6 100644 --- a/server/src/interfaces/Model.ts +++ b/server/src/interfaces/Model.ts @@ -1,17 +1,72 @@ - - export interface IModel { - name: string, - tableName: string, - fields: { [key: string]: any, }, -}; + name: string; + tableName: string; + fields: { [key: string]: any }; +} export interface IFilterMeta { - sortOrder: string, - sortBy: string, -}; + sortOrder: string; + sortBy: string; +} export interface IPaginationMeta { - pageSize: number, - page: number, -}; \ No newline at end of file + pageSize: number; + page: number; +} + +export interface IModelMetaDefaultSort { + sortOrder: ISortOrder; + sortField: string; +} + +export type IModelColumnType = + | 'text' + | 'number' + | 'enumeration' + | 'boolean' + | 'relation'; + +export type ISortOrder = 'DESC' | 'ASC'; + +export interface IModelMetaFieldCommon { + name: string; + column: string; + columnable?: boolean; + fieldType: IModelColumnType; + customQuery?: Function; +} + +export interface IModelMetaFieldNumber { + fieldType: 'number'; + minLength?: number; + maxLength?: number; +} + +export interface IModelMetaFieldOther { + fieldType: 'text' | 'boolean'; +} + +export type IModelMetaField = IModelMetaFieldCommon & + (IModelMetaFieldOther | IModelMetaEnumerationField | IModelMetaRelationField); + +export interface IModelMetaEnumerationOption { + key: string; + label: string; +} + +export interface IModelMetaEnumerationField { + fieldType: 'enumeration'; + options: IModelMetaEnumerationOption[]; +} + +export interface IModelMetaRelationField { + fieldType: 'relation'; + relationToModel: IModel; + relationToField: string; +} + +export interface IModelMeta { + defaultFilterField: string; + defaultSort: IModelMetaDefaultSort; + fields: { [key: string]: IModelMetaField }; +} diff --git a/server/src/lib/DynamicFilter/DynamicFilter.ts b/server/src/lib/DynamicFilter/DynamicFilter.ts index 8e42e325e..669f7952a 100644 --- a/server/src/lib/DynamicFilter/DynamicFilter.ts +++ b/server/src/lib/DynamicFilter/DynamicFilter.ts @@ -1,52 +1,73 @@ import { forEach, uniqBy } from 'lodash'; -import { buildFilterRolesJoins } from 'lib/ViewRolesBuilder'; -import { IModel } from 'interfaces'; +import DynamicFilterAbstructor from './DynamicFilterAbstructor'; +import { IDynamicFilter, IFilterRole, IModel } from 'interfaces'; -export default class DynamicFilter { - model: IModel; - tableName: string; +export default class DynamicFilter extends DynamicFilterAbstructor{ + private model: IModel; + private tableName: string; + private dynamicFilters: IDynamicFilter[]; /** * Constructor. * @param {String} tableName - */ constructor(model) { + super(); + this.model = model; this.tableName = model.tableName; - this.filters = []; + this.dynamicFilters = []; } /** - * Set filter. - * @param {*} filterRole - Filter role. + * Registers the given dynamic filter. + * @param {IDynamicFilter} filterRole - Filter role. */ - setFilter(filterRole) { - filterRole.setModel(this.model); - this.filters.push(filterRole); + public setFilter = (dynamicFilter: IDynamicFilter) => { + dynamicFilter.setModel(this.model); + this.dynamicFilters.push(dynamicFilter); + } + + /** + * Retrieve dynamic filter build queries. + * @returns + */ + private dynamicFiltersBuildQuery = () => { + return this.dynamicFilters.map((filter) => { + return filter.buildQuery() + }); + } + + /** + * Retrieve dynamic filter roles. + * @returns {IFilterRole[]} + */ + private dynamicFilterTableColumns = (): IFilterRole[] => { + const localFilterRoles = []; + + this.dynamicFilters.forEach((dynamicFilter) => { + const { filterRoles } = dynamicFilter; + + localFilterRoles.push( + ...(Array.isArray(filterRoles) ? filterRoles : [filterRoles]) + ); + }); + return localFilterRoles; } /** * Builds queries of filter roles. */ - buildQuery() { - const buildersCallbacks = []; - const tableColumns = []; - - this.filters.forEach((filter) => { - const { filterRoles } = filter; - - buildersCallbacks.push(filter.buildQuery()); - tableColumns.push( - ...(Array.isArray(filterRoles) ? filterRoles : [filterRoles]) - ); - }); + public buildQuery = () => { + const buildersCallbacks = this.dynamicFiltersBuildQuery(); + const tableColumns = this.dynamicFilterTableColumns(); return (builder) => { buildersCallbacks.forEach((builderCallback) => { builderCallback(builder); }); - buildFilterRolesJoins( + this.buildFilterRolesJoins( this.model, uniqBy(tableColumns, 'columnKey') )(builder); @@ -56,10 +77,10 @@ export default class DynamicFilter { /** * Retrieve response metadata from all filters adapters. */ - getResponseMeta() { + public getResponseMeta = () => { const responseMeta = {}; - this.filters.forEach((filter) => { + this.dynamicFilters.forEach((filter) => { const { responseMeta: filterMeta } = filter; forEach(filterMeta, (value, key) => { diff --git a/server/src/lib/DynamicFilter/DynamicFilterAbstructor.ts b/server/src/lib/DynamicFilter/DynamicFilterAbstructor.ts new file mode 100644 index 000000000..44c50560f --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterAbstructor.ts @@ -0,0 +1,37 @@ +import { IModel, IFilterRole } from 'interfaces'; + +export default class DynamicFilterAbstructor { + /** + * Extract relation table name from relation. + * @param {String} column - + * @return {String} - join relation table. + */ + protected getTableFromRelationColumn = (column: string) => { + const splitedColumn = column.split('.'); + return splitedColumn.length > 0 ? splitedColumn[0] : ''; + }; + + /** + * Builds view roles join queries. + * @param {String} tableName - Table name. + * @param {Array} roles - Roles. + */ + protected buildFilterRolesJoins = (model: IModel, roles: IFilterRole[]) => { + return (builder) => { + roles.forEach((role) => { + const field = model.getField(role.fieldKey); + + if (field.relation) { + const joinTable = this.getTableFromRelationColumn(field.relation); + + builder.join( + joinTable, + `${model.tableName}.${field.column}`, + '=', + field.relation + ); + } + }); + }; + }; +} diff --git a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts index 34374cb0f..a37cdb0fe 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts @@ -1,10 +1,8 @@ -import { difference } from 'lodash'; -import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; -import { buildFilterQuery } from 'lib/ViewRolesBuilder'; +import DynamicFilterRoleAbstructor from './DynamicFilterRoleAbstructor'; import { IFilterRole } from 'interfaces'; export default class FilterRoles extends DynamicFilterRoleAbstructor { - filterRoles: IFilterRole[]; + private filterRoles: IFilterRole[]; /** * Constructor method. @@ -13,7 +11,7 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { */ constructor(filterRoles: IFilterRole[]) { super(); - + this.filterRoles = filterRoles; this.setResponseMeta(); } @@ -24,7 +22,7 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { */ private buildLogicExpression(): string { let expression = ''; - + this.filterRoles.forEach((role, index) => { expression += index === 0 ? `${role.index} ` : `${role.condition} ${role.index} `; @@ -35,17 +33,22 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { /** * Builds database query of view roles. */ - buildQuery() { + protected buildQuery() { + const logicExpression = this.buildLogicExpression(); + return (builder) => { - const logicExpression = this.buildLogicExpression(); - buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder); + this.buildFilterQuery( + this.model, + this.filterRoles, + logicExpression + )(builder); }; } /** * Sets response meta. */ - setResponseMeta() { + private setResponseMeta() { this.responseMeta = { filterRoles: this.filterRoles, }; diff --git a/server/src/lib/DynamicFilter/DynamicFilterQueryParser.ts b/server/src/lib/DynamicFilter/DynamicFilterQueryParser.ts new file mode 100644 index 000000000..931ec90a0 --- /dev/null +++ b/server/src/lib/DynamicFilter/DynamicFilterQueryParser.ts @@ -0,0 +1,61 @@ +import { OPERATION } from 'lib/LogicEvaluation/Parser'; + +export default class QueryParser { + + constructor(tree, queries) { + this.tree = tree; + this.queries = queries; + this.query = null; + } + + setQuery(query) { + this.query = query.clone(); + } + + parse() { + return this.parseNode(this.tree); + } + + parseNode(node) { + if (typeof node === 'string') { + const nodeQuery = this.getQuery(node); + return (query) => { nodeQuery(query); }; + } + if (OPERATION[node.operation] === undefined) { + throw new Error(`unknow expression ${node.operation}`); + } + const leftQuery = this.getQuery(node.left); + const rightQuery = this.getQuery(node.right); + + switch (node.operation) { + case '&&': + case 'AND': + default: + return (nodeQuery) => nodeQuery.where((query) => { + query.where((q) => { leftQuery(q); }); + query.andWhere((q) => { rightQuery(q); }); + }); + case '||': + case 'OR': + return (nodeQuery) => nodeQuery.where((query) => { + query.where((q) => { leftQuery(q); }); + query.orWhere((q) => { rightQuery(q); }); + }); + } + } + + getQuery(node) { + if (typeof node !== 'string' && node !== null) { + return this.parseNode(node); + } + const value = parseFloat(node); + + if (!isNaN(value)) { + if (typeof this.queries[node] === 'undefined') { + throw new Error(`unknow query under index ${node}`); + } + return this.queries[node]; + } + return null; + } +} \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts index 006008ddd..25dc30c8e 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts @@ -1,13 +1,300 @@ -import { IFilterRole, IDynamicFilter, IModel } from "interfaces"; +import moment from 'moment'; +import { IFilterRole, IDynamicFilter, IModel } from 'interfaces'; +import { Lexer } from 'lib/LogicEvaluation/Lexer'; +import Parser from 'lib/LogicEvaluation/Parser'; +import DynamicFilterQueryParser from './DynamicFilterQueryParser'; +import { COMPARATOR_TYPE, FIELD_TYPE } from './constants'; -export default class DynamicFilterAbstructor implements IDynamicFilter { - filterRoles: IFilterRole[] = []; - tableName: string; - model: IModel; - responseMeta: { [key: string]: any } = {}; +export default abstract class DynamicFilterAbstructor + implements IDynamicFilter +{ + protected filterRoles: IFilterRole[] = []; + protected tableName: string; + protected model: IModel; + protected responseMeta: { [key: string]: any } = {}; - setModel(model: IModel) { + /** + * Sets model the dynamic filter service. + * @param {IModel} model + */ + public setModel(model: IModel) { this.model = model; this.tableName = model.tableName; } -} \ No newline at end of file + + /** + * Transformes filter roles to map by index. + * @param {IModel} model + * @param {IFilterRole[]} roles + * @returns + */ + protected convertRolesMapByIndex = (model, roles) => { + const rolesIndexSet = {}; + + roles.forEach((role) => { + rolesIndexSet[role.index] = this.buildRoleQuery(model, role); + }); + return rolesIndexSet; + }; + + /** + * Builds database query from stored view roles. + * @param {Array} roles - + * @return {Function} + */ + protected buildFilterRolesQuery = ( + model: IModel, + roles: IFilterRole[], + logicExpression: string = '' + ) => { + const rolesIndexSet = this.convertRolesMapByIndex(model, roles); + + // Lexer for logic expression. + const lexer = new Lexer(logicExpression); + const tokens = lexer.getTokens(); + + // Parse the logic expression. + const parser = new Parser(tokens); + const parsedTree = parser.parse(); + + const queryParser = new DynamicFilterQueryParser(parsedTree, rolesIndexSet); + + return queryParser.parse(); + }; + + /** + * Builds filter query for query builder. + * @param {String} tableName - Table name. + * @param {Array} roles - Filter roles. + * @param {String} logicExpression - Logic expression. + */ + protected buildFilterQuery = ( + model: IModel, + roles: IFilterRole[], + logicExpression: string + ) => { + return (builder) => { + this.buildFilterRolesQuery(model, roles, logicExpression)(builder); + }; + }; + + /** + * Builds roles queries. + * @param {IModel} model - + * @param {Object} role - + */ + protected buildRoleQuery = (model: IModel, role: IFilterRole) => { + const fieldRelation = model.getField(role.fieldKey); + const comparatorColumn = `${model.tableName}.${fieldRelation.column}`; + + // Field relation custom query. + if (typeof fieldRelation.customQuery !== 'undefined') { + return (builder) => { + fieldRelation.customQuery(builder, role); + }; + } + switch (fieldRelation.fieldType) { + case FIELD_TYPE.BOOLEAN: + case FIELD_TYPE.ENUMERATION: + return this.booleanRoleQueryBuilder(role, comparatorColumn); + case FIELD_TYPE.NUMBER: + return this.numberRoleQueryBuilder(role, comparatorColumn); + case FIELD_TYPE.DATE: + return this.dateQueryBuilder(role, comparatorColumn); + case FIELD_TYPE.TEXT: + default: + return this.textRoleQueryBuilder(role, comparatorColumn); + } + }; + + /** + * Boolean column query builder. + * @param {IFilterRole} role + * @param {string} comparatorColumn + * @returns + */ + protected booleanRoleQueryBuilder = ( + role: IFilterRole, + comparatorColumn: string + ) => { + switch (role.comparator) { + case COMPARATOR_TYPE.EQUALS: + case COMPARATOR_TYPE.EQUAL: + case COMPARATOR_TYPE.IS: + default: + return (builder) => { + builder.where(comparatorColumn, '=', role.value); + }; + case COMPARATOR_TYPE.NOT_EQUAL: + case COMPARATOR_TYPE.NOT_EQUALS: + case COMPARATOR_TYPE.IS_NOT: + return (builder) => { + builder.where(comparatorColumn, '<>', role.value); + }; + } + }; + + /** + * Numeric column query builder. + * @param {IFilterRole} role + * @param {string} comparatorColumn + * @returns + */ + protected numberRoleQueryBuilder = ( + role: IFilterRole, + comparatorColumn: string + ) => { + switch (role.comparator) { + case COMPARATOR_TYPE.EQUALS: + case COMPARATOR_TYPE.EQUAL: + default: + return (builder) => { + builder.where(comparatorColumn, '=', role.value); + }; + case COMPARATOR_TYPE.NOT_EQUAL: + case COMPARATOR_TYPE.NOT_EQUALS: + return (builder) => { + builder.whereNot(comparatorColumn, role.value); + }; + case COMPARATOR_TYPE.BIGGER_THAN: + case COMPARATOR_TYPE.BIGGER: + return (builder) => { + builder.where(comparatorColumn, '>', role.value); + }; + case COMPARATOR_TYPE.BIGGER_OR_EQUALS: + return (builder) => { + builder.where(comparatorColumn, '>=', role.value); + }; + case COMPARATOR_TYPE.SMALLER_THAN: + case COMPARATOR_TYPE.SMALLER: + return (builder) => { + builder.where(comparatorColumn, '<', role.value); + }; + case COMPARATOR_TYPE.SMALLER_OR_EQUALS: + return (builder) => { + builder.where(comparatorColumn, '<=', role.value); + }; + } + }; + + /** + * Text column query builder. + * @param {IFilterRole} role + * @param {string} comparatorColumn + * @returns {Function} + */ + protected textRoleQueryBuilder = ( + role: IFilterRole, + comparatorColumn: string + ) => { + switch (role.comparator) { + case COMPARATOR_TYPE.EQUAL: + case COMPARATOR_TYPE.EQUALS: + case COMPARATOR_TYPE.IS: + default: + return (builder) => { + builder.where(comparatorColumn, role.value); + }; + case COMPARATOR_TYPE.NOT_EQUALS: + case COMPARATOR_TYPE.NOT_EQUAL: + case COMPARATOR_TYPE.IS_NOT: + return (builder) => { + builder.whereNot(comparatorColumn, role.value); + }; + case COMPARATOR_TYPE.CONTAIN: + case COMPARATOR_TYPE.CONTAINS: + return (builder) => { + builder.where(comparatorColumn, 'LIKE', `%${role.value}%`); + }; + case COMPARATOR_TYPE.NOT_CONTAIN: + case COMPARATOR_TYPE.NOT_CONTAINS: + return (builder) => { + builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`); + }; + } + }; + + /** + * Date column query builder. + * @param {IFilterRole} role + * @param {string} comparatorColumn + * @returns {Function} + */ + protected dateQueryBuilder = ( + role: IFilterRole, + comparatorColumn: string + ) => { + switch (role.comparator) { + case COMPARATOR_TYPE.AFTER: + case COMPARATOR_TYPE.BEFORE: + return (builder) => { + this.dateQueryAfterBeforeComparator(role, comparatorColumn, builder); + }; + case COMPARATOR_TYPE.IN: + return (builder) => { + this.dateQueryInComparator(role, comparatorColumn, builder); + }; + } + }; + + /** + * Date query 'IN' comparator type. + * @param {IFilterRole} role + * @param {string} comparatorColumn + * @param builder + */ + protected dateQueryInComparator = ( + role: IFilterRole, + comparatorColumn: string, + builder + ) => { + const hasTimeFormat = moment( + role.value, + 'YYYY-MM-DD HH:MM', + true + ).isValid(); + const dateFormat = 'YYYY-MM-DD HH:MM:SS'; + + if (hasTimeFormat) { + const targetDateTime = moment(role.value).format(dateFormat); + builder.where(comparatorColumn, '=', targetDateTime); + } else { + const startDate = moment(role.value).startOf('day'); + const endDate = moment(role.value).endOf('day'); + + builder.where(comparatorColumn, '>=', startDate.format(dateFormat)); + builder.where(comparatorColumn, '<=', endDate.format(dateFormat)); + } + } + + /** + * Date query after/before comparator type. + * @param {IFilterRole} role + * @param {string} comparatorColumn - Column. + * @param builder + */ + protected dateQueryAfterBeforeComparator = ( + role: IFilterRole, + comparatorColumn: string, + builder + ) => { + const comparator = role.comparator === COMPARATOR_TYPE.BEFORE ? '<' : '>'; + const hasTimeFormat = moment( + role.value, + 'YYYY-MM-DD HH:MM', + true + ).isValid(); + const targetDate = moment(role.value); + const dateFormat = 'YYYY-MM-DD HH:MM:SS'; + + if (!hasTimeFormat) { + if (role.comparator === COMPARATOR_TYPE.BEFORE) { + targetDate.startOf('day'); + } else { + targetDate.endOf('day'); + } + } + const comparatorValue = targetDate.format(dateFormat); + builder.where(comparatorColumn, comparator, comparatorValue); + } +} diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts index d019be118..2ed89faec 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts @@ -5,8 +5,13 @@ import { getTableFromRelationColumn, } from 'lib/ViewRolesBuilder'; +interface ISortRole { + fieldKey: string; + order: string; +} + export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { - sortRole: { fieldKey: string; order: string } = {}; + private sortRole: ISortRole = {}; /** * Constructor method. @@ -23,39 +28,28 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { this.setResponseMeta(); } - /** - * Validate the given field key with the model. - */ - validate() { - validateFieldKeyExistance(this.model, this.sortRole.fieldKey); - } /** * Builds database query of sort by column on the given direction. */ - buildQuery() { - const fieldRelation = getRoleFieldColumn( - this.model, - this.sortRole.fieldKey - ); - const comparatorColumn = - fieldRelation.relationColumn || - `${this.tableName}.${fieldRelation.column}`; + public buildQuery() { + const field = this.model.getField(this.sortRole.fieldKey); + const comparatorColumn = `${this.tableName}.${field.column}`; - if (typeof fieldRelation.sortQuery !== 'undefined') { + if (typeof field.customSortQuery !== 'undefined') { return (builder) => { - fieldRelation.sortQuery(builder, this.sortRole); + field.customSortQuery(builder, this.sortRole); }; } + return (builder) => { if (this.sortRole.fieldKey) { builder.orderBy(`${comparatorColumn}`, this.sortRole.order); } - this.joinBuildQuery()(builder); }; } - joinBuildQuery() { + private joinBuildQuery() { const fieldColumn = getRoleFieldColumn(this.model, this.sortRole.fieldKey); return (builder) => { @@ -75,7 +69,7 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { /** * Sets response meta. */ - setResponseMeta() { + public setResponseMeta() { this.responseMeta = { sortOrder: this.sortRole.fieldKey, sortBy: this.sortRole.order, diff --git a/server/src/lib/DynamicFilter/constants.ts b/server/src/lib/DynamicFilter/constants.ts new file mode 100644 index 000000000..52ec3986e --- /dev/null +++ b/server/src/lib/DynamicFilter/constants.ts @@ -0,0 +1,36 @@ +export const COMPARATOR_TYPE = { + EQUAL: 'equal', + EQUALS: 'equals', + + NOT_EQUAL: 'not_equal', + NOT_EQUALS: 'not_equals', + + BIGGER_THAN: 'bigger_than', + BIGGER: 'bigger', + BIGGER_OR_EQUALS: 'bigger_or_equals', + + SMALLER_THAN: 'smaller_than', + SMALLER: 'smaller', + SMALLER_OR_EQUALS: 'smaller_or_equals', + + IS: 'is', + IS_NOT: 'is_not', + + CONTAINS: 'contains', + CONTAIN: 'contain', + NOT_CONTAINS: 'contains', + NOT_CONTAIN: 'contain', + + AFTER: 'after', + BEFORE: 'before', + IN: 'in', +}; + +export const FIELD_TYPE = { + TEXT: 'text', + NUMBER: 'number', + ENUMERATION: 'enumeration', + BOOLEAN: 'boolean', + RELATION: 'relation', + DATE: 'date', +}; diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts index 128d4729b..0daa68630 100644 --- a/server/src/lib/ViewRolesBuilder/index.ts +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -1,121 +1,7 @@ import { difference } from 'lodash'; -import moment from 'moment'; -import { Lexer } from 'lib/LogicEvaluation/Lexer'; -import Parser from 'lib/LogicEvaluation/Parser'; -import QueryParser from 'lib/LogicEvaluation/QueryParser'; + import { IFilterRole, IModel } from 'interfaces'; -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: IFilterRole, comparatorColumn: string) => { - switch (role.comparator) { - case 'equals': - case 'is': - default: - return (builder) => { - builder.where(comparatorColumn, role.value); - }; - case 'not_equal': - case 'not_equals': - case 'is_not': - return (builder) => { - builder.whereNot(comparatorColumn, role.value); - }; - case 'contain': - case 'contains': - return (builder) => { - builder.where(comparatorColumn, 'LIKE', `%${role.value}%`); - }; - case 'not_contain': - case 'not_contains': - return (builder) => { - builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`); - }; - } -}; - -const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { - switch (role.comparator) { - case 'after': - case 'before': - return (builder) => { - const comparator = role.comparator === 'before' ? '<' : '>'; - const hasTimeFormat = moment( - role.value, - 'YYYY-MM-DD HH:MM', - true - ).isValid(); - const targetDate = moment(role.value); - const dateFormat = 'YYYY-MM-DD HH:MM:SS'; - - if (!hasTimeFormat) { - if (role.comparator === 'before') { - targetDate.startOf('day'); - } else { - targetDate.endOf('day'); - } - } - const comparatorValue = targetDate.format(dateFormat); - builder.where(comparatorColumn, comparator, comparatorValue); - }; - case 'in': - return (builder) => { - const hasTimeFormat = moment( - role.value, - 'YYYY-MM-DD HH:MM', - true - ).isValid(); - const dateFormat = 'YYYY-MM-DD HH:MM:SS'; - - if (hasTimeFormat) { - const targetDateTime = moment(role.value).format(dateFormat); - builder.where(comparatorColumn, '=', targetDateTime); - } else { - const startDate = moment(role.value).startOf('day'); - const endDate = moment(role.value).endOf('day'); - - builder.where(comparatorColumn, '>=', startDate.format(dateFormat)); - builder.where(comparatorColumn, '<=', endDate.format(dateFormat)); - } - }; - } -}; - /** * Get field column metadata and its relation with other tables. * @param {String} tableName - Table name of target column. @@ -126,68 +12,6 @@ export function getRoleFieldColumn(model: IModel, fieldKey: string) { return tableFields[fieldKey] ? tableFields[fieldKey] : null; } -/** - * Builds roles queries. - * @param {IModel} model - - * @param {Object} role - - */ -export function buildRoleQuery(model: IModel, role: IFilterRole) { - const fieldRelation = getRoleFieldColumn(model, role.fieldKey); - const comparatorColumn = - fieldRelation.relationColumn || - `${model.tableName}.${fieldRelation.column}`; - - if (typeof fieldRelation.query !== 'undefined') { - return (builder) => { - fieldRelation.query(builder, role); - }; - } - switch (fieldRelation.columnType) { - case 'number': - return numberRoleQueryBuilder(role, comparatorColumn); - case 'date': - return dateQueryBuilder(role, comparatorColumn); - case 'text': - case 'varchar': - default: - return textRoleQueryBuilder(role, comparatorColumn); - } -} - -/** - * Extract relation table name from relation. - * @param {String} column - - * @return {String} - join relation table. - */ -export const getTableFromRelationColumn = (column: string) => { - const splitedColumn = column.split('.'); - return splitedColumn.length > 0 ? splitedColumn[0] : ''; -}; - -/** - * Builds view roles join queries. - * @param {String} tableName - Table name. - * @param {Array} roles - Roles. - */ -export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) { - return (builder) => { - roles.forEach((role) => { - const fieldColumn = getRoleFieldColumn(model, role.fieldKey); - - if (fieldColumn.relation) { - const joinTable = getTableFromRelationColumn(fieldColumn.relation); - - builder.join( - joinTable, - `${model.tableName}.${fieldColumn.column}`, - '=', - fieldColumn.relation - ); - } - }); - }; -} - export function buildSortColumnJoin(model: IModel, sortColumnKey: string) { return (builder) => { const fieldColumn = getRoleFieldColumn(model, sortColumnKey); @@ -204,50 +28,6 @@ export function buildSortColumnJoin(model: IModel, sortColumnKey: string) { }; } -/** - * Builds database query from stored view roles. - * - * @param {Array} roles - - * @return {Function} - */ -export function buildFilterRolesQuery( - model: IModel, - roles: IFilterRole[], - logicExpression: string = '' -) { - const rolesIndexSet = {}; - - roles.forEach((role) => { - rolesIndexSet[role.index] = buildRoleQuery(model, role); - }); - // Lexer for logic expression. - const lexer = new Lexer(logicExpression); - const tokens = lexer.getTokens(); - - // Parse the logic expression. - const parser = new Parser(tokens); - const parsedTree = parser.parse(); - - const queryParser = new QueryParser(parsedTree, rolesIndexSet); - return queryParser.parse(); -} - -/** - * Builds filter query for query builder. - * @param {String} tableName - - * @param {Array} roles - - * @param {String} logicExpression - - */ -export const buildFilterQuery = ( - model: IModel, - roles: IFilterRole[], - logicExpression: string -) => { - return (builder) => { - buildFilterRolesQuery(model, roles, logicExpression)(builder); - }; -}; - /** * Mapes the view roles to view conditionals. * @param {Array} viewRoles - @@ -316,14 +96,6 @@ export function validateFieldKeyExistance(model: any, fieldKey: string) { return model?.fields?.[fieldKey] || false; } -export function validateFilterRolesFieldsExistance( - model, - filterRoles: IFilterRole[] -) { - return filterRoles.filter((filterRole: IFilterRole) => { - return !validateFieldKeyExistance(model, filterRole.fieldKey); - }); -} /** * Retrieve model fields keys. diff --git a/server/src/models/Account.Settings.ts b/server/src/models/Account.Settings.ts new file mode 100644 index 000000000..ff1e85435 --- /dev/null +++ b/server/src/models/Account.Settings.ts @@ -0,0 +1,63 @@ +import { IModelMeta } from 'interfaces'; + +export default { + defaultFilterField: 'name', + defaultSort: { + sortOrder: 'DESC', + sortField: 'name', + }, + fields: { + name: { + name: 'Account name', + column: 'name', + columnable: true, + fieldType: 'text', + }, + description: { + name: 'Description', + column: 'description', + columnable: true, + fieldType: 'text', + }, + code: { + name: 'Account code', + column: 'code', + columnable: true, + fieldType: 'text', + }, + root_type: { + name: 'Root type', + column: 'root_type', + columnable: true, + fieldType: 'enumeration', + options: [ + { key: 'asset', label: 'Asset' }, + { key: 'liability', label: 'Liability' }, + { key: 'equity', label: 'Equity' }, + { key: 'Income', label: 'Income' }, + { key: 'expense', label: 'Expense' }, + ], + }, + active: { + name: 'Active', + column: 'active', + fieldType: 'boolean', + }, + amount: { + name: 'Account balance', + column: 'amount', + columnable: true, + fieldType: 'number', + }, + currency: { + name: 'Currency', + column: 'currency_code', + fieldType: 'text', + }, + created_at: { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + }, + }, +} as IModelMeta; diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 9f73c4ae6..e435542cc 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -1,16 +1,15 @@ /* eslint-disable global-require */ -import { Model } from 'objection'; -import { flatten, castArray } from 'lodash'; +import { mixin, Model } from 'objection'; +import { castArray } from 'lodash'; import TenantModel from 'models/TenantModel'; -import { - buildFilterQuery, - buildSortColumnQuery, -} from 'lib/ViewRolesBuilder'; +import { buildFilterQuery, buildSortColumnQuery } from 'lib/ViewRolesBuilder'; import { flatToNestedArray } from 'utils'; import DependencyGraph from 'lib/DependencyGraph'; -import AccountTypesUtils from 'lib/AccountTypes' +import AccountTypesUtils from 'lib/AccountTypes'; +import AccountSettings from './Account.Settings'; +import ModelSettings from './ModelSetting'; -export default class Account extends TenantModel { +export default class Account extends mixin(TenantModel, [ModelSettings]) { /** * Table name. */ @@ -21,7 +20,7 @@ export default class Account extends TenantModel { /** * Timestamps columns. */ - get timestamps() { + static get timestamps() { return ['createdAt', 'updatedAt']; } @@ -35,7 +34,7 @@ export default class Account extends TenantModel { 'accountRootType', 'accountNormal', 'isBalanceSheetAccount', - 'isPLSheet' + 'isPLSheet', ]; } @@ -95,6 +94,13 @@ export default class Account extends TenantModel { const TABLE_NAME = Account.tableName; return { + /** + * Inactive/Active mode. + */ + inactiveMode(query, active = false) { + query.where('active', !active); + }, + filterAccounts(query, accountIds) { if (accountIds.length > 0) { query.whereIn(`${TABLE_NAME}.id`, accountIds); @@ -134,10 +140,10 @@ export default class Account extends TenantModel { }, }; } - + /** * Detarmines whether the given type equals the account type. - * @param {string} accountType + * @param {string} accountType * @return {boolean} */ isAccountType(accountType) { @@ -147,7 +153,7 @@ export default class Account extends TenantModel { /** * Detarmines whether the given root type equals the account type. - * @param {string} rootType + * @param {string} rootType * @return {boolean} */ isRootType(rootType) { @@ -156,11 +162,14 @@ export default class Account extends TenantModel { /** * Detarmine whether the given parent type equals the account type. - * @param {string} parentType + * @param {string} parentType * @return {boolean} */ isParentType(parentType) { - return AccountTypesUtils.isParentTypeEqualsKey(this.accountType, parentType); + return AccountTypesUtils.isParentTypeEqualsKey( + this.accountType, + parentType + ); } /** @@ -188,105 +197,32 @@ export default class Account extends TenantModel { } /** - * Converts flatten accounts list to nested array. - * @param {Array} accounts - * @param {Object} options + * Converts flatten accounts list to nested array. + * @param {Array} accounts + * @param {Object} options */ static toNestedArray(accounts, options = { children: 'children' }) { - return flatToNestedArray(accounts, { id: 'id', parentId: 'parentAccountId' }) + return flatToNestedArray(accounts, { + id: 'id', + parentId: 'parentAccountId', + }); } /** * Transformes the accounts list to depenedency graph structure. - * @param {IAccount[]} accounts - */ + * @param {IAccount[]} accounts + */ static toDependencyGraph(accounts) { - return DependencyGraph.fromArray( - accounts, { itemId: 'id', parentItemId: 'parentAccountId' } - ); + return DependencyGraph.fromArray(accounts, { + itemId: 'id', + parentItemId: 'parentAccountId', + }); } /** - * Model defined fields. + * Model settings. */ - static get fields() { - return { - name: { - label: 'Account name', - column: 'name', - columnType: 'string', - fieldType: 'text', - }, - type: { - label: 'Account type', - column: 'account_type', - }, - description: { - label: 'Description', - column: 'description', - columnType: 'string', - - fieldType: 'text', - }, - code: { - label: 'Account code', - column: 'code', - columnType: 'string', - fieldType: 'text', - }, - root_type: { - label: 'Root type', - options: [ - { key: 'asset', label: 'Asset', }, - { key: 'liability', label: 'Liability' }, - { key: 'equity', label: 'Equity' }, - { key: 'Income', label: 'Income' }, - { key: 'expense', label: 'Expense' }, - ], - query: (query, role) => { - const accountsTypes = AccountTypesUtils.getTypesByRootType(role.value); - const accountsTypesKeys = accountsTypes.map(type => type.key); - - query.whereIn('account_type', accountsTypesKeys); - }, - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - fieldType: 'date', - }, - active: { - label: 'Active', - column: 'active', - columnType: 'boolean', - fieldType: 'checkbox', - }, - balance: { - label: 'Balance', - column: 'amount', - columnType: 'number', - fieldType: 'number', - }, - currency: { - label: 'Currency', - column: 'currency_code', - fieldType: 'options', - optionsResource: 'currency', - optionsKey: 'currency_code', - optionsLabel: 'currency_name', - }, - normal: { - label: 'Account normal', - column: 'account_type_id', - fieldType: 'options', - relation: 'account_types.id', - relationColumn: 'account_types.normal', - options: [ - { key: 'credit', label: 'Credit' }, - { key: 'debit', label: 'Debit' }, - ], - }, - }; + static get meta() { + return AccountSettings; } } diff --git a/server/src/models/Bill.Settings.ts b/server/src/models/Bill.Settings.ts new file mode 100644 index 000000000..6a01455f1 --- /dev/null +++ b/server/src/models/Bill.Settings.ts @@ -0,0 +1,79 @@ +import { IModelMeta } from 'interfaces'; +import Bill from './Bill'; + +export default { + defaultFilterField: 'vendor', + defaultSort: { + sortOrder: 'DESC', + sortField: 'bill_date', + }, + fields: { + // vendor: { + // name: 'Vendor', + // column: 'vendor_id', + // }, + 'bill_number': { + name: 'Bill number', + column: 'bill_number', + columnable: true, + fieldType: 'text', + }, + 'bill_date': { + name: 'Bill date', + column: 'bill_date', + columnable: true, + fieldType: 'date', + }, + 'due_date': { + name: 'Due date', + column: 'due_date', + columnable: true, + fieldType: 'date', + }, + 'reference_no': { + name: 'Reference No.', + column: 'reference_no', + columnable: true, + fieldType: 'text', + }, + 'status': { + name: 'Status', + fieldType: 'enumeration', + columnable: true, + options: [ + { name: 'Paid', key: 'paid' }, + { name: 'Partially paid', key: 'partially-paid' }, + { name: 'Overdue', key: 'overdue' }, + { name: 'Unpaid', key: 'unpaid' }, + { name: 'Opened', key: 'opened' }, + { name: 'Draft', key: 'draft' }, + ], + // filterQuery: Bill.statusFieldFilterQuery, + // sortQuery: Bill.statusFieldSortQuery, + }, + 'amount': { + name: 'Amount', + column: 'amount', + columnable: true, + fieldType: 'number', + }, + 'payment_amount': { + name: 'Payment amount', + column: 'payment_amount', + columnable: true, + fieldType: 'number', + }, + 'note': { + name: 'Note', + column: 'note', + columnable: true, + fieldType: 'text', + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + columnable: true, + fieldType: 'date', + }, + }, +} as IModelMeta; diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index f7e050eb2..51ea5afb8 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -1,10 +1,11 @@ -import { Model, raw } from 'objection'; +import { Model, raw, mixin } from 'objection'; import moment from 'moment'; import { difference } from 'lodash'; import TenantModel from 'models/TenantModel'; -import { query } from 'winston'; +import BillSettings from './Bill.Settings'; +import ModelSetting from './ModelSetting'; -export default class Bill extends TenantModel { +export default class Bill extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ @@ -12,10 +13,9 @@ export default class Bill extends TenantModel { return 'bills'; } - static get resourceable() { - return true; - } - + /** + * Model modifiers. + */ static get modifiers() { return { /** @@ -198,6 +198,13 @@ export default class Bill extends TenantModel { return Math.max(date.diff(dueDate, 'days'), 0); } + /** + * Bill model settings. + */ + static get meta() { + return BillSettings; + } + /** * Relationship mapping. */ @@ -270,87 +277,31 @@ export default class Bill extends TenantModel { [changeMethod]('payment_amount', Math.abs(amount)); } - static get fields() { - return { - vendor: { - label: 'Vendor', - column: 'vendor_id', - relation: 'contacts.id', - relationColumn: 'contacts.display_name', - }, - bill_number: { - label: 'Bill number', - column: 'bill_number', - columnType: 'string', - fieldType: 'text', - }, - bill_date: { - label: 'Bill date', - column: 'bill_date', - columnType: 'date', - fieldType: 'date', - }, - due_date: { - label: 'Due date', - column: 'due_date', - }, - reference_no: { - label: 'Reference No.', - column: 'reference_no', - columnType: 'string', - fieldType: 'text', - }, - status: { - label: 'Status', - options: [], - query: (query, role) => { - switch (role.value) { - case 'draft': - query.modify('draft'); - break; - case 'opened': - query.modify('opened'); - break; - case 'unpaid': - query.modify('unpaid'); - break; - case 'overdue': - query.modify('overdue'); - break; - case 'partially-paid': - query.modify('partiallyPaid'); - break; - case 'paid': - query.modify('paid'); - break; - } - }, - sortQuery(query, role) { - query.modify('sortByStatus', role.order); - }, - }, - amount: { - label: 'Amount', - column: 'amount', - columnType: 'number', - fieldType: 'number', - }, - payment_amount: { - label: 'Payment amount', - column: 'payment_amount', - columnType: 'number', - fieldType: 'number', - }, - note: { - label: 'Note', - column: 'note', - }, - user: {}, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - }; - } + + static statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'draft': + query.modify('draft'); + break; + case 'opened': + query.modify('opened'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + case 'overdue': + query.modify('overdue'); + break; + case 'partially-paid': + query.modify('partiallyPaid'); + break; + case 'paid': + query.modify('paid'); + break; + } + }; + + static statusFieldSortQuery(query, role) { + return query.modify('sortByStatus', role.order); + }; } diff --git a/server/src/models/BillPayment.Settings.ts b/server/src/models/BillPayment.Settings.ts new file mode 100644 index 000000000..f87c2e6b9 --- /dev/null +++ b/server/src/models/BillPayment.Settings.ts @@ -0,0 +1,61 @@ +import { IModelMeta } from 'interfaces'; + +export default { + defaultFilterField: 'vendor', + defaultSort: { + sortOrder: 'DESC', + sortField: 'bill_date', + }, + fields: { + 'vendor': { + name: 'Vendor name', + column: 'vendor_id', + }, + 'amount': { + name: 'Amount', + column: 'amount', + columnable: true, + fieldType: 'number', + }, + 'due_amount': { + name: 'Due amount', + column: 'due_amount', + columnable: true, + fieldType: 'number', + }, + 'payment_account': { + name: 'Payment account', + column: 'payment_account_id', + }, + 'payment_number': { + name: 'Payment number', + column: 'payment_number', + columnable: true, + fieldType: 'number', + }, + 'payment_date': { + name: 'Payment date', + column: 'payment_date', + columnable: true, + fieldType: 'date', + }, + 'reference_no': { + name: 'Reference No.', + column: 'reference', + columnable: true, + fieldType: 'text', + }, + 'description': { + name: 'Description', + column: 'description', + columnable: true, + fieldType: 'text', + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + columnable: true, + fieldType: 'date', + }, + }, +}; diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index 52ddd96d0..323b8219c 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -1,7 +1,9 @@ -import { Model } from "objection"; +import { Model, mixin } from "objection"; import TenantModel from "models/TenantModel"; +import ModelSetting from "./ModelSetting"; +import BillPaymentSettings from "./BillPayment.Settings"; -export default class BillPayment extends TenantModel { +export default class BillPayment extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ @@ -16,8 +18,11 @@ export default class BillPayment extends TenantModel { return ["createdAt", "updatedAt"]; } - static get resourceable() { - return true; + /** + * Model settings. + */ + static get meta() { + return BillPaymentSettings; } /** @@ -73,70 +78,4 @@ export default class BillPayment extends TenantModel { }, }; } - - /** - * Resource fields. - */ - static get fields() { - return { - vendor: { - label: "Vendor name", - column: "vendor_id", - relation: "contacts.id", - relationColumn: "contacts.display_name", - }, - amount: { - label: "Amount", - column: "amount", - columnType: "number", - fieldType: "number", - }, - due_amount: { - label: "Due amount", - column: "due_amount", - columnType: "number", - fieldType: "number", - }, - payment_account: { - label: "Payment account", - column: "payment_account_id", - relation: "accounts.id", - relationColumn: "accounts.name", - - fieldType: "options", - optionsResource: "Account", - optionsKey: "id", - optionsLabel: "name", - }, - payment_number: { - label: "Payment number", - column: "payment_number", - columnType: "string", - fieldType: "text", - }, - payment_date: { - label: "Payment date", - column: "payment_date", - columnType: "date", - fieldType: "date", - }, - reference_no: { - label: "Reference No.", - column: "reference", - columnType: "string", - fieldType: "text", - }, - description: { - label: "Description", - column: "description", - columnType: "string", - fieldType: "text", - }, - created_at: { - label: "Created at", - column: "created_at", - columnType: "date", - }, - }; - } } diff --git a/server/src/models/Customer.Settings.ts b/server/src/models/Customer.Settings.ts new file mode 100644 index 000000000..ff466ae62 --- /dev/null +++ b/server/src/models/Customer.Settings.ts @@ -0,0 +1,98 @@ +export default { + fields: { + display_name: { + name: 'Display name', + column: 'display_name', + fieldType: 'text', + columnable: true, + }, + email: { + name: 'Email', + column: 'email', + fieldType: 'text', + columnable: true, + }, + work_phone: { + name: 'Work phone', + column: 'work_phone', + fieldType: 'text', + columnable: true, + }, + personal_phone: { + name: 'Personal phone', + column: 'personal_phone', + fieldType: 'text', + columnable: true, + }, + company_name: { + name: 'Company name', + column: 'company_name', + fieldType: 'text', + columnable: true, + }, + website: { + name: 'Website', + column: 'website', + fieldType: 'text', + columnable: true, + }, + created_at: { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + columnable: true, + }, + balance: { + name: 'Balance', + column: 'balance', + fieldType: 'number', + columnable: true, + }, + opening_balance: { + name: 'Opening balance', + column: 'opening_balance', + fieldType: 'number', + columnable: true, + }, + opening_balance_at: { + name: 'Opening balance at', + column: 'opening_balance_at', + filterable: false, + fieldType: 'date', + columnable: true, + }, + currency_code: { + column: 'currency_code', + columnable: true, + fieldType: 'text', + }, + status: { + label: 'Status', + options: [ + { key: 'active', label: 'Active' }, + { key: 'inactive', label: 'Inactive' }, + { key: 'overdue', label: 'Overdue' }, + { key: 'unpaid', label: 'Unpaid' }, + ], + columnable: true, + filterQuery: statusFieldFilterQuery, + }, + }, +}; + +function statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'active': + query.modify('active'); + break; + case 'inactive': + query.modify('inactive'); + break; + case 'overdue': + query.modify('overdue'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + } +} diff --git a/server/src/models/Customer.js b/server/src/models/Customer.js index 024917888..4943c2777 100644 --- a/server/src/models/Customer.js +++ b/server/src/models/Customer.js @@ -1,7 +1,9 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; import PaginationQueryBuilder from './Pagination'; import QueryParser from 'lib/LogicEvaluation/QueryParser'; +import ModelSetting from './ModelSetting'; +import CustomerSettings from './Customer.Settings'; class CustomerQueryBuilder extends PaginationQueryBuilder { constructor(...args) { @@ -15,7 +17,7 @@ class CustomerQueryBuilder extends PaginationQueryBuilder { } } -export default class Customer extends TenantModel { +export default class Customer extends mixin(TenantModel, [ModelSetting]) { /** * Query builder. */ @@ -63,6 +65,13 @@ export default class Customer extends TenantModel { */ static get modifiers() { return { + /** + * Inactive/Active mode. + */ + inactiveMode(query, active = false) { + query.where('active', !active); + }, + /** * Filters the active customers. */ @@ -81,10 +90,9 @@ export default class Customer extends TenantModel { overdue(query) { query.select( '*', - Customer - .relatedQuery('overDueInvoices', query.knex()) + Customer.relatedQuery('overDueInvoices', query.knex()) .count() - .as('countOverdue'), + .as('countOverdue') ); query.having('countOverdue', '>', 0); }, @@ -93,7 +101,7 @@ export default class Customer extends TenantModel { */ unpaid(query) { query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0'); - } + }, }; } @@ -122,77 +130,12 @@ export default class Customer extends TenantModel { }, filter: (query) => { query.modify('overdue'); - } - } + }, + }, }; } - static get fields() { - return { - contact_service: { - column: 'contact_service', - }, - display_name: { - column: 'display_name', - }, - email: { - column: 'email', - }, - work_phone: { - column: 'work_phone', - }, - personal_phone: { - column: 'personal_phone', - }, - company_name: { - column: 'company_name', - }, - website: { - column: 'website' - }, - created_at: { - column: 'created_at', - }, - balance: { - column: 'balance', - }, - opening_balance: { - column: 'opening_balance', - }, - opening_balance_at: { - column: 'opening_balance_at', - }, - currency_code: { - column: 'currency_code', - }, - status: { - label: 'Status', - options: [ - { key: 'active', label: 'Active' }, - { key: 'inactive', label: 'Inactive' }, - { key: 'overdue', label: 'Overdue' }, - { key: 'unpaid', label: 'Unpaid' }, - ], - query: (query, role) => { - switch(role.value) { - case 'active': - query.modify('active'); - break; - case 'inactive': - query.modify('inactive'); - break; - case 'overdue': - query.modify('overdue'); - break; - case 'unpaid': - query.modify('unpaid'); - break; - } - }, - }, - created_at: { - column: 'created_at', - } - }; + static get meta() { + return CustomerSettings; } } diff --git a/server/src/models/Expense.Settings.ts b/server/src/models/Expense.Settings.ts new file mode 100644 index 000000000..c97b9ef55 --- /dev/null +++ b/server/src/models/Expense.Settings.ts @@ -0,0 +1,82 @@ +/** + * Expense - Settings. + */ +export default { + defaultFilterField: 'description', + defaultSort: { + sortOrder: 'DESC', + sortField: 'name', + }, + fields: { + 'payment_date': { + name: 'Payment date', + column: 'payment_date', + fieldType: 'date', + }, + 'payment_account': { + name: 'Payment account', + column: 'payment_account_id', + + fieldType: 'relation', + fieldRelation: 'paymentAccount', + + fieldRelationType: 'enumeration', + relationLabelField: 'name', + relationKeyField: 'slug', + }, + 'amount': { + name: 'Amount', + column: 'total_amount', + fieldType: 'number', + }, + // currency_code: { + // name: 'Currency', + // column: 'currency_code', + // }, + 'reference_no': { + name: 'Reference No.', + column: 'reference_no', + fieldType: 'text', + }, + 'description': { + name: 'Description', + column: 'description', + fieldType: 'text', + }, + 'published': { + name: 'Published', + column: 'published_at', + fieldType: 'date', + }, + 'status': { + name: 'Status', + fieldType: 'enumeration', + options: [ + { key: 'draft', name: 'Draft' }, + { key: 'published', name: 'Published' }, + ], + filterQuery: statusFieldFilterQuery, + sortQuery: statusFieldSortQuery, + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + }, + }, +}; + +function statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'draft': + query.modify('filterByDraft'); + break; + case 'published': + query.modify('filterByPublished'); + break; + } +} + +function statusFieldSortQuery(query, role) { + return query.modify('sortByStatus', role.order); +} diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index efadc2bbb..4e96c9636 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -1,8 +1,10 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; import { viewRolesBuilder } from 'lib/ViewRolesBuilder'; +import ModelSetting from './ModelSetting'; +import ExpenseSettings from './Expense.Settings'; -export default class Expense extends TenantModel { +export default class Expense extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ @@ -31,12 +33,6 @@ export default class Expense extends TenantModel { return true; } - /** - * - */ - static get media() { - return true; - } static get virtualAttributes() { return ['isPublished', 'unallocatedCostAmount']; @@ -142,71 +138,7 @@ export default class Expense extends TenantModel { }; } - /** - * Model defined fields. - */ - static get fields() { - return { - payment_date: { - label: 'Payment date', - column: 'payment_date', - columnType: 'date', - }, - payment_account: { - label: 'Payment account', - column: 'payment_account_id', - relation: 'accounts.id', - optionsResource: 'account', - }, - amount: { - label: 'Amount', - column: 'total_amount', - columnType: 'number', - }, - currency_code: { - label: 'Currency', - column: 'currency_code', - optionsResource: 'currency', - }, - reference_no: { - label: 'Reference No.', - column: 'reference_no', - columnType: 'string', - }, - description: { - label: 'Description', - column: 'description', - columnType: 'string', - }, - published: { - label: 'Published', - column: 'published_at', - }, - status: { - label: 'Status', - options: [ - { key: 'draft', label: 'Draft' }, - { key: 'published', label: 'Published' }, - ], - query: (query, role) => { - switch (role.value) { - case 'draft': - query.modify('filterByDraft'); - break; - case 'published': - query.modify('filterByPublished'); - break; - } - }, - sortQuery(query, role) { - query.modify('sortByStatus', role.order); - }, - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - }; + static get meta() { + return ExpenseSettings; } } diff --git a/server/src/models/InventoryAdjustment.Settings.ts b/server/src/models/InventoryAdjustment.Settings.ts new file mode 100644 index 000000000..a005aa6d7 --- /dev/null +++ b/server/src/models/InventoryAdjustment.Settings.ts @@ -0,0 +1,60 @@ +export default { + defaultFilterField: 'date', + defaultSort: { + sortOrder: 'DESC', + sortField: 'date', + }, + fields: { + 'date': { + name: 'Date', + column: 'date', + fieldType: 'date', + columnable: true, + }, + 'type': { + name: 'Adjustment type', + column: 'type', + fieldType: 'enumeration', + options: [ + { key: 'increment', name: 'Increment' }, + { key: 'decrement', name: 'Decrement' }, + ], + columnable: true, + }, + 'adjustment_account': { + name: 'Adjustment account', + column: 'adjustment_account_id', + columnable: true, + }, + 'reason': { + name: 'Reason', + column: 'reason', + fieldType: 'text', + columnable: true, + }, + 'reference_no': { + name: 'Reference No.', + column: 'reference_no', + fieldType: 'text', + columnable: true, + }, + 'description': { + name: 'Description', + column: 'description', + fieldType: 'text', + columnable: true, + }, + 'published_at': { + name: 'Published at', + column: 'published_at', + fieldType: 'date', + columnable: true, + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + columnable: true, + }, + }, +}; diff --git a/server/src/models/InventoryAdjustment.js b/server/src/models/InventoryAdjustment.js index a79bdcdab..b43220687 100644 --- a/server/src/models/InventoryAdjustment.js +++ b/server/src/models/InventoryAdjustment.js @@ -1,5 +1,6 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; +import InventoryAdjustmentSettings from './InventoryAdjustment.Settings'; export default class InventoryAdjustment extends TenantModel { /** @@ -40,8 +41,8 @@ export default class InventoryAdjustment extends TenantModel { static getInventoryDirection(type) { const directions = { - 'increment': 'IN', - 'decrement': 'OUT', + increment: 'IN', + decrement: 'OUT', }; return directions[type] || ''; } @@ -81,52 +82,9 @@ export default class InventoryAdjustment extends TenantModel { } /** - * Model defined fields. + * Model settings. */ - static get fields() { - return { - date: { - label: 'Date', - column: 'date', - columnType: 'date', - }, - type: { - label: 'Adjustment type', - column: 'type', - options: [ - { key: 'increment', label: 'Increment', }, - { key: 'decrement', label: 'Decrement' }, - ], - }, - adjustment_account: { - column: 'adjustment_account_id', - }, - reason: { - label: 'Reason', - column: 'reason', - }, - reference_no: { - label: 'Reference No.', - column: 'reference_no', - }, - description: { - label: 'Description', - column: 'description', - }, - user: { - label: 'User', - column: 'user_id', - }, - published_at: { - label: 'Published at', - column: 'published_at' - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - fieldType: 'date', - }, - }; + static get meta() { + return InventoryAdjustmentSettings; } } diff --git a/server/src/models/Item.Settings.ts b/server/src/models/Item.Settings.ts new file mode 100644 index 000000000..590e775ea --- /dev/null +++ b/server/src/models/Item.Settings.ts @@ -0,0 +1,102 @@ +export default { + defaultFilterField: 'name', + defaultSort: { + sortField: 'name', + sortOrder: 'DESC', + }, + fields: { + 'type': { + name: 'Item type', + column: 'type', + columnable: true, + fieldType: 'enumeration', + options: [ + { key: 'inventory', label: 'Inventory', }, + { key: 'service', label: 'Service' }, + { key: 'non-inventory', label: 'Non Inventory', }, + ], + }, + 'name': { + name: 'Name', + column: 'name', + columnable: true, + fieldType: 'text', + }, + 'code': { + name: 'Code', + column: 'code', + columnable: true, + fieldType: 'text', + }, + 'sellable': { + name: 'Sellable', + column: 'sellable', + fieldType: 'boolean', + }, + 'purchasable': { + name: 'Purchasable', + column: 'purchasable', + fieldType: 'boolean', + }, + 'sell_price': { + name: 'Sell price', + column: 'sell_price', + fieldType: 'number', + }, + 'cost_price': { + name: 'Cost price', + column: 'cost_price', + fieldType: 'number', + }, + 'cost_account': { + name: 'Cost account', + column: 'cost_account_id', + columnable: true, + }, + 'sell_account': { + name: 'Sell account', + column: 'sell_account_id', + }, + 'inventory_account': { + name: 'Inventory account', + column: 'inventory_account_id', + }, + 'sell_description': { + name: 'Sell description', + column: 'sell_description', + fieldType: 'text', + }, + 'purchase_description': { + name: 'Purchase description', + column: 'purchase_description', + fieldType: 'text', + }, + 'quantity_on_hand': { + name: 'Quantity on hand', + column: 'quantity_on_hand', + fieldType: 'number', + }, + 'note': { + name: 'Note', + column: 'note', + fieldType: 'text', + columnable: true, + }, + 'category': { + name: 'Category', + column: 'category_id', + columnable: true, + }, + 'active': { + name: 'Active', + column: 'active', + fieldType: 'boolean', + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + columnType: 'date', + fieldType: 'date', + }, + }, +}; diff --git a/server/src/models/Item.js b/server/src/models/Item.js index 330321e87..e249302f4 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -1,20 +1,22 @@ -import { Model } from "objection"; -import TenantModel from "models/TenantModel"; -import { buildFilterQuery } from "lib/ViewRolesBuilder"; +import { Model, mixin } from 'objection'; +import TenantModel from 'models/TenantModel'; +import { buildFilterQuery } from 'lib/ViewRolesBuilder'; +import ItemSettings from './Item.Settings'; +import ModelSetting from './ModelSetting'; -export default class Item extends TenantModel { +export default class Item extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ static get tableName() { - return "items"; + return 'items'; } /** * Model timestamps. */ get timestamps() { - return ["createdAt", "updatedAt"]; + return ['createdAt', 'updatedAt']; } /** @@ -35,6 +37,13 @@ export default class Item extends TenantModel { viewRolesBuilder(query, conditions, logicExpression) { buildFilterQuery(Item.tableName, conditions, logicExpression)(query); }, + + /** + * Inactive/Active mode. + */ + inactiveMode(query, active = false) { + query.where('active', !active); + }, }; } @@ -42,9 +51,9 @@ export default class Item extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const Media = require("models/Media"); - const Account = require("models/Account"); - const ItemCategory = require("models/ItemCategory"); + const Media = require('models/Media'); + const Account = require('models/Account'); + const ItemCategory = require('models/ItemCategory'); return { /** @@ -54,8 +63,8 @@ export default class Item extends TenantModel { relation: Model.BelongsToOneRelation, modelClass: ItemCategory.default, join: { - from: "items.categoryId", - to: "items_categories.id", + from: 'items.categoryId', + to: 'items_categories.id', }, }, @@ -63,8 +72,8 @@ export default class Item extends TenantModel { relation: Model.BelongsToOneRelation, modelClass: Account.default, join: { - from: "items.costAccountId", - to: "accounts.id", + from: 'items.costAccountId', + to: 'accounts.id', }, }, @@ -72,8 +81,8 @@ export default class Item extends TenantModel { relation: Model.BelongsToOneRelation, modelClass: Account.default, join: { - from: "items.sellAccountId", - to: "accounts.id", + from: 'items.sellAccountId', + to: 'accounts.id', }, }, @@ -81,8 +90,8 @@ export default class Item extends TenantModel { relation: Model.BelongsToOneRelation, modelClass: Account.default, join: { - from: "items.inventoryAccountId", - to: "accounts.id", + from: 'items.inventoryAccountId', + to: 'accounts.id', }, }, @@ -90,110 +99,21 @@ export default class Item extends TenantModel { relation: Model.ManyToManyRelation, modelClass: Media.default, join: { - from: "items.id", + from: 'items.id', through: { - from: "media_links.model_id", - to: "media_links.media_id", + from: 'media_links.model_id', + to: 'media_links.media_id', }, - to: "media.id", + to: 'media.id', }, }, }; } /** - * Item fields. + * Model settings. */ - static get fields() { - return { - type: { - label: "Type", - column: "type", - }, - name: { - label: "Name", - column: "name", - }, - code: { - label: "Code", - column: "code", - }, - sellable: { - label: "Sellable", - column: "sellable", - }, - purchasable: { - label: "Purchasable", - column: "purchasable", - }, - sell_price: { - label: "Sell price", - column: "sell_price", - }, - cost_price: { - label: "Cost price", - column: "cost_price", - }, - currency_code: { - label: "Currency", - column: "currency_code", - }, - cost_account: { - label: "Cost account", - column: "cost_account_id", - relation: "accounts.id", - relationColumn: "accounts.name", - }, - sell_account: { - label: "Sell account", - column: "sell_account_id", - relation: "accounts.id", - relationColumn: "accounts.name", - }, - inventory_account: { - label: "Inventory account", - column: "inventory_account_id", - relation: "accounts.id", - relationColumn: "accounts.name", - }, - sell_description: { - label: "Sell description", - column: "sell_description", - }, - purchase_description: { - label: "Purchase description", - column: "purchase_description", - }, - quantity_on_hand: { - label: "Quantity on hand", - column: "quantity_on_hand", - }, - note: { - label: "Note", - column: "note", - }, - category: { - label: "Category", - column: "category_id", - relation: "items_categories.id", - relationColumn: "items_categories.name", - }, - active: { - label: "Active", - column: "active", - }, - // user: { - // label: 'User', - // column: 'user_id', - // relation: 'users.id', - // relationColumn: 'users.', - // }, - created_at: { - label: "Created at", - column: "created_at", - columnType: "date", - fieldType: "date", - }, - }; + static get meta() { + return ItemSettings; } } diff --git a/server/src/models/ItemCategory.Settings.ts b/server/src/models/ItemCategory.Settings.ts new file mode 100644 index 000000000..a92ac7f16 --- /dev/null +++ b/server/src/models/ItemCategory.Settings.ts @@ -0,0 +1,41 @@ +export default { + defaultFilterField: 'name', + defaultSort: { + sortField: 'name', + sortOrder: 'DESC', + }, + fields: { + name: { + label: 'Name', + column: 'name', + fieldType: 'text', + }, + description: { + label: 'Description', + column: 'description', + fieldType: 'text', + }, + cost_account: { + label: 'Cost account', + column: 'cost_account_id', + }, + sell_account: { + label: 'Sell account', + column: 'sell_account_id', + }, + inventory_account: { + label: 'Inventory account', + column: 'inventory_account_id', + }, + count: { + label: 'Count', + column: 'count', + sortQuery: this.sortCountQuery, + }, + created_at: { + label: 'Created at', + column: 'created_at', + columnType: 'date', + }, + }, +}; diff --git a/server/src/models/ItemCategory.js b/server/src/models/ItemCategory.js index 808bc30b7..ce62877fa 100644 --- a/server/src/models/ItemCategory.js +++ b/server/src/models/ItemCategory.js @@ -42,68 +42,6 @@ export default class ItemCategory extends TenantModel { }; } - /** - * Item category fields. - */ - static get fields() { - return { - name: { - label: 'Name', - column: 'name', - columnType: 'string' - }, - description: { - label: 'Description', - column: 'description', - columnType: 'string' - }, - user: { - label: 'User', - column: 'user_id', - relation: 'users.id', - relationColumn: 'users.id', - }, - cost_account: { - label: 'Cost account', - column: 'cost_account_id', - relation: 'accounts.id', - optionsResource: 'account' - }, - sell_account: { - label: 'Sell account', - column: 'sell_account_id', - relation: 'accounts.id', - optionsResource: 'account' - }, - inventory_account: { - label: 'Inventory account', - column: 'inventory_account_id', - relation: 'accounts.id', - optionsResource: 'account' - }, - cost_method: { - label: 'Cost method', - column: 'cost_method', - options: [{ - key: 'FIFO', label: 'First-in first-out (FIFO)', - key: 'LIFO', label: 'Last-in first-out (LIFO)', - key: 'average', label: 'Average rate', - }], - columnType: 'string', - }, - count: { - label: 'Count', - column: 'count', - sortQuery: this.sortCountQuery - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - }; - } - static sortCountQuery(query, role) { query.orderBy('count', role.order); } diff --git a/server/src/models/ManualJournal.Settings.ts b/server/src/models/ManualJournal.Settings.ts new file mode 100644 index 000000000..1f57a9a0c --- /dev/null +++ b/server/src/models/ManualJournal.Settings.ts @@ -0,0 +1,54 @@ +export default { + defaultFilterField: 'date', + defaultSort: { + sortOrder: 'DESC', + sortField: 'name', + }, + fields: { + 'date': { + label: 'Date', + column: 'date', + fieldType: 'date', + }, + 'journal_number': { + label: 'Journal number', + column: 'journal_number', + fieldType: 'text', + }, + 'reference': { + label: 'Reference No.', + column: 'reference', + fieldType: 'text', + }, + 'journal_type': { + label: 'Journal type', + column: 'journal_type', + fieldType: 'text', + }, + 'amount': { + label: 'Amount', + column: 'amount', + columnType: 'number', + }, + 'description': { + label: 'Description', + column: 'description', + fieldType: 'text', + }, + 'status': { + label: 'Status', + column: 'status', + fieldType: 'enumeration', + sortQuery: statusFieldSortQuery, + }, + 'created_at': { + label: 'Created at', + column: 'created_at', + fieldType: 'date', + }, + }, +}; + +function statusFieldSortQuery(query, role) { + return query.modify('sortByStatus', role.order); +} diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js index 3035101b9..f19878f90 100644 --- a/server/src/models/ManualJournal.js +++ b/server/src/models/ManualJournal.js @@ -1,8 +1,10 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; import { formatNumber } from 'utils'; +import ModelSetting from './ModelSetting'; +import ManualJournalSettings from './ManualJournal.Settings'; -export default class ManualJournal extends TenantModel { +export default class ManualJournal extends mixin(TenantModel, [ModelSetting]) { /** * Table name. */ @@ -99,52 +101,7 @@ export default class ManualJournal extends TenantModel { }; } - /** - * Model defined fields. - */ - static get fields() { - return { - date: { - label: 'Date', - column: 'date', - columnType: 'date', - }, - journal_number: { - label: 'Journal number', - column: 'journal_number', - columnType: 'string', - }, - reference: { - label: 'Reference No.', - column: 'reference', - columnType: 'string', - }, - journal_type: { - label: 'Journal type', - column: 'journal_type', - columnType: 'string', - }, - amount: { - label: 'Amount', - column: 'amount', - columnType: 'number', - }, - description: { - label: 'Description', - column: 'description', - columnType: 'string', - }, - status: { - label: 'Status', - column: 'status', - sortQuery(query, role) { - query.modify('sortByStatus', role.order); - }, - }, - created_at: { - label: 'Created at', - column: 'created_at', - }, - }; + static get meta() { + return ManualJournalSettings; } } diff --git a/server/src/models/ModelSetting.ts b/server/src/models/ModelSetting.ts new file mode 100644 index 000000000..353a636c3 --- /dev/null +++ b/server/src/models/ModelSetting.ts @@ -0,0 +1,54 @@ +import { get } from 'lodash'; +import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from 'interfaces'; + +export default (Model) => + class ModelSettings extends Model { + /** + * + */ + static get meta(): IModelMeta { + throw new Error(''); + } + + /** + * Retrieve specific model field meta of the given field key. + * @param {string} key + * @returns {IModelMetaField} + */ + public static getField(key: string): IModelMetaField { + return get(this.meta.fields, key); + } + + /** + * Retrieve the specific model meta. + * @param {string} key + * @returns + */ + public static getMeta(key: string) { + return get(this.meta, key); + } + + /** + * Retrieve the model meta fields. + * @return {{ [key: string]: IModelMetaField }} + */ + public static get fields(): { [key: string]: IModelMetaField } { + return this.getMeta('fields'); + } + + /** + * Retrieve the model default sort settings. + * @return {IModelMetaDefaultSort} + */ + public static get defaultSort(): IModelMetaDefaultSort { + return this.getMeta('defaultSort'); + } + + /** + * Retrieve the default filter field key. + * @return {string} + */ + public static get defaultFilterField(): string { + return this.getMeta('defaultFilterField'); + } + }; diff --git a/server/src/models/PaymentReceive.Settings.ts b/server/src/models/PaymentReceive.Settings.ts new file mode 100644 index 000000000..4c99cbac8 --- /dev/null +++ b/server/src/models/PaymentReceive.Settings.ts @@ -0,0 +1,43 @@ + +export default { + fields: { + customer: { + name: 'Customer', + column: 'customer_id', + }, + payment_date: { + name: 'Payment date', + column: 'payment_date', + fieldType: 'date', + }, + amount: { + name: 'Amount', + column: 'amount', + fieldType: 'number', + }, + reference_no: { + name: 'Reference No.', + column: 'reference_no', + fieldType: 'text', + }, + deposit_account: { + name: 'Deposit account', + column: 'deposit_account_id', + }, + payment_receive_no: { + name: 'Payment receive No.', + column: 'payment_receive_no', + fieldType: 'text', + }, + statement: { + name: 'Statement', + column: 'statement', + fieldType: 'text', + }, + created_at: { + name: 'Created at', + column: 'created_at', + fieldDate: 'date', + }, + }, +}; diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index 5db76e5d1..8592aae08 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -1,7 +1,9 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; +import ModelSetting from './ModelSetting'; +import PaymentReceiveSettings from './PaymentReceive.Settings'; -export default class PaymentReceive extends TenantModel { +export default class PaymentReceive extends mixin(TenantModel, [ModelSetting]) { /** * Table name. */ @@ -75,63 +77,9 @@ export default class PaymentReceive extends TenantModel { } /** - * Model defined fields. + * */ - static get fields() { - return { - customer: { - label: 'Customer', - column: 'customer_id', - relation: 'contacts.id', - relationColumn: 'contacts.displayName', - - fieldType: 'options', - optionsResource: 'customers', - optionsKey: 'id', - optionsLable: 'displayName', - }, - payment_date: { - label: 'Payment date', - column: 'payment_date', - columnType: 'date', - fieldType: 'date', - }, - amount: { - label: 'Amount', - column: 'amount', - columnType: 'number', - fieldType: 'number', - }, - reference_no: { - label: 'Reference No.', - column: 'reference_no', - columnType: 'string', - fieldType: 'text', - }, - deposit_account: { - column: 'deposit_account_id', - lable: 'Deposit account', - relation: "accounts.id", - relationColumn: 'accounts.name', - optionsResource: "account", - }, - payment_receive_no: { - label: 'Payment receive No.', - column: 'payment_receive_no', - columnType: 'string', - fieldType: 'text', - }, - description: { - label: 'description', - column: 'description', - columnType: 'string', - fieldType: 'text', - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - }; + static get meta() { + return PaymentReceiveSettings; } } diff --git a/server/src/models/SaleEstimate.Settings.ts b/server/src/models/SaleEstimate.Settings.ts new file mode 100644 index 000000000..6feb44fe6 --- /dev/null +++ b/server/src/models/SaleEstimate.Settings.ts @@ -0,0 +1,85 @@ +export default { + defaultFilterField: 'estimate_date', + defaultSort: { + sortOrder: 'DESC', + sortField: 'estimate_date', + }, + fields: { + 'amount': { + name: 'Amount', + column: 'amount', + fieldType: 'number', + }, + 'estimate_number': { + name: 'Estimate number', + column: 'estimate_number', + fieldType: 'text', + }, + 'customer': { + name: 'Customer', + column: 'customer_id', + }, + 'estimate_date': { + name: 'Estimate date', + column: 'estimate_date', + fieldType: 'date', + }, + 'expiration_date': { + name: 'Expiration date', + column: 'expiration_date', + fieldType: 'date', + }, + 'reference_no': { + name: 'Reference No.', + column: 'reference', + fieldType: 'text', + }, + 'note': { + name: 'Note', + column: 'note', + fieldType: 'text', + }, + 'terms_conditions': { + name: 'Terms & conditions', + column: 'terms_conditions', + fieldType: 'text', + }, + 'status': { + name: 'Status', + filterQuery: statusFieldFilterQuery, + sortQuery: statusFieldSortQuery, + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + columnType: 'date', + }, + }, +}; + +function statusFieldSortQuery(query, role) { + return query.modify('orderByDraft', role.order); +} + +function statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'draft': + query.modify('draft'); + break; + case 'delivered': + query.modify('delivered'); + break; + case 'approved': + query.modify('approved'); + break; + case 'rejected': + query.modify('rejected'); + break; + case 'invoiced': + query.modify('invoiced'); + break; + case 'expired': + query.modify('expired'); + break; + } +} diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index fc732da79..728122c0a 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -1,11 +1,11 @@ import moment from 'moment'; -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; import { defaultToTransform } from 'utils'; -import HasItemEntries from 'services/Sales/HasItemsEntries'; -import { query } from 'winston'; +import SaleEstimateSettings from './SaleEstimate.Settings'; +import ModelSetting from './ModelSetting'; -export default class SaleEstimate extends TenantModel { +export default class SaleEstimate extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ @@ -29,9 +29,9 @@ export default class SaleEstimate extends TenantModel { 'isExpired', 'isConvertedToInvoice', 'isApproved', - 'isRejected' + 'isRejected', ]; - } + } /** * Detarmines whether the sale estimate converted to sale invoice. @@ -57,7 +57,7 @@ export default class SaleEstimate extends TenantModel { return defaultToTransform( this.expirationDate, moment().isAfter(this.expirationDate, 'day'), - false, + false ); } @@ -123,14 +123,14 @@ export default class SaleEstimate extends TenantModel { * Filters the approved estimates transactions. */ approved(query) { - query.whereNot('approved_at', null) + query.whereNot('approved_at', null); }, /** * Sorting the estimates orders by delivery status. */ orderByDraft(query, order) { - query.orderByRaw(`delivered_at is null ${order}`) - } + query.orderByRaw(`delivered_at is null ${order}`); + }, }; } @@ -151,7 +151,7 @@ export default class SaleEstimate extends TenantModel { }, filter(query) { query.where('contact_service', 'customer'); - } + }, }, entries: { relation: Model.HasManyRelation, @@ -168,91 +168,9 @@ export default class SaleEstimate extends TenantModel { } /** - * Model defined fields. + * Model settings. */ - static get fields() { - return { - amount: { - label: 'Amount', - column: 'amount', - columnType: 'number', - fieldType: 'number', - }, - estimate_number: { - label: 'Estimate number', - column: 'estimate_number', - columnType: 'text', - fieldType: 'text', - }, - customer: { - label: 'Customer', - column: 'customer_id', - relation: 'contacts.id', - relationColumn: 'contacts.displayName', - - fieldType: 'options', - optionsResource: 'customers', - optionsKey: 'id', - optionsLable: 'displayName', - }, - estimate_date: { - label: 'Estimate date', - column: 'estimate_date', - columnType: 'date', - fieldType: 'date', - }, - expiration_date: { - label: 'Expiration date', - column: 'expiration_date', - columnType: 'date', - fieldType: 'date', - }, - reference_no: { - label: "Reference No.", - column: "reference", - columnType: "number", - fieldType: "number", - }, - note: { - label: 'Note', - column: 'note', - columnType: 'text', - fieldType: 'text', - }, - terms_conditions: { - label: 'Terms & conditions', - column: 'terms_conditions', - columnType: 'text', - fieldType: 'text', - }, - status: { - label: 'Status', - query: (query, role) => { - switch(role.value) { - case 'draft': - query.modify('draft'); break; - case 'delivered': - query.modify('delivered'); break; - case 'approved': - query.modify('approved'); break; - case 'rejected': - query.modify('rejected'); break; - case 'invoiced': - query.modify('invoiced'); - break; - case 'expired': - query.modify('expired'); break; - } - }, - sortQuery: (query, role) => { - query.modify('orderByDraft', role.order); - } - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - }; + static get meta() { + return SaleEstimateSettings; } } diff --git a/server/src/models/SaleInvoice.Settings.ts b/server/src/models/SaleInvoice.Settings.ts new file mode 100644 index 000000000..576752f3e --- /dev/null +++ b/server/src/models/SaleInvoice.Settings.ts @@ -0,0 +1,86 @@ +export default { + defaultFilterField: 'customer', + defaultSort: { + sortOrder: 'DESC', + sortField: 'created_at', + }, + fields: { + // customer: { + // name: 'Customer', + // column: 'customer_id', + // }, + invoice_date: { + name: 'Invoice date', + column: 'invoice_date', + fieldType: 'date', + columnable: true, + }, + due_date: { + name: 'Due date', + column: 'due_date', + fieldType: 'date', + columnable: true, + }, + invoice_no: { + name: 'Invoice No.', + column: 'invoice_no', + fieldType: 'text', + columnable: true, + }, + reference_no: { + name: 'Reference No.', + column: 'reference_no', + fieldType: 'text', + columnable: true, + }, + invoice_message: { + name: 'Invoice message', + column: 'invoice_message', + fieldType: 'text', + columnable: true, + }, + terms_conditions: { + name: 'Terms & conditions', + column: 'terms_conditions', + fieldType: 'text', + columnable: true, + }, + amount: { + name: 'Invoice amount', + column: 'balance', + columnable: true, + fieldType: 'number', + }, + payment_amount: { + name: 'Payment amount', + column: 'payment_amount', + fieldType: 'number', + }, + due_amount: { + name: 'Due amount', + column: 'due_amount', + fieldType: 'number', + // sortQuery: SaleInvoice.dueAmountFieldSortQuery, + }, + created_at: { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + }, + // status: { + // name: 'Status', + // columnable: true, + // fieldType: 'enumeration', + // options: [ + // { key: 'draft', name: 'Draft' }, + // { key: 'delivered', name: 'Delivered' }, + // { key: 'unpaid', name: 'Unpaid' }, + // { key: 'overdue', name: 'Overdue' }, + // { key: 'partially-paid', name: 'Partially paid' }, + // { key: 'paid', name: 'Paid' }, + // ], + // // filterQuery: SaleInvoice.statusFieldFilterQuery, + // // sortQuery: SaleInvoice.statusFieldSortQuery, + // }, + }, +}; diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 628d378ec..a3b07418b 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -1,8 +1,10 @@ -import { Model, raw } from 'objection'; +import { mixin, Model, raw } from 'objection'; import moment from 'moment'; import TenantModel from 'models/TenantModel'; +import ModelSetting from './ModelSetting'; +import SaleInvoiceMeta from './SaleInvoice.Settings'; -export default class SaleInvoice extends TenantModel { +export default class SaleInvoice extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ @@ -243,6 +245,9 @@ export default class SaleInvoice extends TenantModel { const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); return { + /** + * Sale invoice associated entries. + */ entries: { relation: Model.HasManyRelation, modelClass: ItemEntry.default, @@ -255,6 +260,9 @@ export default class SaleInvoice extends TenantModel { }, }, + /** + * Belongs to customer model. + */ customer: { relation: Model.BelongsToOneRelation, modelClass: Contact.default, @@ -267,6 +275,9 @@ export default class SaleInvoice extends TenantModel { }, }, + /** + * Invoice has associated account transactions. + */ transactions: { relation: Model.HasManyRelation, modelClass: AccountTransaction.default, @@ -316,125 +327,40 @@ export default class SaleInvoice extends TenantModel { } /** - * Model defined fields. + * Sale invoice meta. */ - static get fields() { - return { - customer: { - label: 'Customer', - column: 'customer_id', - relation: 'contacts.id', - relationColumn: 'contacts.displayName', + static get meta() { + return SaleInvoiceMeta; + } - fieldType: 'options', - optionsResource: 'customers', - optionsKey: 'id', - optionsLable: 'displayName', - }, - invoice_date: { - label: 'Invoice date', - column: 'invoice_date', - columnType: 'date', - fieldType: 'date', - }, - due_date: { - label: 'Due date', - column: 'due_date', - columnType: 'date', - fieldType: 'date', - }, - invoice_no: { - label: 'Invoice No.', - column: 'invoice_no', - columnType: 'number', - fieldType: 'number', - }, - reference_no: { - label: 'Reference No.', - column: 'reference_no', - columnType: 'number', - fieldType: 'number', - }, - invoice_message: { - label: 'Invoice message', - column: 'invoice_message', - columnType: 'text', - fieldType: 'text', - }, - terms_conditions: { - label: 'Terms & conditions', - column: 'terms_conditions', - columnType: 'text', - fieldType: 'text', - }, - invoice_amount: { - label: 'Invoice amount', - column: 'invoice_amount', - columnType: 'number', - fieldType: 'number', - }, - payment_amount: { - label: 'Payment amount', - column: 'payment_amount', - columnType: 'number', - fieldType: 'number', - }, - balance: { - label: 'Balance', - column: 'balance', - columnType: 'number', - fieldType: 'number', - }, - due_amount: { - label: 'Due amount', - column: 'due_amount', - columnType: 'number', - fieldType: 'number', - sortQuery(query, role) { - query.modify('sortByDueAmount', role.order); - }, - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - status: { - label: 'Status', - options: [ - { key: 'draft', label: 'Draft' }, - { key: 'delivered', label: 'Delivered' }, - { key: 'unpaid', label: 'Unpaid' }, - { key: 'overdue', label: 'Overdue' }, - { key: 'partially-paid', label: 'Partially paid' }, - { key: 'paid', label: 'Paid' }, - ], - query: (query, role) => { - switch (role.value) { - case 'draft': - query.modify('draft'); - break; - case 'delivered': - query.modify('delivered'); - break; - case 'unpaid': - query.modify('unpaid'); - break; - case 'overdue': - query.modify('overdue'); - break; - case 'partially-paid': - query.modify('partiallyPaid'); - break; - case 'paid': - query.modify('paid'); - break; - } - }, - sortQuery(query, role) { - query.modify('sortByStatus', role.order); - }, - }, - }; + static dueAmountFieldSortQuery(query, role) { + query.modify('sortByDueAmount', role.order); + } + + static statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'draft': + query.modify('draft'); + break; + case 'delivered': + query.modify('delivered'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + case 'overdue': + query.modify('overdue'); + break; + case 'partially-paid': + query.modify('partiallyPaid'); + break; + case 'paid': + query.modify('paid'); + break; + } + } + + static statusFieldSortQuery(query, role) { + query.modify('sortByStatus', role.order); } } diff --git a/server/src/models/SaleReceipt.Settings.ts b/server/src/models/SaleReceipt.Settings.ts new file mode 100644 index 000000000..0ddc6153c --- /dev/null +++ b/server/src/models/SaleReceipt.Settings.ts @@ -0,0 +1,87 @@ +export default { + defaultFilterField: 'receipt_date', + defaultSort: { + sortOrder: 'DESC', + sortField: 'created_at', + }, + fields: { + 'amount': { + name: 'Amount', + column: 'amount', + fieldType: 'number', + columnable: true + }, + 'deposit_account': { + column: 'deposit_account_id', + name: 'Deposit account', + columnable: true + }, + 'customer': { + name: 'Customer', + column: 'customer_id', + columnable: true + }, + 'receipt_date': { + name: 'Receipt date', + column: 'receipt_date', + fieldType: 'date', + columnable: true + }, + 'receipt_number': { + name: 'Receipt No.', + column: 'receipt_number', + fieldType: 'text', + columnable: true + }, + 'reference_no': { + name: 'Reference No.', + column: 'reference_no', + fieldType: 'text', + columnable: true + }, + 'receipt_message': { + name: 'Receipt message', + column: 'receipt_message', + fieldType: 'text', + columnable: true + }, + 'statement': { + name: 'Statement', + column: 'statement', + fieldType: 'text', + columnable: true + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + columnable: true + }, + 'status': { + name: 'Status', + fieldType: 'enumeration', + options: [ + { key: 'draft', name: 'Draft' }, + { key: 'closed', name: 'Closed' }, + ], + query: statusFieldFilterQuery, + sortQuery: statusFieldSortQuery, + columnable: true + }, + }, +}; + +function statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'draft': + query.modify('draft'); + break; + case 'closed': + query.modify('closed'); + break; + } +} + +function statusFieldSortQuery(query, role) { + query.modify('sortByStatus', role.order); +} diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js index 3dd993894..ea226ad05 100644 --- a/server/src/models/SaleReceipt.js +++ b/server/src/models/SaleReceipt.js @@ -1,7 +1,9 @@ import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; +import ModelSetting from './ModelSetting'; +import SaleReceiptSettings from './SaleReceipt.Settings'; -export default class SaleReceipt extends TenantModel { +export default class SaleReceipt extends mixin(TenantModel, [ModelSetting]) { /** * Table name */ @@ -16,14 +18,11 @@ export default class SaleReceipt extends TenantModel { return ['created_at', 'updated_at']; } - /** + /** * Virtual attributes. */ static get virtualAttributes() { - return [ - 'isClosed', - 'isDraft', - ]; + return ['isClosed', 'isDraft']; } /** @@ -66,7 +65,7 @@ export default class SaleReceipt extends TenantModel { */ sortByStatus(query, order) { query.orderByRaw(`CLOSED_AT IS NULL ${order}`); - } + }, }; } @@ -89,7 +88,7 @@ export default class SaleReceipt extends TenantModel { }, filter(query) { query.where('contact_service', 'customer'); - } + }, }, depositAccount: { @@ -118,95 +117,19 @@ export default class SaleReceipt extends TenantModel { modelClass: AccountTransaction.default, join: { from: 'sales_receipts.id', - to: 'accounts_transactions.referenceId' + to: 'accounts_transactions.referenceId', }, filter(builder) { builder.where('reference_type', 'SaleReceipt'); }, - } + }, }; } /** - * Model defined fields. + * Sale invoice meta. */ - static get fields() { - return { - amount: { - label: 'Amount', - column: 'amount', - columnType: 'number', - fieldType: 'number', - }, - deposit_account: { - column: 'deposit_account_id', - label: 'Deposit account', - relation: "accounts.id", - optionsResource: "account", - }, - customer: { - label: 'Customer', - column: 'customer_id', - fieldType: 'options', - optionsResource: 'customers', - optionsKey: 'id', - optionsLable: 'displayName', - }, - receipt_date: { - label: 'Receipt date', - column: 'receipt_date', - columnType: 'date', - fieldType: 'date', - }, - receipt_number: { - label: 'Receipt No.', - column: 'receipt_number', - columnType: 'string', - fieldType: 'text', - }, - reference_no: { - label: 'Reference No.', - column: 'reference_no', - columnType: 'text', - fieldType: 'text', - }, - receipt_message: { - label: 'Receipt message', - column: 'receipt_message', - columnType: 'text', - fieldType: 'text', - }, - statement: { - label: 'Statement', - column: 'statement', - columnType: 'text', - fieldType: 'text', - }, - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, - status: { - label: 'Status', - options: [ - { key: 'draft', label: 'Draft', }, - { key: 'closed', label: 'Closed' }, - ], - query: (query, role) => { - switch(role.value) { - case 'draft': - query.modify('draft'); - break; - case 'closed': - query.modify('closed'); - break; - } - }, - sortQuery(query, role) { - query.modify('sortByStatus', role.order); - } - } - }; + static get meta() { + return SaleReceiptSettings; } } diff --git a/server/src/models/Vendor.Settings.ts b/server/src/models/Vendor.Settings.ts new file mode 100644 index 000000000..8496cc0f5 --- /dev/null +++ b/server/src/models/Vendor.Settings.ts @@ -0,0 +1,89 @@ +export default { + defaultFilterField: 'display_name', + defaultSort: { + sortOrder: 'DESC', + sortField: 'created_at', + }, + fields: { + 'display_name': { + name: 'Display name', + column: 'display_name', + fieldType: 'text', + }, + 'email': { + name: 'Email', + column: 'email', + fieldType: 'text', + }, + 'work_phone': { + name: 'Work phone', + column: 'work_phone', + fieldType: 'text', + }, + 'personal_phone': { + name: 'Personal phone', + column: 'personal_phone', + fieldType: 'text', + }, + 'company_name': { + name: 'Company name', + column: 'company_name', + fieldType: 'text', + }, + 'website': { + name: 'Website', + column: 'website', + fieldType: 'text', + }, + 'created_at': { + name: 'Created at', + column: 'created_at', + fieldType: 'date', + }, + 'balance': { + name: 'Balance', + column: 'balance', + fieldType: 'number', + }, + 'opening_balance': { + name: 'Opening balance', + column: 'opening_balance', + fieldType: 'number', + }, + 'opening_balance_at': { + name: 'Opening balance at', + column: 'opening_balance_at', + fieldType: 'date', + }, + 'currency_code': { + name: 'Currency code', + column: 'currency_code', + fieldType: 'text', + }, + 'status': { + label: 'Status', + options: [ + { key: 'active', label: 'Active' }, + { key: 'inactive', label: 'Inactive' }, + { key: 'overdue', label: 'Overdue' }, + { key: 'unpaid', label: 'Unpaid' }, + ], + query: (query, role) => { + switch (role.value) { + case 'active': + query.modify('active'); + break; + case 'inactive': + query.modify('inactive'); + break; + case 'overdue': + query.modify('overdue'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + } + }, + }, + }, +}; diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js index 62eab7b4c..bf4f16bdb 100644 --- a/server/src/models/Vendor.js +++ b/server/src/models/Vendor.js @@ -1,6 +1,8 @@ -import { Model, QueryBuilder } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; import PaginationQueryBuilder from './Pagination'; +import ModelSetting from './ModelSetting'; +import VendorSettings from './Vendor.Settings'; class VendorQueryBuilder extends PaginationQueryBuilder { constructor(...args) { @@ -14,7 +16,7 @@ class VendorQueryBuilder extends PaginationQueryBuilder { } } -export default class Vendor extends TenantModel { +export default class Vendor extends mixin(TenantModel, [ModelSetting]) { /** * Query builder. */ @@ -62,6 +64,13 @@ export default class Vendor extends TenantModel { */ static get modifiers() { return { + /** + * Inactive/Active mode. + */ + inactiveMode(query, active = false) { + query.where('active', !active); + }, + /** * Filters the active customers. */ @@ -125,72 +134,7 @@ export default class Vendor extends TenantModel { }; } - static get fields() { - return { - contact_service: { - column: 'contact_service', - }, - display_name: { - column: 'display_name', - }, - email: { - column: 'email', - }, - work_phone: { - column: 'work_phone', - }, - personal_phone: { - column: 'personal_phone', - }, - company_name: { - column: 'company_name', - }, - website: { - column: 'website' - }, - created_at: { - column: 'created_at', - }, - balance: { - column: 'balance', - }, - opening_balance: { - column: 'opening_balance', - }, - opening_balance_at: { - column: 'opening_balance_at', - }, - currency_code: { - column: 'currency_code', - }, - status: { - label: 'Status', - options: [ - { key: 'active', label: 'Active' }, - { key: 'inactive', label: 'Inactive' }, - { key: 'overdue', label: 'Overdue' }, - { key: 'unpaid', label: 'Unpaid' }, - ], - query: (query, role) => { - switch(role.value) { - case 'active': - query.modify('active'); - break; - case 'inactive': - query.modify('inactive'); - break; - case 'overdue': - query.modify('overdue'); - break; - case 'unpaid': - query.modify('unpaid'); - break; - } - }, - }, - created_at: { - column: 'created_at', - } - }; + static get meta() { + return VendorSettings; } } diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index a7a0d6505..0f56e122f 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { difference, chain, uniq } from 'lodash'; import { kebabCase } from 'lodash'; +import R from 'ramda'; import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; import { @@ -606,6 +607,17 @@ export default class AccountsService { this.eventDispatcher.dispatch(events.accounts.onActivated); } + /** + * Parsees accounts list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieve accounts datatable list. * @param {number} tenantId @@ -613,21 +625,26 @@ export default class AccountsService { */ public async getAccountsList( tenantId: number, - filter: IAccountsFilter + filterDTO: IAccountsFilter ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> { const { Account } = this.tenancy.models(tenantId); + + // Parses the stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. 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) => { dynamicList.buildQuery()(builder); + builder.modify('inactiveMode', filter.inactiveMode); }); return { @@ -727,10 +744,11 @@ export default class AccountsService { })); return flatToNestedArray( this.i18nService.i18nMapper(_accounts, ['account_type_label'], tenantId), - { - id: 'id', - parentId: 'parent_account_id', - }); + { + id: 'id', + parentId: 'parent_account_id', + } + ); } /** diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index efeb789bc..37266d4e4 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -1,6 +1,6 @@ import { Inject, Service } from 'typedi'; import { omit, defaultTo } from 'lodash'; -import async from 'async'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -265,6 +265,16 @@ export default class CustomersService { return this.transformContactToCustomer(contact); } + /** + * Parses customers list filter DTO. + * @param filterDTO - + */ + private parseCustomersListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieve customers paginated list. * @param {number} tenantId - Tenant id. @@ -272,7 +282,7 @@ export default class CustomersService { */ public async getCustomersList( tenantId: number, - customersFilter: ICustomersFilter + filterDTO: ICustomersFilter ): Promise<{ customers: ICustomer[]; pagination: IPaginationMeta; @@ -280,17 +290,23 @@ export default class CustomersService { }> { const { Customer } = this.tenancy.models(tenantId); + // Parses customers list filter DTO. + const filter = this.parseCustomersListFilterDTO(filterDTO); + // Dynamic list. const dynamicList = await this.dynamicListService.dynamicList( tenantId, Customer, - customersFilter + filter ); + + // Customers. const { results, pagination } = await Customer.query() - .onBuild((query) => { - dynamicList.buildQuery()(query); + .onBuild((builder) => { + dynamicList.buildQuery()(builder); + builder.modify('inactiveMode', filter.inactiveMode); }) - .pagination(customersFilter.page - 1, customersFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { customers: results.map(this.transformContactToCustomer), diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 6efa0f939..6104eb15d 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -1,5 +1,6 @@ import { Inject, Service } from 'typedi'; -import { intersection, defaultTo } from 'lodash'; +import { defaultTo } from 'lodash'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -255,6 +256,12 @@ export default class VendorsService { ); } + private parseVendorsListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieve vendors datatable list. * @param {number} tenantId - Tenant id. @@ -262,7 +269,7 @@ export default class VendorsService { */ public async getVendorsList( tenantId: number, - vendorsFilter: IVendorsFilter + filterDTO: IVendorsFilter ): Promise<{ vendors: IVendor[]; pagination: IPaginationMeta; @@ -270,21 +277,28 @@ export default class VendorsService { }> { const { Vendor } = this.tenancy.models(tenantId); - const dynamicFilter = await this.dynamicListService.dynamicList( + // Parses vendors list filter DTO. + const filter = this.parseVendorsListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( tenantId, Vendor, - vendorsFilter + filter ); + + // Vendors list. const { results, pagination } = await Vendor.query() .onBuild((builder) => { - dynamicFilter.buildQuery()(builder); + dynamicList.buildQuery()(builder); + builder.modify('inactiveMode', filter.inactiveMode); }) - .pagination(vendorsFilter.page - 1, vendorsFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { vendors: results, pagination, - filterMeta: dynamicFilter.getResponseMeta(), + filterMeta: dynamicList.getResponseMeta(), }; } diff --git a/server/src/services/DynamicListing/DynamicListAbstruct.ts b/server/src/services/DynamicListing/DynamicListAbstruct.ts new file mode 100644 index 000000000..d7997966b --- /dev/null +++ b/server/src/services/DynamicListing/DynamicListAbstruct.ts @@ -0,0 +1,6 @@ + + + +export default class DynamicListAbstruct { + +} \ No newline at end of file diff --git a/server/src/services/DynamicListing/DynamicListCustomView.ts b/server/src/services/DynamicListing/DynamicListCustomView.ts new file mode 100644 index 000000000..ecaff2177 --- /dev/null +++ b/server/src/services/DynamicListing/DynamicListCustomView.ts @@ -0,0 +1,52 @@ +import { Inject, Service } from 'typedi'; +import DynamicListAbstruct from './DynamicListAbstruct'; +import DynamicFilterViews from 'lib/DynamicFilter/DynamicFilterViews'; +import { ServiceError } from 'exceptions'; +import HasTenancyService from 'services/Tenancy/TenancyService'; +import {ERRORS } from './constants'; +import { IModel }from 'interfaces'; + +@Service() +export default class DynamicListCustomView extends DynamicListAbstruct { + @Inject() + tenancy: HasTenancyService; + + /** + * Retreive custom view or throws error not found. + * @param {number} tenantId + * @param {number} viewId + * @return {Promise} + */ + private getCustomViewOrThrowError = async ( + tenantId: number, + viewId: number, + model: IModel + ) => { + const { viewRepository } = this.tenancy.repositories(tenantId); + const view = await viewRepository.findOneById(viewId, 'roles'); + + if (!view || view.resourceModel !== model.name) { + throw new ServiceError(ERRORS.VIEW_NOT_FOUND); + } + return view; + }; + + /** + * Dynamic list custom view. + * @param {IModel} model + * @param {number} customViewId + * @returns + */ + public dynamicListCustomView = async ( + tenantId: number, + model, + customViewId: number + ) => { + const view = await this.getCustomViewOrThrowError( + tenantId, + customViewId, + model + ); + return new DynamicFilterViews(view); + }; +} diff --git a/server/src/services/DynamicListing/DynamicListFilterRoles.ts b/server/src/services/DynamicListing/DynamicListFilterRoles.ts new file mode 100644 index 000000000..c79944b27 --- /dev/null +++ b/server/src/services/DynamicListing/DynamicListFilterRoles.ts @@ -0,0 +1,103 @@ +import { Service } from 'typedi'; +import * as R from 'ramda'; +import validator from 'is-my-json-valid'; +import { IFilterRole, IModel } from 'interfaces'; +import DynamicListAbstruct from './DynamicListAbstruct'; +import DynamicFilterFilterRoles from 'lib/DynamicFilter/DynamicFilterFilterRoles'; +import { ERRORS } from './constants'; +import { ServiceError } from 'exceptions'; + +@Service() +export default class DynamicListFilterRoles extends DynamicListAbstruct { + /** + * Validates filter roles schema. + * @param {IFilterRole[]} filterRoles - Filter roles. + */ + private validateFilterRolesSchema = (filterRoles: IFilterRole[]) => { + const validate = validator({ + required: true, + type: 'object', + properties: { + condition: { type: 'string' }, + fieldKey: { required: true, type: 'string' }, + value: { required: true }, + }, + }); + const invalidFields = filterRoles.filter((filterRole) => { + return !validate(filterRole); + }); + if (invalidFields.length > 0) { + throw new ServiceError(ERRORS.STRINGIFIED_FILTER_ROLES_INVALID); + } + }; + + /** + * Retrieve filter roles fields key that not exists on the given model. + * @param {IModel} model + * @param {IFilterRole} filterRoles + * @returns {string[]} + */ + private getFilterRolesFieldsNotExist = ( + model, + filterRoles: IFilterRole[] + ): string[] => { + return filterRoles + .filter((filterRole) => !model.getField(filterRole.fieldKey)) + .map((filterRole) => filterRole.fieldKey); + }; + + /** + * Validates existance the fields of filter roles. + * @param {IModel} model + * @param {IFilterRole[]} filterRoles + * @throws {ServiceError} + */ + private validateFilterRolesFieldsExistance = ( + model: IModel, + filterRoles: IFilterRole[] + ) => { + const invalidFieldsKeys = this.getFilterRolesFieldsNotExist( + model, + filterRoles + ); + if (invalidFieldsKeys.length > 0) { + throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); + } + }; + + /** + * Associate index to filter roles. + * @param {IFilterRole[]} filterRoles + * @returns {IFilterRole[]} + */ + private incrementFilterRolesIndex = ( + filterRoles: IFilterRole[] + ): IFilterRole[] => { + return filterRoles.map((filterRole, index) => ({ + ...filterRole, + index: index + 1, + })); + }; + + /** + * Dynamic list filter roles. + * @param {IModel} model + * @param {IFilterRole[]} filterRoles + * @returns {DynamicFilterFilterRoles} + */ + public dynamicList = ( + model: IModel, + filterRoles: IFilterRole[] + ): DynamicFilterFilterRoles => { + const filterRolesParsed = R.compose(this.incrementFilterRolesIndex)( + filterRoles + ); + // Validate filter roles json schema. + this.validateFilterRolesSchema(filterRolesParsed); + + // Validate the model resource fields. + this.validateFilterRolesFieldsExistance(model, filterRoles); + + return new DynamicFilterFilterRoles(filterRolesParsed); + }; +} diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts index d7abb5e94..94ee4ec0a 100644 --- a/server/src/services/DynamicListing/DynamicListService.ts +++ b/server/src/services/DynamicListing/DynamicListService.ts @@ -1,161 +1,88 @@ import { Service, Inject } from 'typedi'; -import validator from 'is-my-json-valid'; import { Request, Response, NextFunction } from 'express'; +import { castArray, isEmpty } from 'lodash'; import { ServiceError } from 'exceptions'; +import { DynamicFilter } from 'lib/DynamicFilter'; import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterViews, - DynamicFilterFilterRoles, -} from 'lib/DynamicFilter'; -import { - validateFieldKeyExistance, - validateFilterRolesFieldsExistance, -} from 'lib/ViewRolesBuilder'; -import { - IDynamicListFilterDTO, - IFilterRole, + IDynamicListFilter, IDynamicListService, + IFilterRole, IModel, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; - -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', -}; +import DynamicListFilterRoles from './DynamicListFilterRoles'; +import DynamicListSortBy from './DynamicListSortBy'; +import DynamicListCustomView from './DynamicListCustomView'; @Service() export default class DynamicListService implements IDynamicListService { @Inject() tenancy: TenancyService; - /** - * Retreive custom view or throws error not found. - * @param {number} tenantId - * @param {number} viewId - * @return {Promise} - */ - private async getCustomViewOrThrowError( - tenantId: number, - viewId: number, - model: IModel - ) { - const { viewRepository } = this.tenancy.repositories(tenantId); - const view = await viewRepository.findOneById(viewId, 'roles'); + @Inject() + dynamicListFilterRoles: DynamicListFilterRoles; - if (!view || view.resourceModel !== model.name) { - throw new ServiceError(ERRORS.VIEW_NOT_FOUND); - } - return view; - } + @Inject() + dynamicListSortBy: DynamicListSortBy; + + @Inject() + dynamicListView: DynamicListCustomView; /** - * Validates the sort column whether exists. - * @param {IModel} model - * @param {string} columnSortBy - Sort column - * @throws {ServiceError} + * Parses filter DTO. */ - private validateSortColumnExistance(model: any, columnSortBy: string) { - const notExistsField = validateFieldKeyExistance(model, 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} - */ - private validateRolesFieldsExistance( - model: IModel, - filterRoles: IFilterRole[] - ) { - const invalidFieldsKeys = validateFilterRolesFieldsExistance( - model, - filterRoles - ); - - if (invalidFieldsKeys.length > 0) { - throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); - } - } - - /** - * Validates filter roles schema. - * @param {IFilterRole[]} filterRoles - */ - private validateFilterRolesSchema(filterRoles: IFilterRole[]) { - const validate = validator({ - required: true, - type: 'object', - properties: { - condition: { type: 'string' }, - fieldKey: { required: true, type: 'string' }, - value: { required: true }, - }, - }); - const invalidFields = filterRoles.filter((filterRole) => { - const isValid = validate(filterRole); - return isValid ? false : true; - }); - if (invalidFields.length > 0) { - throw new ServiceError('stringified_filter_roles_invalid'); - } - } + private parseFilterObject = (model, filterDTO) => { + return { + // Merges the default properties with filter object. + ...model.defaultSort ? { + sortOrder: model.defaultSort.sortOrder, + columnSortBy: model.defaultSort.sortOrder, + } : {}, + ...filterDTO, + }; + }; /** * Dynamic listing. * @param {number} tenantId - Tenant id. * @param {IModel} model - Model. - * @param {IDynamicListFilterDTO} filter - Dynamic filter DTO. + * @param {IDynamicListFilter} filter - Dynamic filter DTO. */ - public async dynamicList( + public dynamicList = async ( tenantId: number, model: IModel, - filter: IDynamicListFilterDTO - ) { + filter: IDynamicListFilter + ) => { const dynamicFilter = new DynamicFilter(model); - // Custom view filter roles. - if (filter.customViewId) { - const view = await this.getCustomViewOrThrowError( - tenantId, - filter.customViewId, - model - ); - const viewFilter = new DynamicFilterViews(view); - dynamicFilter.setFilter(viewFilter); - } - // Sort by the given column. - if (filter.columnSortBy) { - this.validateSortColumnExistance(model, filter.columnSortBy); + // Parses the filter object. + const parsedFilter = this.parseFilterObject(model, filter); - const sortByFilter = new DynamicFilterSortBy( - filter.columnSortBy, - filter.sortOrder + // Custom view filter roles. + // if (filter.customViewId) { + // const dynamicListCustomView = this.dynamicListView.dynamicListCustomView(); + + // dynamicFilter.setFilter(dynamicListCustomView); + // } + // Sort by the given column. + if (parsedFilter.columnSortBy) { + const dynmaicListSortBy = this.dynamicListSortBy.dynamicSortBy( + model, + parsedFilter.columnSortBy, + parsedFilter.sortOrder, ); - dynamicFilter.setFilter(sortByFilter); + dynamicFilter.setFilter(dynmaicListSortBy); } // Filter roles. - if (filter.filterRoles.length > 0) { - const filterRoles = filter.filterRoles.map((filterRole, index) => ({ - ...filterRole, - index: index + 1, - })); - this.validateFilterRolesSchema(filterRoles); - this.validateRolesFieldsExistance(model, filterRoles); - - // Validate the model resource fields. - const dynamicFilterRoles = new DynamicFilterFilterRoles(filterRoles); + if (!isEmpty(parsedFilter.filterRoles)) { + const dynamicFilterRoles = this.dynamicListFilterRoles.dynamicList( + model, + parsedFilter.filterRoles + ); dynamicFilter.setFilter(dynamicFilterRoles); } return dynamicFilter; - } + }; /** * Middleware to catch services errors @@ -173,25 +100,62 @@ export default class DynamicListService implements IDynamicListService { if (error instanceof ServiceError) { if (error.errorType === 'sort_column_not_found') { return res.boom.badRequest(null, { - errors: [{ type: 'SORT.COLUMN.NOT.FOUND', code: 200 }], + errors: [ + { + type: 'SORT.COLUMN.NOT.FOUND', + message: '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 }], + errors: [ + { + type: 'CUSTOM.VIEW.NOT.FOUND', + message: '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 }], + errors: [ + { + type: 'FILTER.ROLES.FIELDS.NOT.FOUND', + message: '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 }], + errors: [ + { + type: 'STRINGIFIED_FILTER_ROLES_INVALID', + message: 'Stringified filter roles json invalid.', + code: 400, + }, + ], }); } } next(error); } + + /** + * Parses stringified filter roles. + * @param {string} stringifiedFilterRoles - Stringified filter roles. + */ + public parseStringifiedFilter = (filterRoles: IDynamicListFilter) => { + return { + ...filterRoles, + filterRoles: filterRoles.stringifiedFilterRoles + ? castArray(JSON.parse(filterRoles.stringifiedFilterRoles)) + : [], + }; + }; } diff --git a/server/src/services/DynamicListing/DynamicListSortBy.ts b/server/src/services/DynamicListing/DynamicListSortBy.ts new file mode 100644 index 000000000..df6c75fa7 --- /dev/null +++ b/server/src/services/DynamicListing/DynamicListSortBy.ts @@ -0,0 +1,40 @@ +import { Service } from 'typedi'; +import DynamicListAbstruct from './DynamicListAbstruct'; +import DynamicFilterSortBy from 'lib/DynamicFilter/DynamicFilterSortBy'; +import { IModel, ISortOrder } from 'interfaces'; +import { ServiceError } from 'exceptions'; +import { ERRORS } from './constants'; + +@Service() +export default class DynamicListSortBy extends DynamicListAbstruct { + /** + * Dynamic list sort by. + * @param {IModel} model + * @param {string} columnSortBy + * @param {ISortOrder} sortOrder + * @returns {DynamicFilterSortBy} + */ + public dynamicSortBy( + model: IModel, + columnSortBy: string, + sortOrder: ISortOrder + ) { + this.validateSortColumnExistance(model, columnSortBy); + + return new DynamicFilterSortBy(columnSortBy, sortOrder); + } + + /** + * Validates the sort column whether exists. + * @param {IModel} model - Model. + * @param {string} columnSortBy - Sort column + * @throws {ServiceError} + */ + private validateSortColumnExistance(model: any, columnSortBy: string) { + const field = model.getField(columnSortBy); + + if (!field) { + throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND); + } + } +} diff --git a/server/src/services/DynamicListing/constants.ts b/server/src/services/DynamicListing/constants.ts new file mode 100644 index 000000000..414de46ac --- /dev/null +++ b/server/src/services/DynamicListing/constants.ts @@ -0,0 +1,6 @@ +export const ERRORS = { + STRINGIFIED_FILTER_ROLES_INVALID: 'stringified_filter_roles_invalid', + VIEW_NOT_FOUND: 'view_not_found', + SORT_COLUMN_NOT_FOUND: 'sort_column_not_found', + FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found', +}; diff --git a/server/src/services/DynamicListing/validators.ts b/server/src/services/DynamicListing/validators.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 39a8e8fdd..6fc465de8 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -1,6 +1,7 @@ import { Service, Inject } from 'typedi'; -import { difference, sumBy, omit, map } from 'lodash'; +import { difference, sumBy, omit } from 'lodash'; import moment from 'moment'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -630,6 +631,16 @@ export default class ExpensesService implements IExpensesService { return expenses.filter((expense) => expense.publishedAt); } + /** + * Parses filter DTO of expenses list. + * @param filterDTO - + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve expenses datatable lsit. * @param {number} tenantId @@ -638,35 +649,41 @@ export default class ExpensesService implements IExpensesService { */ public async getExpensesList( tenantId: number, - expensesFilter: IExpensesFilter + filterDTO: IExpensesFilter ): Promise<{ expenses: IExpense[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { Expense } = this.tenancy.models(tenantId); - const dynamicFilter = await this.dynamicListService.dynamicList( + + // Parses list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( tenantId, Expense, - expensesFilter + filter, ); this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, - expensesFilter, + filter, }); const { results, pagination } = await Expense.query() .onBuild((builder) => { builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('categories.expenseAccount'); - dynamicFilter.buildQuery()(builder); + + dynamicList.buildQuery()(builder); }) - .pagination(expensesFilter.page - 1, expensesFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { expenses: results, pagination, - filterMeta: dynamicFilter.getResponseMeta(), + filterMeta: dynamicList.getResponseMeta(), }; } diff --git a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts index f03abf77c..71f320256 100644 --- a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts +++ b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts @@ -29,7 +29,6 @@ export default class JournalSheetService { fromRange: null, toRange: null, accountsIds: [], - transactionTypes: [], numberFormat: { noCents: false, divideOn1000: false, @@ -107,6 +106,13 @@ export default class JournalSheetService { } query.modify('filterDateRange', filter.fromDate, filter.toDate); query.orderBy(['date', 'createdAt', 'indexGroup', 'index']); + + if (filter.transactionType) { + return query.where('reference_type', filter.transactionType); + } + if (filter.transactionType && filter.transactionId) { + return query.where('reference_id', filter.transactionId); + } }); // Transform the transactions array to journal collection. const transactionsJournal = Journal.fromTransactions( diff --git a/server/src/services/Inventory/InventoryAdjustmentService.ts b/server/src/services/Inventory/InventoryAdjustmentService.ts index 2019c4a94..2b0b75da2 100644 --- a/server/src/services/Inventory/InventoryAdjustmentService.ts +++ b/server/src/services/Inventory/InventoryAdjustmentService.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { omit } from 'lodash'; import moment from 'moment'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -225,8 +226,8 @@ export default class InventoryAdjustmentService { /** * Publish the inventory adjustment transaction. - * @param tenantId - * @param inventoryAdjustmentId + * @param {number} tenantId + * @param {number} inventoryAdjustmentId */ async publishInventoryAdjustment( tenantId: number, @@ -265,6 +266,17 @@ export default class InventoryAdjustmentService { ); } + /** + * Parses inventory adjustments list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve the inventory adjustments paginated list. * @param {number} tenantId @@ -272,17 +284,21 @@ export default class InventoryAdjustmentService { */ async getInventoryAdjustments( tenantId: number, - adjustmentsFilter: IInventoryAdjustmentsFilter + filterDTO: IInventoryAdjustmentsFilter ): Promise<{ inventoryAdjustments: IInventoryAdjustment[]; pagination: IPaginationMeta; }> { const { InventoryAdjustment } = this.tenancy.models(tenantId); + // Parses inventory adjustments list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, InventoryAdjustment, - adjustmentsFilter + filter, ); const { results, pagination } = await InventoryAdjustment.query() .onBuild((query) => { @@ -291,7 +307,7 @@ export default class InventoryAdjustmentService { dynamicFilter.buildQuery()(query); }) - .pagination(adjustmentsFilter.page - 1, adjustmentsFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { inventoryAdjustments: results, diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index 96a3c615c..a53cd2e58 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -1,5 +1,6 @@ import { Inject } from 'typedi'; import { difference } from 'lodash'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -377,6 +378,18 @@ export default class ItemCategoriesService implements IItemCategoriesService { } } + /** + * Parses items categories filter DTO. + * @param {} filterDTO + * @returns + */ + private parsesListFilterDTO(filterDTO) { + return R.compose( + // Parses stringified filter roles. + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve item categories list. * @param {number} tenantId @@ -384,10 +397,15 @@ export default class ItemCategoriesService implements IItemCategoriesService { */ public async getItemCategoriesList( tenantId: number, - filter: IItemCategoriesFilter, + filterDTO: IItemCategoriesFilter, authorizedUser: ISystemUser ): Promise<{ itemCategories: IItemCategory[]; filterMeta: IFilterMeta }> { const { ItemCategory } = this.tenancy.models(tenantId); + + // Parses list filter DTO. + const filter = this.parsesListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicList = await this.dynamicListService.dynamicList( tenantId, ItemCategory, diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 4fb06bd14..86aa4894c 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -1,5 +1,6 @@ -import { defaultTo, difference } from 'lodash'; +import { defaultTo } from 'lodash'; import { Service, Inject } from 'typedi'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -525,20 +526,37 @@ export default class ItemsService implements IItemsService { return this.transformItemToResponse(tenantId, item); } + /** + * Parses items list filter DTO. + * @param {} filterDTO - Filter DTO. + */ + private parseItemsListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve items datatable list. * @param {number} tenantId * @param {IItemsFilter} itemsFilter */ - public async itemsList(tenantId: number, itemsFilter: IItemsFilter) { + public async itemsList(tenantId: number, filterDTO: IItemsFilter) { const { Item } = this.tenancy.models(tenantId); + + // Parses items list filter DTO. + const filter = this.parseItemsListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, Item, - itemsFilter + filter ); const { results: items, pagination } = await Item.query() .onBuild((builder) => { + builder.modify('inactiveMode', filter.inactiveMode); + builder.withGraphFetched('inventoryAccount'); builder.withGraphFetched('sellAccount'); builder.withGraphFetched('costAccount'); @@ -546,7 +564,7 @@ export default class ItemsService implements IItemsService { dynamicFilter.buildQuery()(builder); }) - .pagination(itemsFilter.page - 1, itemsFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); const results = items.map((item) => this.transformItemToResponse(tenantId, item) diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index 1261fec4a..2c1a02083 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -1,7 +1,8 @@ import { difference, sumBy, omit, map } from 'lodash'; import { Service, Inject } from 'typedi'; import moment from 'moment'; -import { ServiceError, ServiceErrors } from 'exceptions'; +import * as R from 'ramda'; +import { ServiceError } from 'exceptions'; import { IManualJournalDTO, IManualJournalsService, @@ -768,33 +769,47 @@ export default class ManualJournalsService implements IManualJournalsService { ); } + /** + * Parses filter DTO of the manual journals list. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieve manual journals datatable list. - * @param {number} tenantId - * @param {IManualJournalsFilter} filter + * @param {number} tenantId - + * @param {IManualJournalsFilter} filter - */ public async getManualJournals( tenantId: number, - filter: IManualJournalsFilter + filterDTO: IManualJournalsFilter ): Promise<{ manualJournals: IManualJournal; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { ManualJournal } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList( + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic service. + const dynamicService = await this.dynamicListService.dynamicList( tenantId, ManualJournal, filter ); - this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter, }); const { results, pagination } = await ManualJournal.query() .onBuild((builder) => { - dynamicList.buildQuery()(builder); + dynamicService.buildQuery()(builder); builder.withGraphFetched('entries.account'); }) .pagination(filter.page - 1, filter.pageSize); @@ -802,7 +817,7 @@ export default class ManualJournalsService implements IManualJournalsService { return { manualJournals: results, pagination, - filterMeta: dynamicList.getResponseMeta(), + filterMeta: dynamicService.getResponseMeta(), }; } diff --git a/server/src/services/Purchases/BillPayments/BillPayments.ts b/server/src/services/Purchases/BillPayments/BillPayments.ts index 896f6c5dc..2ada489ac 100644 --- a/server/src/services/Purchases/BillPayments/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments/BillPayments.ts @@ -1,5 +1,6 @@ import { Inject, Service } from 'typedi'; -import { omit, sumBy, difference } from 'lodash'; +import { sumBy, difference } from 'lodash'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -25,7 +26,7 @@ import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { entriesAmountDiff, formatDateFields } from 'utils'; import { ServiceError } from 'exceptions'; -import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes'; +import { ACCOUNT_TYPE } from 'data/AccountTypes'; import VendorsService from 'services/Contacts/VendorsService'; import { ERRORS } from './constants'; @@ -635,6 +636,12 @@ export default class BillPaymentsService implements IBillPaymentsService { ]); } + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieve bill payment paginted and filterable list. * @param {number} tenantId @@ -642,34 +649,40 @@ export default class BillPaymentsService implements IBillPaymentsService { */ public async listBillPayments( tenantId: number, - billPaymentsFilter: IBillPaymentsFilter + filterDTO: IBillPaymentsFilter ): Promise<{ billPayments: IBillPayment; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { BillPayment } = this.tenancy.models(tenantId); - const dynamicFilter = await this.dynamicListService.dynamicList( + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( tenantId, BillPayment, - billPaymentsFilter + filter ); this.logger.info('[bill_payment] try to get bill payments list.', { tenantId, }); + const { results, pagination } = await BillPayment.query() .onBuild((builder) => { builder.withGraphFetched('vendor'); builder.withGraphFetched('paymentAccount'); - dynamicFilter.buildQuery()(builder); + dynamicList.buildQuery()(builder); }) - .pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { billPayments: results, pagination, - filterMeta: dynamicFilter.getResponseMeta(), + filterMeta: dynamicList.getResponseMeta(), }; } diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 439cdfb74..ed95c3c98 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -1,6 +1,7 @@ import { omit, runInContext, sumBy } from 'lodash'; import moment from 'moment'; import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; import composeAsync from 'async/compose'; import { EventDispatcher, @@ -521,6 +522,16 @@ export default class BillsService }); } + /** + * Parses bills list filter DTO. + * @param filterDTO - + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve bills data table list. * @param {number} tenantId - @@ -528,28 +539,33 @@ export default class BillsService */ public async getBills( tenantId: number, - billsFilter: IBillsFilter + filterDTO: IBillsFilter ): Promise<{ bills: IBill; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { Bill } = this.tenancy.models(tenantId); + + // Parses bills list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, Bill, - billsFilter + filter, ); this.logger.info('[bills] trying to get bills data table.', { tenantId, - billsFilter, + filter, }); const { results, pagination } = await Bill.query() .onBuild((builder) => { builder.withGraphFetched('vendor'); dynamicFilter.buildQuery()(builder); }) - .pagination(billsFilter.page - 1, billsFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { bills: results, diff --git a/server/src/services/Resource/ResourceService.ts b/server/src/services/Resource/ResourceService.ts index 9bbf40be2..f672e8e90 100644 --- a/server/src/services/Resource/ResourceService.ts +++ b/server/src/services/Resource/ResourceService.ts @@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi'; import { camelCase, upperFirst } from 'lodash'; import pluralize from 'pluralize'; import { buildFilter } from 'objection-filter'; -import { IModel } from 'interfaces'; +import { IModel, IModelMeta } from 'interfaces'; import { getModelFields, } from 'lib/ViewRolesBuilder' @@ -102,4 +102,18 @@ export default class ResourceService { return buildFilter(resourceModel).build(filter); } + + /** + * Retrieve the resource meta. + * @param {number} tenantId + * @param {string} modelName + * @returns {IModelMeta} + */ + public getResourceMeta(tenantId: number, modelName: string): IModelMeta { + const resourceModel = this.getResourceModel(tenantId, modelName); + + const settings = resourceModel.meta(); + + return settings; + } } \ No newline at end of file diff --git a/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts b/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts index 0db0a1931..167bee544 100644 --- a/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentReceives/PaymentsReceives.ts @@ -1,5 +1,6 @@ import { omit, sumBy, difference } from 'lodash'; import { Service, Inject } from 'typedi'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -613,6 +614,16 @@ export default class PaymentReceiveService implements IPaymentsReceiveService { return saleInvoices; } + /** + * Parses payments receive list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieve payment receives paginated and filterable list. * @param {number} tenantId @@ -620,33 +631,39 @@ export default class PaymentReceiveService implements IPaymentsReceiveService { */ public async listPaymentReceives( tenantId: number, - paymentReceivesFilter: IPaymentReceivesFilter + filterDTO: IPaymentReceivesFilter ): Promise<{ paymentReceives: IPaymentReceive[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { PaymentReceive } = this.tenancy.models(tenantId); - const dynamicFilter = await this.dynamicListService.dynamicList( + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( tenantId, PaymentReceive, - paymentReceivesFilter + filter ); const { results, pagination } = await PaymentReceive.query() .onBuild((builder) => { builder.withGraphFetched('customer'); builder.withGraphFetched('depositAccount'); - dynamicFilter.buildQuery()(builder); + dynamicList.buildQuery()(builder); }) .pagination( - paymentReceivesFilter.page - 1, - paymentReceivesFilter.pageSize + filter.page - 1, + filter.pageSize ); + return { paymentReceives: results, pagination, - filterMeta: dynamicFilter.getResponseMeta(), + filterMeta: dynamicList.getResponseMeta(), }; } diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index 0b6b81f96..ca55b78e1 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -1,5 +1,6 @@ -import { omit, sumBy } from 'lodash'; +import { filter, omit, sumBy } from 'lodash'; import { Service, Inject } from 'typedi'; +import * as R from 'ramda'; import { IEstimatesFilter, IFilterMeta, @@ -412,6 +413,16 @@ export default class SaleEstimateService implements ISalesEstimatesService{ return estimate; } + /** + * Parses estimates list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter + )(filterDTO); + } + /** * Retrieves estimates filterable and paginated list. * @param {number} tenantId - @@ -419,17 +430,22 @@ export default class SaleEstimateService implements ISalesEstimatesService{ */ public async estimatesList( tenantId: number, - estimatesFilter: IEstimatesFilter + filterDTO: IEstimatesFilter ): Promise<{ salesEstimates: ISaleEstimate[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { SaleEstimate } = this.tenancy.models(tenantId); + + // Parses filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, SaleEstimate, - estimatesFilter + filter, ); const { results, pagination } = await SaleEstimate.query() @@ -438,7 +454,7 @@ export default class SaleEstimateService implements ISalesEstimatesService{ builder.withGraphFetched('entries'); dynamicFilter.buildQuery()(builder); }) - .pagination(estimatesFilter.page - 1, estimatesFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { salesEstimates: results, diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index dfdf04290..cf6e2b8b6 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,5 +1,6 @@ import { Service, Inject } from 'typedi'; -import { omit, sumBy, join, entries } from 'lodash'; +import { omit, sumBy } from 'lodash'; +import * as R from 'ramda'; import moment from 'moment'; import composeAsync from 'async/compose'; import { @@ -647,6 +648,17 @@ export default class SaleInvoicesService implements ISalesInvoicesService { return saleInvoice; } + /** + * Parses the sale invoice list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve sales invoices filterable and paginated list. * @param {Request} req @@ -655,22 +667,27 @@ export default class SaleInvoicesService implements ISalesInvoicesService { */ public async salesInvoicesList( tenantId: number, - salesInvoicesFilter: ISalesInvoicesFilter + filterDTO: ISalesInvoicesFilter ): Promise<{ salesInvoices: ISaleInvoice[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { SaleInvoice } = this.tenancy.models(tenantId); + + // Parses stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, SaleInvoice, - salesInvoicesFilter + filter ); this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, - salesInvoicesFilter, + filter, }); const { results, pagination } = await SaleInvoice.query() .onBuild((builder) => { @@ -678,7 +695,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService { builder.withGraphFetched('customer'); dynamicFilter.buildQuery()(builder); }) - .pagination(salesInvoicesFilter.page - 1, salesInvoicesFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { salesInvoices: results, diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index ae4a8a934..3c651b841 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -1,6 +1,7 @@ import { omit, sumBy } from 'lodash'; import { Service, Inject } from 'typedi'; import moment from 'moment'; +import * as R from 'ramda'; import { EventDispatcher, EventDispatcherInterface, @@ -406,6 +407,16 @@ export default class SalesReceiptService implements ISalesReceiptsService { return saleReceipt; } + /** + * Parses the sale receipts list filter DTO. + * @param filterDTO + */ + private parseListFilterDTO(filterDTO) { + return R.compose( + this.dynamicListService.parseStringifiedFilter, + )(filterDTO); + } + /** * Retrieve sales receipts paginated and filterable list. * @param {number} tenantId @@ -413,17 +424,22 @@ export default class SalesReceiptService implements ISalesReceiptsService { */ public async salesReceiptsList( tenantId: number, - salesReceiptsFilter: ISaleReceiptFilter + filterDTO: ISaleReceiptFilter ): Promise<{ salesReceipts: ISaleReceipt[]; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { SaleReceipt } = this.tenancy.models(tenantId); + + // Parses the stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, SaleReceipt, - salesReceiptsFilter + filter, ); this.logger.info('[sale_receipt] try to get sales receipts list.', { @@ -437,7 +453,7 @@ export default class SalesReceiptService implements ISalesReceiptsService { dynamicFilter.buildQuery()(builder); }) - .pagination(salesReceiptsFilter.page - 1, salesReceiptsFilter.pageSize); + .pagination(filter.page - 1, filter.pageSize); return { salesReceipts: results,