diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index fb3670580..96d3870b3 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -127,18 +127,12 @@ export default class AccountsController extends BaseController{ ]; } - /** - * Account param schema validation. - */ get accountParamSchema() { return [ param('id').exists().isNumeric().toInt() ]; } - /** - * Accounts list schema validation. - */ get accountsListSchema() { return [ query('custom_view_id').optional().isNumeric().toInt(), @@ -149,9 +143,6 @@ export default class AccountsController extends BaseController{ ]; } - /** - * - */ get bulkSelectIdsQuerySchema() { return [ query('ids').isArray({ min: 2 }), @@ -328,8 +319,12 @@ export default class AccountsController extends BaseController{ filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const accounts = await this.accountsService.getAccountsList(tenantId, filter); - return res.status(200).send({ accounts }); + const { accounts, filterMeta } = await this.accountsService.getAccountsList(tenantId, filter); + + return res.status(200).send({ + accounts, + filter_meta: this.transfromToResponse(filterMeta) + }); } catch (error) { next(error); } @@ -358,9 +353,8 @@ export default class AccountsController extends BaseController{ } if (error.errorType === 'account_type_not_found') { return res.boom.badRequest( - 'The given account type not found.', { - errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] - } + 'The given account type not found.', + { errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] } ); } if (error.errorType === 'account_type_not_allowed_to_changed') { diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts index 1ff423c00..6299a9ac0 100644 --- a/server/src/api/controllers/BaseController.ts +++ b/server/src/api/controllers/BaseController.ts @@ -1,6 +1,6 @@ import { Response, Request, NextFunction } from 'express'; import { matchedData, validationResult } from "express-validator"; -import { camelCase, omit } from "lodash"; +import { camelCase, snakeCase, omit } from "lodash"; import { mapKeysDeep } from 'utils' export default class BaseController { @@ -55,4 +55,12 @@ export default class BaseController { } next(); } + + /** + * Transform the given data to response. + * @param {any} data + */ + transfromToResponse(data: any) { + return mapKeysDeep(data, (v, k) => snakeCase(k)); + } } \ No newline at end of file diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index b271cf7f9..0ee03b2bc 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -46,6 +46,12 @@ export default class CustomersController extends ContactsController { this.validationResult, asyncMiddleware(this.deleteBulkCustomers.bind(this)) ); + router.get('/', [ + + ], + this.validationResult, + asyncMiddleware(this.getCustomersList.bind(this)) + ); router.get('/:id', [ ...this.specificContactSchema, ], @@ -193,4 +199,15 @@ export default class CustomersController extends ContactsController { next(error); } } + + + async getCustomersList(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + + try { + await this.customersService.getCustomersList(tenantId) + } catch (error) { + next(error); + } + } } \ No newline at end of file diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index 668dbba45..e0f557664 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -30,7 +30,8 @@ export default class ExpensesController extends BaseController { asyncMiddleware(this.newExpense.bind(this)), this.catchServiceErrors, ); - router.post('/publish', [ + router.post( + '/publish', [ ...this.bulkSelectSchema, ], this.bulkPublishExpenses.bind(this), @@ -69,7 +70,9 @@ export default class ExpensesController extends BaseController { this.catchServiceErrors, ); router.get( - '/', + '/', [ + ...this.expensesListSchema, + ], asyncMiddleware(this.getExpensesList.bind(this)), this.dynamicListService.handlerErrorsToResponse, this.catchServiceErrors, @@ -89,6 +92,7 @@ export default class ExpensesController extends BaseController { check('currency_code').optional(), check('exchange_rate').optional().isNumeric().toFloat(), check('publish').optional().isBoolean().toBoolean(), + check('categories').exists().isArray({ min: 1 }), check('categories.*.index').exists().isNumeric().toInt(), check('categories.*.expense_account_id').exists().isNumeric().toInt(), @@ -120,6 +124,20 @@ export default class ExpensesController extends BaseController { ]; } + + get expensesListSchema() { + return [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ]; + } + /** * Creates a new expense on * @param {Request} req @@ -240,12 +258,23 @@ export default class ExpensesController extends BaseController { 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 = await this.expensesService.getExpensesList(tenantId, filter); - return res.status(200).send({ expenses }); + const { expenses, pagination, filterMeta } = await this.expensesService.getExpensesList(tenantId, filter); + + return res.status(200).send({ + expenses, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } @@ -259,29 +288,34 @@ export default class ExpensesController extends BaseController { catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { if (error.errorType === 'expense_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }], - }); + return res.boom.badRequest( + 'Expense not found.', + { errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }] } + ); } if (error.errorType === 'total_amount_equals_zero') { - return res.boom.badRequest(null, { - errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }], - }); + return res.boom.badRequest( + 'Expense total should not equal zero.', + { errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }] }, + ); } if (error.errorType === 'payment_account_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }], - }); + return res.boom.badRequest( + 'Payment account not found.', + { errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }] }, + ); } if (error.errorType === 'some_expenses_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] - }) + return res.boom.badRequest( + 'Some expense accounts not found.', + { errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] }, + ); } if (error.errorType === 'payment_account_has_invalid_type') { - return res.boom.badRequest(null, { - errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], - }); + return res.boom.badRequest( + 'Payment account has invalid type.', + { errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], }, + ); } if (error.errorType === 'expenses_account_has_invalid_type') { return res.boom.badRequest(null, { diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index f31b95991..8eb5d28e8 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -10,12 +10,16 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware'; import { IItemCategoryOTD } from 'interfaces'; import { ServiceError } from 'exceptions'; import BaseController from 'api/controllers/BaseController'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class ItemsCategoriesController extends BaseController { @Inject() itemCategoriesService: ItemCategoriesService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor method. */ @@ -64,6 +68,7 @@ export default class ItemsCategoriesController extends BaseController { this.validationResult, asyncMiddleware(this.getList.bind(this)), this.handlerServiceError, + this.dynamicListService.handlerErrorsToResponse, ); return router; } @@ -112,8 +117,9 @@ export default class ItemsCategoriesController extends BaseController { */ get categoriesListValidationSchema() { return [ - query('column_sort_order').optional().trim().escape(), + query('column_sort_by').optional().trim().escape(), query('sort_order').optional().trim().escape().isIn(['desc', 'asc']), + query('stringified_filter_roles').optional().isJSON(), ]; } @@ -189,13 +195,21 @@ export default class ItemsCategoriesController extends BaseController { */ async getList(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; - const itemCategoriesFilter = this.matchedQueryData(req); + const itemCategoriesFilter = { + filterRoles: [], + sortOrder: 'asc', + columnSortBy: 'created_at', + ...this.matchedQueryData(req), + }; try { - const itemCategories = await this.itemCategoriesService.getItemCategoriesList( + const { itemCategories, filterMeta } = await this.itemCategoriesService.getItemCategoriesList( tenantId, itemCategoriesFilter, user, ); - return res.status(200).send({ item_categories: itemCategories }); + return res.status(200).send({ + item_categories: itemCategories, + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 22ea33c28..a063f01ac 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -149,10 +149,12 @@ export default class ItemsController extends BaseController { */ get validateListQuerySchema() { return [ - query('column_sort_order').optional().trim().escape(), + query('column_sort_by').optional().trim().escape(), query('sort_order').optional().isIn(['desc', 'asc']), + query('page').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(), + query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), ] @@ -239,14 +241,22 @@ export default class ItemsController extends BaseController { 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 items = await this.itemsService.itemsList(tenantId, filter); - return res.status(200).send({ items }); + const { items, pagination, filterMeta } = await this.itemsService.itemsList(tenantId, filter); + + return res.status(200).send({ + items, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } @@ -344,6 +354,16 @@ export default class ItemsController extends BaseController { errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }], }); } + if (error.errorType === 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS') { + return res.status(400).send({ + errors: [{ type: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', code: 310 }], + }); + } + if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') { + return res.status(400).send({ + errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }], + }) + } } } } \ No newline at end of file diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index 486d681f8..b4ad00dc8 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -299,17 +299,22 @@ export default class ManualJournalsController extends BaseController { 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 = await this.manualJournalsService.getManualJournals(tenantId, filter); - return res.status(200).send({ manualJournals }); - } catch (error) { - console.log(error); + const { manualJournals, pagination, filterMeta } = await this.manualJournalsService.getManualJournals(tenantId, filter); + return res.status(200).send({ + manual_journals: manualJournals, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); + } catch (error) { next(error); } } diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index 478d7eb66..1b72a99d6 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -381,6 +381,20 @@ export default class BillsPayments extends BaseController { * @return {Response} */ async getBillsPayments(req: Request, res: Response) { + const { tenantId } = req.params; + const billPaymentsFilter = this.matchedQueryData(req); + try { + const { billPayments, pagination, filterMeta } = await this.billPaymentService + .listBillPayments(tenantId, billPaymentsFilter); + + return res.status(200).send({ + bill_payments: billPayments, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta) + }); + } catch (error) { + next(error); + } } } \ No newline at end of file diff --git a/server/src/api/controllers/Resources.js b/server/src/api/controllers/Resources.js deleted file mode 100644 index bf1063a41..000000000 --- a/server/src/api/controllers/Resources.js +++ /dev/null @@ -1,115 +0,0 @@ -import express from 'express'; -import { - param, - query, -} from 'express-validator'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.get('/:resource_slug/data', - this.resourceData.validation, - asyncMiddleware(this.resourceData.handler)); - - router.get('/:resource_slug/columns', - this.resourceColumns.validation, - asyncMiddleware(this.resourceColumns.handler)); - - router.get('/:resource_slug/fields', - this.resourceFields.validation, - asyncMiddleware(this.resourceFields.handler)); - - return router; - }, - - /** - * Retrieve resource data of the given resource key/slug. - */ - resourceData: { - validation: [ - param('resource_slug').trim().escape().exists(), - ], - async handler(req, res) { - const { AccountType } = req.models; - const { resource_slug: resourceSlug } = req.params; - - const data = await AccountType.query(); - - return res.status(200).send({ - data, - resource_slug: resourceSlug, - }); - }, - }, - - /** - * Retrieve resource columns of the given resource. - */ - resourceColumns: { - validation: [ - param('resource_slug').trim().escape().exists(), - ], - async handler(req, res) { - const { resource_slug: resourceSlug } = req.params; - const { Resource } = req.models; - - const resource = await Resource.query() - .where('name', resourceSlug) - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }], - }); - } - const resourceFields = resource.fields - .filter((field) => field.columnable) - .map((field) => ({ - id: field.id, - label: field.labelName, - key: field.key, - })); - - return res.status(200).send({ - resource_columns: resourceFields, - resource_slug: resourceSlug, - }); - }, - }, - - /** - * Retrieve resource fields of the given resource. - */ - resourceFields: { - validation: [ - param('resource_slug').trim().escape().exists(), - query('predefined').optional().isBoolean().toBoolean(), - query('builtin').optional().isBoolean().toBoolean(), - ], - async handler(req, res) { - const { resource_slug: resourceSlug } = req.params; - const { Resource } = req.models; - - const resource = await Resource.query() - .where('name', resourceSlug) - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }], - }); - } - return res.status(200).send({ - resource_fields: resource.fields, - resource_slug: resourceSlug, - }); - }, - }, -}; diff --git a/server/src/api/controllers/Resources.ts b/server/src/api/controllers/Resources.ts new file mode 100644 index 000000000..91f0d8955 --- /dev/null +++ b/server/src/api/controllers/Resources.ts @@ -0,0 +1,44 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { + param, + query, +} from 'express-validator'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseController from './BaseController'; +import { Service } from 'typedi'; +import ResourceFieldsKeys from 'data/ResourceFieldsKeys'; + +@Service() +export default class ResourceController extends BaseController{ + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get('/:resource_model/fields', + this.resourceModelParamSchema, + asyncMiddleware(this.resourceFields.bind(this)) + ); + return router; + } + + get resourceModelParamSchema() { + return [ + param('resource_model').exists().trim().escape(), + ]; + } + + /** + * Retrieve resource fields of the given resource. + */ + resourceFields(req: Request, res: Response, next: NextFunction) { + const { resource_model: resourceModel } = req.params; + + try { + + } catch (error) { + next(error); + } + } +}; diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 2e5f9d401..dd5f589ec 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -297,7 +297,21 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async getEstimates(req: Request, res: Response) { - + async getEstimates(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const estimatesFilter: ISalesEstimatesFilter = this.matchedQueryData(req); + + try { + const { salesEstimates, pagination, filterMeta } = await this.saleEstimateService + .estimatesList(tenantId, estimatesFilter); + + return res.status(200).send({ + sales_estimates: this.transfromToResponse(salesEstimates), + pagination, + filter_meta: this.transfromToResponse(filterMeta), + }) + } catch (error) { + next(error); + } } }; diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index b4b9799de..db4ce0a16 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { difference } from 'lodash'; import { raw } from 'objection'; @@ -7,7 +7,8 @@ import BaseController from '../BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleInvoiceService from 'services/Sales/SalesInvoices'; import ItemsService from 'services/Items/ItemsService'; -import { ISaleInvoiceOTD } from 'interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces'; @Service() export default class SaleInvoicesController extends BaseController{ @@ -17,6 +18,9 @@ export default class SaleInvoicesController extends BaseController{ @Inject() saleInvoiceService: SaleInvoiceService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor. */ @@ -412,7 +416,21 @@ export default class SaleInvoicesController extends BaseController{ * @param {Response} res * @param {Function} next */ - async getSalesInvoices(req, res) { - + public async getSalesInvoices(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req.params; + const salesInvoicesFilter: ISalesInvoicesFilter = req.query; + + try { + const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList( + tenantId, salesInvoicesFilter, + ); + return res.status(200).send({ + sales_invoices: salesInvoices, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); + } catch (error) { + next(error); + } } } diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index 1eb197e24..53823def1 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -274,7 +274,6 @@ export default class SalesReceiptsController extends BaseController{ const { id: saleReceiptId } = req.params; const saleReceipt = { ...req.body }; - const errorReasons = []; // Handle all errors with reasons messages. @@ -296,7 +295,19 @@ export default class SalesReceiptsController extends BaseController{ * @param {Request} req * @param {Response} res */ - async listingSalesReceipts(req: Request, res: Response) { + async getSalesReceipts(req: Request, res: Response) { + const { tenantId } = req; + const filter = { + sortOrder: 'asc', + page: 1, + pageSize: 12, + ...this.matchedBodyData(req), + }; + try { + + } catch (error) { + next(error); + } } }; diff --git a/server/src/api/controllers/Users.ts b/server/src/api/controllers/Users.ts index 0dc47c8ae..1c2bae9b8 100644 --- a/server/src/api/controllers/Users.ts +++ b/server/src/api/controllers/Users.ts @@ -222,19 +222,22 @@ export default class UsersController extends BaseController{ } if (error instanceof ServiceError) { if (error.errorType === 'user_not_found') { - return res.status(404).send({ - errors: [{ type: 'USER.NOT.FOUND', code: 100 }], - }); + return res.boom.badRequest( + 'User not found.', + { errors: [{ type: 'USER.NOT.FOUND', code: 100 }] } + ); } if (error.errorType === 'user_already_active') { - return res.status(404).send({ - errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }], - }); + return res.boom.badRequest( + 'User is already active.', + { errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] }, + ); } if (error.errorType === 'user_already_inactive') { - return res.status(404).send({ - errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }], - }); + return res.boom.badRequest( + 'User is already inactive.', + { errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] }, + ); } if (error.errorType === 'user_same_the_authorized_user') { return res.boom.badRequest( diff --git a/server/src/api/controllers/Views.js b/server/src/api/controllers/Views.js deleted file mode 100644 index 68063e77f..000000000 --- a/server/src/api/controllers/Views.js +++ /dev/null @@ -1,473 +0,0 @@ -import { difference, pick } from 'lodash'; -import express from 'express'; -import { - check, - query, - param, - oneOf, - validationResult, -} from 'express-validator'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import { - validateViewRoles, -} from 'lib/ViewRolesBuilder'; - -export default { - resource: 'items', - - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.get('/', - this.listViews.validation, - asyncMiddleware(this.listViews.handler)); - - router.post('/', - this.createView.validation, - asyncMiddleware(this.createView.handler)); - - router.post('/:view_id', - this.editView.validation, - asyncMiddleware(this.editView.handler)); - - router.delete('/:view_id', - this.deleteView.validation, - asyncMiddleware(this.deleteView.handler)); - - router.get('/:view_id', - asyncMiddleware(this.getView.handler)); - - router.get('/:view_id/resource', - this.getViewResource.validation, - asyncMiddleware(this.getViewResource.handler)); - - return router; - }, - - /** - * List all views that associated with the given resource. - */ - listViews: { - validation: [ - oneOf([ - query('resource_name').exists().trim().escape(), - ], [ - query('resource_id').exists().isNumeric().toInt(), - ]), - ], - async handler(req, res) { - const { Resource, View } = req.models; - const filter = { ...req.query }; - - const resource = await Resource.query().onBuild((builder) => { - if (filter.resource_id) { - builder.where('id', filter.resource_id); - } - if (filter.resource_name) { - builder.where('name', filter.resource_name); - } - builder.first(); - }); - - const views = await View.query().where('resource_id', resource.id); - - return res.status(200).send({ views }); - }, - }, - - /** - * Retrieve view details of the given view id. - */ - getView: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { view_id: viewId } = req.params; - const { View } = req.models; - - const view = await View.query() - .where('id', viewId) - .withGraphFetched('resource') - .withGraphFetched('columns') - .withGraphFetched('roles.field') - .first(); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }], - }); - } - return res.status(200).send({ view: view.toJSON() }); - }, - }, - - /** - * Delete the given view of the resource. - */ - deleteView: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { View } = req.models; - const { view_id: viewId } = req.params; - const view = await View.query().findById(viewId); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }], - }); - } - if (view.predefined) { - return res.boom.badRequest(null, { - errors: [{ type: 'PREDEFINED_VIEW', code: 200 }], - }); - } - await Promise.all([ - view.$relatedQuery('roles').delete(), - view.$relatedQuery('columns').delete(), - ]); - await View.query().where('id', view.id).delete(); - - return res.status(200).send({ id: view.id }); - }, - }, - - /** - * Creates a new view. - */ - createView: { - validation: [ - check('resource_name').exists().escape().trim(), - check('name').exists().escape().trim(), - check('logic_expression').exists().trim().escape(), - check('roles').isArray({ min: 1 }), - check('roles.*.field_key').exists().escape().trim(), - check('roles.*.comparator').exists(), - check('roles.*.value').exists(), - check('roles.*.index').exists().isNumeric().toInt(), - check('columns').exists().isArray({ min: 1 }), - check('columns.*.key').exists().escape().trim(), - check('columns.*.index').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { - Resource, - View, - ViewColumn, - ViewRole, - } = req.models; - const form = { roles: [], ...req.body }; - const resource = await Resource.query().where('name', form.resource_name).first(); - - if (!resource) { - return res.boom.notFound(null, { - errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }], - }); - } - const errorReasons = []; - const fieldsSlugs = form.roles.map((role) => role.field_key); - - const resourceFields = await resource.$relatedQuery('fields'); - const resourceFieldsKeys = resourceFields.map((f) => f.key); - const resourceFieldsKeysMap = new Map(resourceFields.map((field) => [field.key, field])); - const columnsKeys = form.columns.map((c) => c.key); - - // The difference between the stored resource fields and submit fields keys. - const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); - - if (notFoundFields.length > 0) { - errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields }); - } - // The difference between the stored resource fields and the submit columns keys. - const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); - - if (notFoundColumns.length > 0) { - errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns }); - } - // Validates the view conditional logic expression. - if (!validateViewRoles(form.roles, form.logic_expression)) { - errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); - } - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); - } - - // Save view details. - const view = await View.query().insert({ - name: form.name, - predefined: false, - resource_id: resource.id, - roles_logic_expression: form.logic_expression, - }); - // Save view roles async operations. - const saveViewRolesOpers = []; - - form.roles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - - const saveViewRoleOper = ViewRole.query().insert({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - view_id: view.id, - }); - saveViewRolesOpers.push(saveViewRoleOper); - }); - - form.columns.forEach((column) => { - const fieldModel = resourceFieldsKeysMap.get(column.key); - - const saveViewColumnOper = ViewColumn.query().insert({ - field_id: fieldModel.id, - view_id: view.id, - index: column.index, - }); - saveViewRolesOpers.push(saveViewColumnOper); - }); - await Promise.all(saveViewRolesOpers); - - return res.status(200).send({ id: view.id }); - }, - }, - - /** - * Edit the given custom view metadata. - */ - editView: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - check('name').exists().escape().trim(), - check('logic_expression').exists().trim().escape(), - - check('columns').exists().isArray({ min: 1 }), - - check('columns.*.id').optional().isNumeric().toInt(), - check('columns.*.key').exists().escape().trim(), - check('columns.*.index').exists().isNumeric().toInt(), - - check('roles').isArray(), - check('roles.*.id').optional().isNumeric().toInt(), - check('roles.*.field_key').exists().escape().trim(), - check('roles.*.comparator').exists(), - check('roles.*.value').exists(), - check('roles.*.index').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { view_id: viewId } = req.params; - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { - View, ViewRole, ViewColumn, Resource, - } = req.models; - const view = await View.query().where('id', viewId) - .withGraphFetched('roles.field') - .withGraphFetched('columns') - .first(); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }], - }); - } - const form = { ...req.body }; - const resource = await Resource.query() - .where('id', view.resourceId) - .withGraphFetched('fields') - .withGraphFetched('views') - .first(); - - const errorReasons = []; - const fieldsSlugs = form.roles.map((role) => role.field_key); - const resourceFieldsKeys = resource.fields.map((f) => f.key); - const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field])); - const columnsKeys = form.columns.map((c) => c.key); - - // The difference between the stored resource fields and submit fields keys. - const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); - - // Validate not found resource fields keys. - if (notFoundFields.length > 0) { - errorReasons.push({ - type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields, - }); - } - // The difference between the stored resource fields and the submit columns keys. - const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); - - // Validate not found view columns. - if (notFoundColumns.length > 0) { - errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns }); - } - // Validates the view conditional logic expression. - if (!validateViewRoles(form.roles, form.logic_expression)) { - errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); - } - - const viewRolesIds = view.roles.map((r) => r.id); - const viewColumnsIds = view.columns.map((c) => c.id); - - const formUpdatedRoles = form.roles.filter((r) => r.id); - const formInsertRoles = form.roles.filter((r) => !r.id); - - const formRolesIds = formUpdatedRoles.map((r) => r.id); - - const formUpdatedColumns = form.columns.filter((r) => r.id); - const formInsertedColumns = form.columns.filter((r) => !r.id); - const formColumnsIds = formUpdatedColumns.map((r) => r.id); - - const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds); - const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds); - - const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds); - const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds); - - // Validate the not found view roles ids. - if (notFoundViewRolesIds.length) { - errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds }); - } - // Validate the not found view columns ids. - if (notFoundViewColumnsIds.length) { - errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const asyncOpers = []; - - // Save view details. - await View.query() - .where('id', view.id) - .patch({ - name: form.name, - roles_logic_expression: form.logic_expression, - }); - - // Update view roles. - if (formUpdatedRoles.length > 0) { - formUpdatedRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const updateOper = ViewRole.query() - .where('id', role.id) - .update({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - }); - asyncOpers.push(updateOper); - }); - } - // Insert a new view roles. - if (formInsertRoles.length > 0) { - formInsertRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const insertOper = ViewRole.query() - .insert({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Delete view roles. - if (rolesIdsShouldDeleted.length > 0) { - const deleteOper = ViewRole.query() - .whereIn('id', rolesIdsShouldDeleted) - .delete(); - asyncOpers.push(deleteOper); - } - // Insert a new view columns to the storage. - if (formInsertedColumns.length > 0) { - formInsertedColumns.forEach((column) => { - const fieldModel = resourceFieldsKeysMap.get(column.key); - const insertOper = ViewColumn.query() - .insert({ - field_id: fieldModel.id, - index: column.index, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Update the view columns on the storage. - if (formUpdatedColumns.length > 0) { - formUpdatedColumns.forEach((column) => { - const updateOper = ViewColumn.query() - .where('id', column.id) - .update({ - index: column.index, - }); - asyncOpers.push(updateOper); - }); - } - // Delete the view columns from the storage. - if (columnsIdsShouldDelete.length > 0) { - const deleteOper = ViewColumn.query() - .whereIn('id', columnsIdsShouldDelete) - .delete(); - asyncOpers.push(deleteOper); - } - await Promise.all(asyncOpers); - - return res.status(200).send(); - }, - }, - - /** - * Retrieve resource columns that associated to the given custom view. - */ - getViewResource: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { view_id: viewId } = req.params; - const { View } = req.models; - - const view = await View.query() - .where('id', viewId) - .withGraphFetched('resource.fields') - .first(); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'VIEW.NOT.FOUND', code: 100 }], - }); - } - if (!view.resource) { - return res.boom.badData(null, { - errors: [{ type: 'VIEW.HAS.NOT.ASSOCIATED.RESOURCE', code: 200 }], - }); - } - - const resourceColumns = view.resource.fields - .filter((field) => field.columnable) - .map((field) => ({ - id: field.id, - label: field.labelName, - key: field.key, - })); - - return res.status(200).send({ - resource_slug: view.resource.name, - resource_columns: resourceColumns, - resource_fields: view.resource.fields, - }); - } - }, -}; diff --git a/server/src/api/controllers/Views.ts b/server/src/api/controllers/Views.ts index e82a8ae64..238d01b6b 100644 --- a/server/src/api/controllers/Views.ts +++ b/server/src/api/controllers/Views.ts @@ -1,19 +1,10 @@ import { Inject, Service } from 'typedi'; import { Router, Request, NextFunction, Response } from 'express'; -import { - check, - query, - param, - oneOf, - validationResult, -} from 'express-validator'; +import { check, param } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import { - validateViewRoles, -} from 'lib/ViewRolesBuilder'; import ViewsService from 'services/Views/ViewsService'; -import BaseController from './BaseController'; -import { IViewDTO } from 'interfaces'; +import BaseController from 'api/controllers/BaseController'; +import { IViewDTO, IViewEditDTO } from 'interfaces'; import { ServiceError } from 'exceptions'; @Service() @@ -27,66 +18,96 @@ export default class ViewsController extends BaseController{ router() { const router = Router(); - router.get('/', [ - ...this.viewDTOSchemaValidation, + router.get('/resource/:resource_model', [ + ...this.viewsListSchemaValidation, ], - asyncMiddleware(this.listViews) + this.validationResult, + asyncMiddleware(this.listResourceViews.bind(this)), + this.handlerServiceErrors, ); router.post('/', [ ...this.viewDTOSchemaValidation, ], - asyncMiddleware(this.createView) + this.validationResult, + asyncMiddleware(this.createView.bind(this)), + this.handlerServiceErrors ); - - router.post('/:view_id', [ - ...this.viewDTOSchemaValidation, + router.post('/:id', [ + ...this.viewParamSchemaValidation, + ...this.viewEditDTOSchemaValidation, ], - asyncMiddleware(this.editView) + this.validationResult, + asyncMiddleware(this.editView.bind(this)), + this.handlerServiceErrors, ); - - router.delete('/:view_id', [ + router.delete('/:id', [ ...this.viewParamSchemaValidation ], - asyncMiddleware(this.deleteView)); - - router.get('/:view_id', [ - ...this.viewParamSchemaValidation - ] - asyncMiddleware(this.getView) + this.validationResult, + asyncMiddleware(this.deleteView.bind(this)), + this.handlerServiceErrors, ); - - router.get('/:view_id/resource', [ + router.get('/:id', [ ...this.viewParamSchemaValidation ], - asyncMiddleware(this.getViewResource) + this.validationResult, + asyncMiddleware(this.getView.bind(this)), ); - return router; } + /** + * New view DTO schema validation. + */ get viewDTOSchemaValidation() { return [ - check('resource_name').exists().escape().trim(), + check('resource_model').exists().escape().trim(), check('name').exists().escape().trim(), check('logic_expression').exists().trim().escape(), + check('roles').isArray({ min: 1 }), check('roles.*.field_key').exists().escape().trim(), check('roles.*.comparator').exists(), check('roles.*.value').exists(), check('roles.*.index').exists().isNumeric().toInt(), + check('columns').exists().isArray({ min: 1 }), - check('columns.*.key').exists().escape().trim(), + check('columns.*.field_key').exists().escape().trim(), + check('columns.*.index').exists().isNumeric().toInt(), + ]; + } + + /** + * Edit view DTO schema validation. + */ + get viewEditDTOSchemaValidation() { + return [ + check('name').exists().escape().trim(), + check('logic_expression').exists().trim().escape(), + + check('roles').isArray({ min: 1 }), + check('roles.*.field_key').exists().escape().trim(), + check('roles.*.comparator').exists(), + check('roles.*.value').exists(), + check('roles.*.index').exists().isNumeric().toInt(), + + check('columns').exists().isArray({ min: 1 }), + check('columns.*.field_key').exists().escape().trim(), check('columns.*.index').exists().isNumeric().toInt(), ]; } get viewParamSchemaValidation() { return [ - - ] + param('id').exists().isNumeric().toInt(), + ]; } - + get viewsListSchemaValidation() { + return [ + param('resource_model').exists().trim().escape(), + ] + } /** * List all views that associated with the given resource. @@ -94,12 +115,12 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - listViews(req: Request, res: Response, next: NextFunction) { + async listResourceViews(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const filter = req.query; + const { resource_model: resourceModel } = req.params; try { - const views = this.viewsService.listViews(tenantId, filter); + const views = await this.viewsService.listResourceViews(tenantId, resourceModel); return res.status(200).send({ views }); } catch (error) { next(error); @@ -107,16 +128,17 @@ export default class ViewsController extends BaseController{ } /** - * + * Retrieve view details with assocaited roles and columns. * @param {Request} req * @param {Response} res * @param {NextFunction} next */ - getView(req: Request, res: Response, next: NextFunction) { + async getView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const { id: viewId } = req.params; try { - const view = this.viewsService.getView(tenantId, viewId); + const view = await this.viewsService.getView(tenantId, viewId); return res.status(200).send({ view }); } catch (error) { next(error); @@ -129,13 +151,13 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - createView(req: Request, res: Response, next: NextFunction) { + async createView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const viewDTO: IViewDTO = this.matchedBodyData(req); try { - await this.viewsService.newView(tenantId, viewDTO); - return res.status(200).send({ id: 1 }); + const view = await this.viewsService.newView(tenantId, viewDTO); + return res.status(200).send({ id: view.id }); } catch (error) { next(error); } @@ -147,10 +169,10 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - editView(req: Request, res: Response, next: NextFunction) { + async editView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: viewId } = req.params; - const { body: viewEditDTO } = req; + const viewEditDTO: IViewEditDTO = this.matchedBodyData(req); try { await this.viewsService.editView(tenantId, viewId, viewEditDTO); @@ -166,7 +188,7 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - deleteView(req: Request, res: Response, next: NextFunction) { + async deleteView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: viewId } = req.params; @@ -187,6 +209,16 @@ export default class ViewsController extends BaseController{ */ handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { + if (error.errorType === 'VIEW_NAME_NOT_UNIQUE') { + return res.boom.badRequest(null, { + errors: [{ type: 'VIEW_NAME_NOT_UNIQUE', code: 110 }], + }); + } + if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'RESOURCE_MODEL_NOT_FOUND', code: 150, }], + }); + } if (error.errorType === 'INVALID_LOGIC_EXPRESSION') { return res.boom.badRequest(null, { errors: [{ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }], @@ -212,6 +244,17 @@ export default class ViewsController extends BaseController{ errors: [{ type: 'PREDEFINED_VIEW', code: 200 }], }); } + if (error.errorType === 'RESOURCE_FIELDS_KEYS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', code: 300 }], + }) + } + if (error.errorType === 'RESOURCE_COLUMNS_KEYS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', code: 310 }], + }) + } } + next(error); } }; diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 3cfa75f3e..0cf504623 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -87,16 +87,16 @@ export default () => { dashboard.use('/accounts', Container.get(Accounts).router()); dashboard.use('/account_types', Container.get(AccountTypes).router()); dashboard.use('/manual-journals', Container.get(ManualJournals).router()); - dashboard.use('/views', Views.router()); + dashboard.use('/views', Container.get(Views).router()); dashboard.use('/items', Container.get(Items).router()); dashboard.use('/item_categories', Container.get(ItemCategories).router()); dashboard.use('/expenses', Container.get(Expenses).router()); dashboard.use('/financial_statements', FinancialStatements.router()); - dashboard.use('/sales', Container.get(Sales).router()); dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/vendors', Container.get(Vendors).router()); - dashboard.use('/purchases', Container.get(Purchases).router()); - dashboard.use('/resources', Resources.router()); + // dashboard.use('/sales', Container.get(Sales).router()); + // dashboard.use('/purchases', Container.get(Purchases).router()); + dashboard.use('/resources', Container.get(Resources).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); dashboard.use('/media', Media.router()); diff --git a/server/src/api/middleware/TenantDependencyInjection.ts b/server/src/api/middleware/TenantDependencyInjection.ts index fed2b4219..c8c447bca 100644 --- a/server/src/api/middleware/TenantDependencyInjection.ts +++ b/server/src/api/middleware/TenantDependencyInjection.ts @@ -17,6 +17,8 @@ export default (req: Request, tenant: ITenant) => { const repositories = tenantServices.repositories(tenantId) const cacheInstance = tenantServices.cache(tenantId); + tenantServices.setI18nLocals(tenantId, { __: req.__ }); + req.knex = knexInstance; req.organizationId = organizationId; req.tenant = tenant; diff --git a/server/src/config/index.js b/server/src/config/index.js index 1258c04ce..7ce065268 100644 --- a/server/src/config/index.js +++ b/server/src/config/index.js @@ -24,6 +24,7 @@ export default { db_user: process.env.SYSTEM_DB_USER, db_password: process.env.SYSTEM_DB_PASSWORD, db_name: process.env.SYSTEM_DB_NAME, + charset: process.env.SYSTEM_DB_CHARSET, migrations_dir: process.env.SYSTEM_MIGRATIONS_DIR, seeds_dir: process.env.SYSTEM_SEEDS_DIR, }, diff --git a/server/src/data/ResourceFieldsKeys.js b/server/src/data/ResourceFieldsKeys.js index 462142dfd..8504d3fdf 100644 --- a/server/src/data/ResourceFieldsKeys.js +++ b/server/src/data/ResourceFieldsKeys.js @@ -2,7 +2,7 @@ export default { // Expenses. - 'expenses': { + expense: { payment_date: { column: 'payment_date', }, @@ -10,9 +10,12 @@ export default { column: 'payment_account_id', relation: 'accounts.id', }, - total_amount: { + amount: { column: 'total_amount', }, + currency_code: { + column: 'currency_code', + }, reference_no: { column: 'reference_no' }, @@ -30,7 +33,7 @@ export default { }, // Accounts - 'accounts': { + Account: { name: { column: 'name', }, @@ -72,23 +75,106 @@ export default { }, // Items - 'items': { - 'type': { + item: { + type: { column: 'type', }, - 'name': { + name: { column: 'name', }, + sellable: { + column: 'sellable', + }, + purchasable: { + column: 'purchasable', + }, + sell_price: { + column: 'sell_price' + }, + cost_price: { + column: 'cost_price', + }, + currency_code: { + column: 'currency_code', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + sell_description: { + column: 'sell_description', + }, + purchase_description: { + column: 'purchase_description', + }, + quantity_on_hand: { + column: 'quantity_on_hand', + }, + note: { + column: 'note', + }, + category: { + column: 'category_id', + relation: 'categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + created_at: { + column: 'created_at', + } + }, + + // Item category. + item_category: { + name: { + column: 'name', + }, + description: { + column: 'description', + }, + parent_category_id: { + column: 'parent_category_id', + relation: 'items_categories.id', + relationColumn: 'items_categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + cost_method: { + column: 'cost_method', + }, }, // Manual Journals - manual_journals: { + manual_journal: { date: { column: 'date', }, - created_at: { - column: 'created_at', - }, journal_number: { column: 'journal_number', }, @@ -112,5 +198,8 @@ export default { journal_type: { column: 'journal_type', }, + created_at: { + column: 'created_at', + }, } }; diff --git a/server/src/database/migrations/20190822214302_create_account_types_table.js b/server/src/database/migrations/20190822214302_create_account_types_table.js index 3830c0ded..4515e3338 100644 --- a/server/src/database/migrations/20190822214302_create_account_types_table.js +++ b/server/src/database/migrations/20190822214302_create_account_types_table.js @@ -2,7 +2,6 @@ exports.up = (knex) => { return knex.schema.createTable('account_types', (table) => { table.increments(); - table.string('name'); table.string('key').index(); table.string('normal').index(); table.string('root_type').index(); diff --git a/server/src/database/seeds/core/20190423085241_seed_accounts_types.js b/server/src/database/seeds/core/20190423085241_seed_accounts_types.js index fd03d2f7d..ffe6b9b77 100644 --- a/server/src/database/seeds/core/20190423085241_seed_accounts_types.js +++ b/server/src/database/seeds/core/20190423085241_seed_accounts_types.js @@ -1,8 +1,14 @@ +import Container from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService' +import I18nMiddleware from 'api/middleware/I18nMiddleware'; + exports.up = function (knex) { + const tenancyService = Container.get(TenancyService); + const i18n = tenancyService.i18n(knex.userParams.tenantId); + return knex('account_types').insert([ { id: 1, - name: 'Fixed Asset', key: 'fixed_asset', normal: 'debit', root_type: 'asset', @@ -12,7 +18,6 @@ exports.up = function (knex) { }, { id: 2, - name: 'Current Asset', key: 'current_asset', normal: 'debit', root_type: 'asset', @@ -22,7 +27,6 @@ exports.up = function (knex) { }, { id: 14, - name: 'Other Asset', key: 'other_asset', normal: 'debit', root_type: 'asset', @@ -32,7 +36,6 @@ exports.up = function (knex) { }, { id: 3, - name: 'Long Term Liability', key: 'long_term_liability', normal: 'credit', root_type: 'liability', @@ -42,7 +45,6 @@ exports.up = function (knex) { }, { id: 4, - name: 'Current Liability', key: 'current_liability', normal: 'credit', root_type: 'liability', @@ -52,7 +54,6 @@ exports.up = function (knex) { }, { id: 13, - name: 'Other Liability', key: 'other_liability', normal: 'credit', root_type: 'liability', @@ -62,7 +63,6 @@ exports.up = function (knex) { }, { id: 5, - name: 'Equity', key: 'equity', normal: 'credit', root_type: 'equity', @@ -72,7 +72,6 @@ exports.up = function (knex) { }, { id: 6, - name: 'Expense', key: 'expense', normal: 'debit', root_type: 'expense', @@ -82,7 +81,6 @@ exports.up = function (knex) { }, { id: 10, - name: 'Other Expense', key: 'other_expense', normal: 'debit', root_type: 'expense', @@ -91,7 +89,6 @@ exports.up = function (knex) { }, { id: 7, - name: 'Income', key: 'income', normal: 'credit', root_type: 'income', @@ -101,7 +98,6 @@ exports.up = function (knex) { }, { id: 11, - name: 'Other Income', key: 'other_income', normal: 'credit', root_type: 'income', @@ -111,7 +107,6 @@ exports.up = function (knex) { }, { id: 12, - name: 'Cost of Goods Sold (COGS)', key: 'cost_of_goods_sold', normal: 'debit', root_type: 'expenses', @@ -121,7 +116,6 @@ exports.up = function (knex) { }, { id: 8, - name: 'Accounts Receivable (A/R)', key: 'accounts_receivable', normal: 'debit', root_type: 'asset', @@ -131,7 +125,6 @@ exports.up = function (knex) { }, { id: 9, - name: 'Accounts Payable (A/P)', key: 'accounts_payable', normal: 'credit', root_type: 'liability', diff --git a/server/src/database/seeds/core/20190423085240_seed_accounts.js b/server/src/database/seeds/core/20190423085242_seed_accounts.js similarity index 85% rename from server/src/database/seeds/core/20190423085240_seed_accounts.js rename to server/src/database/seeds/core/20190423085242_seed_accounts.js index cce8e6d27..58c29f3cc 100644 --- a/server/src/database/seeds/core/20190423085240_seed_accounts.js +++ b/server/src/database/seeds/core/20190423085242_seed_accounts.js @@ -1,16 +1,18 @@ -import TenancyService from 'services/Tenancy/TenancyService' import Container from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService' exports.up = function (knex) { const tenancyService = Container.get(TenancyService); const i18n = tenancyService.i18n(knex.userParams.tenantId); + console.log(i18n); + return knex('accounts').then(() => { // Inserts seed entries return knex('accounts').insert([ { id: 1, - name: 'Petty Cash', + name: i18n.__('Petty Cash'), slug: 'petty-cash', account_type_id: 2, parent_account_id: null, @@ -22,7 +24,7 @@ exports.up = function (knex) { }, { id: 2, - name: 'Bank', + name: i18n.__('Bank'), slug: 'bank', account_type_id: 2, parent_account_id: null, @@ -34,7 +36,7 @@ exports.up = function (knex) { }, { id: 3, - name: 'Other Income', + name: i18n.__('Other Income'), slug: 'other-income', account_type_id: 7, parent_account_id: null, @@ -46,7 +48,7 @@ exports.up = function (knex) { }, { id: 4, - name: 'Interest Income', + name: i18n.__('Interest Income'), slug: 'interest-income', account_type_id: 7, parent_account_id: null, @@ -58,7 +60,7 @@ exports.up = function (knex) { }, { id: 5, - name: 'Opening Balance', + name: i18n.__('Opening Balance'), slug: 'opening-balance', account_type_id: 5, parent_account_id: null, @@ -70,7 +72,7 @@ exports.up = function (knex) { }, { id: 6, - name: 'Depreciation Expense', + name: i18n.__('Depreciation Expense'), slug: 'depreciation-expense', account_type_id: 6, parent_account_id: null, @@ -82,7 +84,7 @@ exports.up = function (knex) { }, { id: 7, - name: 'Interest Expense', + name: i18n.__('Interest Expense'), slug: 'interest-expense', account_type_id: 6, parent_account_id: null, @@ -94,7 +96,7 @@ exports.up = function (knex) { }, { id: 8, - name: 'Payroll Expenses', + name: i18n.__('Payroll Expenses'), slug: 'payroll-expenses', account_type_id: 6, parent_account_id: null, @@ -106,7 +108,7 @@ exports.up = function (knex) { }, { id: 9, - name: 'Other Expenses', + name: i18n.__('Other Expenses'), slug: 'other-expenses', account_type_id: 6, parent_account_id: null, @@ -118,7 +120,7 @@ exports.up = function (knex) { }, { id: 10, - name: 'Accounts Receivable', + name: i18n.__('Accounts Receivable'), slug: 'accounts-receivable', account_type_id: 8, parent_account_id: null, @@ -130,7 +132,7 @@ exports.up = function (knex) { }, { id: 11, - name: 'Accounts Payable', + name: i18n.__('Accounts Payable'), slug: 'accounts-payable', account_type_id: 9, parent_account_id: null, @@ -142,7 +144,7 @@ exports.up = function (knex) { }, { id: 12, - name: 'Cost of Goods Sold (COGS)', + name: i18n.__('Cost of Goods Sold (COGS)'), slug: 'cost-of-goods-sold', account_type_id: 12, predefined: 1, @@ -153,7 +155,7 @@ exports.up = function (knex) { }, { id: 13, - name: 'Inventory Asset', + name: i18n.__('Inventory Asset'), slug: 'inventory-asset', account_type_id: 14, predefined: 1, @@ -164,7 +166,7 @@ exports.up = function (knex) { }, { id: 14, - name: 'Sales of Product Income', + name: i18n.__('Sales of Product Income'), slug: 'sales-of-product-income', account_type_id: 7, predefined: 1, diff --git a/server/src/database/seeds/core/20200810121807_seed_views.js b/server/src/database/seeds/core/20200810121807_seed_views.js index 87d383888..5e945978c 100644 --- a/server/src/database/seeds/core/20200810121807_seed_views.js +++ b/server/src/database/seeds/core/20200810121807_seed_views.js @@ -1,17 +1,20 @@ exports.up = (knex) => { + const tenancyService = Container.get(TenancyService); + const i18n = tenancyService.i18n(knex.userParams.tenantId); + // Deletes ALL existing entries return knex('views').del() .then(() => { // Inserts seed entries return knex('views').insert([ // Accounts - { id: 15, name: 'Inactive', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 1, name: 'Assets', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 3, name: 'Equity', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 4, name: 'Income', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 5, name: 'Expenses', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 15, name: i18n.__('Inactive'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 1, name: i18n.__('Assets'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 2, name: i18n.__('Liabilities'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 3, name: i18n.__('Equity'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 4, name: i18n.__('Income'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 5, name: i18n.__('Expenses'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, // Items // { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true }, diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts index e0183cd8e..c2be84a73 100644 --- a/server/src/interfaces/Contact.ts +++ b/server/src/interfaces/Contact.ts @@ -178,8 +178,9 @@ export interface IVendorsFilter extends IDynamicListFilter { pageSize?: number, }; -export interface ICustomerFilter extends IDynamicListFilter { +export interface ICustomersFilter extends IDynamicListFilter { stringifiedFilterRoles?: string, page?: number, pageSize?: number, -}; \ No newline at end of file +}; + diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts index 8ed67b362..04e998ea5 100644 --- a/server/src/interfaces/Expenses.ts +++ b/server/src/interfaces/Expenses.ts @@ -1,5 +1,16 @@ import { ISystemUser } from "./User"; +export interface IPaginationMeta { + total: number, + page: number, + pageSize: number, +}; + +export interface IExpensesFilter{ + page: number, + pageSize: number, +}; + export interface IExpense { id: number, totalAmount: number, @@ -53,4 +64,6 @@ export interface IExpensesService { deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise; publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise; + + getExpensesList(tenantId: number, expensesFilter: IExpensesFilter): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>; } \ No newline at end of file diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index d0824056c..2887481ec 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -70,6 +70,6 @@ export interface IItemsService { export interface IItemsFilter extends IDynamicListFilter { stringifiedFilterRoles?: string, - page?: number, - pageSize?: number, + page: number, + pageSize: number, }; \ No newline at end of file diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts index cc40c3611..2a1e0fc05 100644 --- a/server/src/interfaces/ManualJournal.ts +++ b/server/src/interfaces/ManualJournal.ts @@ -37,8 +37,8 @@ export interface IManualJournalDTO { export interface IManualJournalsFilter extends IDynamicListFilterDTO { stringifiedFilterRoles?: string, - page?: number, - pageSize?: number, + page: number, + pageSize: number, } export interface IManuaLJournalsService { @@ -48,5 +48,6 @@ export interface IManuaLJournalsService { deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise; publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise; publishManualJournal(tenantId: number, manualJournalId: number): Promise; - getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise; + + getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }>; } \ No newline at end of file diff --git a/server/src/interfaces/Model.ts b/server/src/interfaces/Model.ts new file mode 100644 index 000000000..91f20c15f --- /dev/null +++ b/server/src/interfaces/Model.ts @@ -0,0 +1,17 @@ + + +export interface IModel { + name: string, + tableName: string, + fields: { [key: string]: any, }, +}; + +export interface IFilterMeta { + sortOrder: string, + sortBy: string, +}; + +export interface IPaginationMeta { + pageSize: number, + page: number, +}; \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index 1628c9da0..ac8850243 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -15,4 +15,9 @@ export interface ISaleInvoiceOTD { invoiceMessage: string, termsConditions: string, entries: any[], -} \ No newline at end of file +} + +export interface ISalesInvoicesFilter{ + page: number, + pageSize: number, +}; \ No newline at end of file diff --git a/server/src/interfaces/SaleReceipt.ts b/server/src/interfaces/SaleReceipt.ts new file mode 100644 index 000000000..1866a6cfd --- /dev/null +++ b/server/src/interfaces/SaleReceipt.ts @@ -0,0 +1,37 @@ +import { ISalesInvoicesFilter } from "./SaleInvoice"; + + +export interface ISaleReceipt { + id?: number, + customerId: number, + depositAccountId: number, + receiptDate: Date, + sendToEmail: string, + referenceNo: string, + receiptMessage: string, + statement: string, + entries: any[], +}; + +export interface ISalesReceiptsFilter { + +}; + +export interface ISaleReceiptDTO { + customerId: number, + depositAccountId: number, + receiptDate: Date, + sendToEmail: string, + referenceNo: string, + receiptMessage: string, + statement: string, + entries: any[], +}; + +export interface ISalesReceiptService { + createSaleReceipt(tenantId: number, saleReceiptDTO: ISaleReceiptDTO): Promise; + editSaleReceipt(tenantId: number, saleReceiptId: number): Promise; + + deleteSaleReceipt(tenantId: number, saleReceiptId: number): Promise; + salesReceiptsList(tennatid: number, salesReceiptsFilter: ISalesReceiptsFilter): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>; +}; \ No newline at end of file diff --git a/server/src/interfaces/View.ts b/server/src/interfaces/View.ts index 5e7ccbcf3..06197cae8 100644 --- a/server/src/interfaces/View.ts +++ b/server/src/interfaces/View.ts @@ -5,7 +5,10 @@ export interface IView { predefined: boolean, resourceModel: string, favourite: boolean, - rolesLogicRxpression: string, + rolesLogicExpression: string, + + roles: IViewRole[], + columns: IViewHasColumn[], }; export interface IViewRole { @@ -42,6 +45,8 @@ export interface IViewColumnDTO { export interface IViewDTO { name: string, logicExpression: string, + resourceModel: string, + roles: IViewRoleDTO[], columns: IViewColumnDTO[], }; @@ -49,12 +54,13 @@ export interface IViewDTO { export interface IViewEditDTO { name: string, logicExpression: string, + roles: IViewRoleDTO[], columns: IViewColumnDTO[], }; export interface IViewsService { - listViews(tenantId: number, resourceModel: string): Promise; + listResourceViews(tenantId: number, resourceModel: string): Promise; newView(tenantId: number, viewDTO: IViewDTO): Promise; editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise; deleteView(tenantId: number, viewId: number): Promise; diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index de5cc16ba..0dd3c1c20 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -1,3 +1,5 @@ + +export * from './Model'; export * from './InventoryTransaction'; export * from './BillPayment'; export * from './InventoryCostMethod'; diff --git a/server/src/lib/DynamicFilter/DynamicFilter.js b/server/src/lib/DynamicFilter/DynamicFilter.ts similarity index 54% rename from server/src/lib/DynamicFilter/DynamicFilter.js rename to server/src/lib/DynamicFilter/DynamicFilter.ts index 62000f11f..cb795ed88 100644 --- a/server/src/lib/DynamicFilter/DynamicFilter.js +++ b/server/src/lib/DynamicFilter/DynamicFilter.ts @@ -1,15 +1,20 @@ -import { uniqBy } from 'lodash'; +import { forEach, uniqBy } from 'lodash'; import { buildFilterRolesJoins, } from 'lib/ViewRolesBuilder'; +import { IModel } from 'interfaces'; export default class DynamicFilter { + model: IModel; + tableName: string; + /** * Constructor. * @param {String} tableName - */ - constructor(tableName) { - this.tableName = tableName; + constructor(model) { + this.model = model; + this.tableName = model.tableName; this.filters = []; } @@ -18,7 +23,7 @@ export default class DynamicFilter { * @param {*} filterRole - */ setFilter(filterRole) { - filterRole.setTableName(this.tableName); + filterRole.setModel(this.model); this.filters.push(filterRole); } @@ -38,7 +43,23 @@ export default class DynamicFilter { buildersCallbacks.forEach((builderCallback) => { builderCallback(builder); }); - buildFilterRolesJoins(this.tableName, uniqBy(tableColumns, 'columnKey'))(builder); + buildFilterRolesJoins(this.model, uniqBy(tableColumns, 'columnKey'))(builder); }; } + + /** + * Retrieve response metadata from all filters adapters. + */ + getResponseMeta() { + const responseMeta = {}; + + this.filters.forEach((filter) => { + const { responseMeta: filterMeta } = filter; + + forEach(filterMeta, (value, key) => { + responseMeta[key] = value; + }); + }); + return responseMeta; + } } \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts index 4a8366a8d..0a64d5a43 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts @@ -14,14 +14,14 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { constructor(filterRoles: IFilterRole[]) { super(); this.filterRoles = filterRoles; + this.setResponseMeta(); } private buildLogicExpression(): string { let expression = ''; this.filterRoles.forEach((role, index) => { expression += (index === 0) ? - `${role.index} ` : - `${role.condition} ${role.index} `; + `${role.index} ` : `${role.condition} ${role.index} `; }); return expression.trim(); } @@ -32,7 +32,16 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { buildQuery() { return (builder) => { const logicExpression = this.buildLogicExpression(); - buildFilterQuery(this.tableName, this.filterRoles, logicExpression)(builder); + buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder); + }; + } + + /** + * Sets response meta. + */ + setResponseMeta() { + this.responseMeta = { + filterRoles: this.filterRoles }; } } \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts index 5ccfe111b..006008ddd 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts @@ -1,10 +1,13 @@ -import { IFilterRole, IDynamicFilter } from "interfaces"; +import { IFilterRole, IDynamicFilter, IModel } from "interfaces"; export default class DynamicFilterAbstructor implements IDynamicFilter { filterRoles: IFilterRole[] = []; tableName: string; + model: IModel; + responseMeta: { [key: string]: any } = {}; - setTableName(tableName) { - this.tableName = tableName; + setModel(model: IModel) { + this.model = model; + this.tableName = model.tableName; } } \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts index 32b35f555..3d413aa5e 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts @@ -16,10 +16,11 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { fieldKey: sortByFieldKey, order: sortDirection, }; + this.setResponseMeta(); } validate() { - validateFieldKeyExistance(this.tableName, this.sortRole.fieldKey); + validateFieldKeyExistance(this.model, this.sortRole.fieldKey); } /** @@ -27,7 +28,7 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { */ buildQuery() { return (builder) => { - const fieldRelation = getRoleFieldColumn(this.tableName, this.sortRole.fieldKey); + const fieldRelation = getRoleFieldColumn(this.model, this.sortRole.fieldKey); const comparatorColumn = fieldRelation.relationColumn || `${this.tableName}.${fieldRelation.column}`; @@ -37,4 +38,14 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { } }; } + + /** + * Sets response meta. + */ + setResponseMeta() { + this.responseMeta = { + sortOrder: this.sortRole.fieldKey, + sortBy: this.sortRole.order, + }; + } } diff --git a/server/src/lib/DynamicFilter/DynamicFilterViews.ts b/server/src/lib/DynamicFilter/DynamicFilterViews.ts index b02a86aa1..d8f2bfc84 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterViews.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterViews.ts @@ -1,25 +1,29 @@ -import { IFilterRole } from 'interfaces'; +import { omit } from 'lodash'; +import { IView, IViewRole } from 'interfaces'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import { - validateViewRoles, buildFilterQuery, } from 'lib/ViewRolesBuilder'; export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { + viewId: number; logicExpression: string; + filterRoles: IViewRole[]; /** * Constructor method. - * @param {*} filterRoles - Filter roles. - * @param {*} logicExpression - Logic expression. + * @param {IView} view - */ - constructor(filterRoles: IFilterRole[], logicExpression: string) { + constructor(view: IView) { super(); - this.filterRoles = filterRoles; - this.logicExpression = logicExpression + this.viewId = view.id; + this.filterRoles = view.roles; + this.logicExpression = view.rolesLogicExpression .replace('AND', '&&') .replace('OR', '||'); + + this.setResponseMeta(); } /** @@ -28,20 +32,27 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { buildLogicExpression() { return this.logicExpression; } - - /** - * Validates filter roles. - */ - validate() { - return validateViewRoles(this.filterRoles, this.logicExpression); - } - + /** * Builds database query of view roles. */ buildQuery() { return (builder) => { - buildFilterQuery(this.tableName, this.filterRoles, this.logicExpression)(builder); + buildFilterQuery(this.model, this.filterRoles, this.logicExpression)(builder); + }; + } + + /** + * Sets response meta. + */ + setResponseMeta() { + this.responseMeta = { + view: { + logicExpression: this.logicExpression, + filterRoles: this.filterRoles + .map((filterRole) => ({ ...omit(filterRole, ['id', 'viewId']) })), + customViewId: this.viewId, + } }; } } \ No newline at end of file diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts index 39af35931..f8eeff373 100644 --- a/server/src/lib/ViewRolesBuilder/index.ts +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -1,10 +1,9 @@ -import { difference, filter } from 'lodash'; +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 resourceFieldsKeys from 'data/ResourceFieldsKeys'; -import { IFilterRole } from 'interfaces'; +import { IFilterRole, IModel } from 'interfaces'; const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { switch (role.comparator) { @@ -93,7 +92,7 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { if (hasTimeFormat) { const targetDateTime = moment(role.value).format(dateFormat); builder.where(comparatorColumn, '=', targetDateTime); - } else { + } else { const startDate = moment(role.value).startOf('day'); const endDate = moment(role.value).endOf('day'); @@ -109,19 +108,19 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { * @param {String} tableName - Table name of target column. * @param {String} fieldKey - Target column key that stored in resource field. */ -export function getRoleFieldColumn(tableName: string, fieldKey: string) { - const tableFields = resourceFieldsKeys[tableName]; +export function getRoleFieldColumn(model: IModel, fieldKey: string) { + const tableFields = model.fields; return (tableFields[fieldKey]) ? tableFields[fieldKey] : null; } /** * Builds roles queries. - * @param {String} tableName - + * @param {IModel} model - * @param {Object} role - */ -export function buildRoleQuery(tableName: string, role: IFilterRole) { - const fieldRelation = getRoleFieldColumn(tableName, role.fieldKey); - const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`; +export function buildRoleQuery(model: IModel, role: IFilterRole) { + const fieldRelation = getRoleFieldColumn(model, role.fieldKey); + const comparatorColumn = fieldRelation.relationColumn || `${model.tableName}.${fieldRelation.column}`; switch (fieldRelation.columnType) { case 'number': @@ -150,26 +149,26 @@ export const getTableFromRelationColumn = (column: string) => { * @param {String} tableName - * @param {Array} roles - */ -export function buildFilterRolesJoins(tableName: string, roles: IFilterRole[]) { +export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) { return (builder) => { roles.forEach((role) => { - const fieldColumn = getRoleFieldColumn(tableName, role.fieldKey); + const fieldColumn = getRoleFieldColumn(model, role.fieldKey); if (fieldColumn.relation) { const joinTable = getTableFromRelationColumn(fieldColumn.relation); - builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); + builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); } }); }; } -export function buildSortColumnJoin(tableName: string, sortColumnKey: string) { +export function buildSortColumnJoin(model: IModel, sortColumnKey: string) { return (builder) => { - const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey); + const fieldColumn = getRoleFieldColumn(model, sortColumnKey); if (fieldColumn.relation) { const joinTable = getTableFromRelationColumn(fieldColumn.relation); - builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); + builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); } }; } @@ -180,11 +179,11 @@ export function buildSortColumnJoin(tableName: string, sortColumnKey: string) { * @param {Array} roles - * @return {Function} */ -export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], logicExpression: string = '') { +export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logicExpression: string = '') { const rolesIndexSet = {}; roles.forEach((role) => { - rolesIndexSet[role.index] = buildRoleQuery(tableName, role); + rolesIndexSet[role.index] = buildRoleQuery(model, role); }); // Lexer for logic expression. const lexer = new Lexer(logicExpression); @@ -204,9 +203,9 @@ export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], l * @param {Array} roles - * @param {String} logicExpression - */ -export const buildFilterQuery = (tableName: string, roles, logicExpression: string) => { +export const buildFilterQuery = (model: IModel, roles: IFilterRole[], logicExpression: string) => { return (builder) => { - buildFilterRolesQuery(tableName, roles, logicExpression)(builder); + buildFilterRolesQuery(model, roles, logicExpression)(builder); }; }; @@ -240,35 +239,33 @@ export function mapFilterRolesToDynamicFilter(roles) { * @param {String} columnKey - * @param {String} sortDirection - */ -export function buildSortColumnQuery(tableName: string, columnKey: string, sortDirection: string) { - const fieldRelation = getRoleFieldColumn(tableName, columnKey); - const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`; +export function buildSortColumnQuery(model: IModel, columnKey: string, sortDirection: string) { + const fieldRelation = getRoleFieldColumn(model, columnKey); + const sortColumn = fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`; return (builder) => { builder.orderBy(sortColumn, sortDirection); - buildSortColumnJoin(tableName, columnKey)(builder); + buildSortColumnJoin(model, columnKey)(builder); }; } export function validateFilterLogicExpression(logicExpression: string, indexes) { const logicExpIndexes = logicExpression.match(/\d+/g) || []; - const diff = !difference(logicExpIndexes.map(Number), indexes).length; + const diff = difference(logicExpIndexes.map(Number), indexes); + return (diff.length > 0) ? false : true; } export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) { return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); } -export function validateFieldKeyExistance(tableName: string, fieldKey: string) { - if (!resourceFieldsKeys?.[tableName]?.[fieldKey]) - return fieldKey; - else - return false; +export function validateFieldKeyExistance(model: any, fieldKey: string) { + return model?.fields?.[fieldKey] || false; } -export function validateFilterRolesFieldsExistance(tableName, filterRoles: IFilterRole[]) { +export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRole[]) { return filterRoles.filter((filterRole: IFilterRole) => { - return validateFieldKeyExistance(tableName, filterRole.fieldKey); + return !validateFieldKeyExistance(model, filterRole.fieldKey); }); } \ No newline at end of file diff --git a/server/src/loaders/i18n.ts b/server/src/loaders/i18n.ts index b87823dd2..1c27f8238 100644 --- a/server/src/loaders/i18n.ts +++ b/server/src/loaders/i18n.ts @@ -4,6 +4,6 @@ import path from 'path'; export default () => i18n.configure({ locales: ['en', 'ar'], register: global, - directory: path.join(global.__root, 'src/locales'), + directory: path.join(global.__root, 'locales'), updateFiles: false }) \ No newline at end of file diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 48c4c5fdc..13a8df5e2 100644 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -1,4 +1,33 @@ { "Empty": "", - "Hello": "Hello" + "Hello": "Hello", + "Petty Cash": "Petty Cash 2", + "Bank": "Bank", + "Other Income": "Other Income", + "Interest Income": "Interest Income", + "Opening Balance": "Opening Balance", + "Depreciation Expense": "Depreciation Expense", + "Interest Expense": "Interest Expense", + "Sales of Product Income": "Sales of Product Income", + "Inventory Asset": "Inventory Asset", + "Cost of Goods Sold (COGS)": "Cost of Goods Sold (COGS)", + "Accounts Payable": "Accounts Payable", + "Other Expenses": "Other Expenses", + "Payroll Expenses": "Payroll Expenses", + "Fixed Asset": "Fixed Asset", + "Current Asset": "Current Asset", + "Other Asset": "Other Asset", + "Long Term Liability": "Long Term Liability", + "Current Liability": "Current Liability", + "Other Liability": "Other Liability", + "Equity": "Equity", + "Expense": "Expense", + "Other Expense": "Other Expense", + "Income": "Income", + "Accounts Receivable (A/R)": "Accounts Receivable (A/R)", + "Accounts Payable (A/P)": "Accounts Payable (A/P)", + "Inactive": "Inactive", + "Assets": "Assets", + "Liabilities": "Liabilities", + "Expenses": "Expenses", } \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index a70b0623a..6fa7625ac 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -8,6 +8,7 @@ import { } from 'lib/ViewRolesBuilder'; import { flatToNestedArray } from 'utils'; import DependencyGraph from 'lib/DependencyGraph'; +import TenantManagerSubscriber from 'subscribers/tenantManager'; export default class Account extends TenantModel { /** @@ -24,6 +25,13 @@ export default class Account extends TenantModel { return ['createdAt', 'updatedAt']; } + /** + * + */ + static get resourceable() { + return true; + } + /** * Model modifiers. */ @@ -106,4 +114,55 @@ export default class Account extends TenantModel { accounts, { itemId: 'id', parentItemId: 'parentAccountId' } ); } + + /** + * Model defined fields. + */ + static get fields() { + return { + name: { + label: 'Name', + column: 'name', + }, + type: { + label: 'Account type', + column: 'account_type_id', + relation: 'account_types.id', + relationColumn: 'account_types.key', + }, + description: { + label: 'Description', + column: 'description', + }, + code: { + label: 'Account code', + column: 'code', + }, + root_type: { + label: 'Type', + column: 'account_type_id', + relation: 'account_types.id', + relationColumn: 'account_types.root_type', + }, + created_at: { + column: 'created_at', + columnType: 'date', + }, + active: { + column: 'active', + }, + balance: { + column: 'amount', + columnType: 'number' + }, + currency: { + column: 'currency_code', + }, + normal: { + column: 'account_type_id', + relation: 'account_types.id', + relationColumn: 'account_types.normal' + }, + }; + } } diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index c422a6065..5ad8baa24 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -59,10 +59,6 @@ export default class Expense extends TenantModel { viewRolesBuilder(query, conditionals, expression) { viewRolesBuilder(conditionals, expression)(query); }, - - orderBy(query) { - - } }; } @@ -72,7 +68,6 @@ export default class Expense extends TenantModel { static get relationMappings() { const Account = require('models/Account'); const ExpenseCategory = require('models/ExpenseCategory'); - const SystemUser = require('system/models/SystemUser'); return { paymentAccount: { @@ -91,14 +86,44 @@ export default class Expense extends TenantModel { to: 'expense_transaction_categories.expenseId', }, }, + }; + } + + /** + * Model defined fields. + */ + static get fields() { + return { + payment_date: { + column: 'payment_date', + }, + payment_account: { + column: 'payment_account_id', + relation: 'accounts.id', + }, + amount: { + column: 'total_amount', + }, + currency_code: { + column: 'currency_code', + }, + reference_no: { + column: 'reference_no' + }, + description: { + column: 'description', + }, + published: { + column: 'published', + }, user: { - relation: Model.BelongsToOneRelation, - modelClass: SystemUser.default, - join: { - from: 'expenses_transactions.userId', - to: 'users.id', - } - } + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + created_at: { + column: 'created_at', + }, }; } } diff --git a/server/src/models/Item.js b/server/src/models/Item.js index ea500c8cc..739342c66 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -95,4 +95,67 @@ export default class Item extends TenantModel { }, }; } + + + static get fields() { + return { + type: { + column: 'type', + }, + name: { + column: 'name', + }, + sellable: { + column: 'sellable', + }, + purchasable: { + column: 'purchasable', + }, + sell_price: { + column: 'sell_price' + }, + cost_price: { + column: 'cost_price', + }, + currency_code: { + column: 'currency_code', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + sell_description: { + column: 'sell_description', + }, + purchase_description: { + column: 'purchase_description', + }, + quantity_on_hand: { + column: 'quantity_on_hand', + }, + note: { + column: 'note', + }, + category: { + column: 'category_id', + relation: 'categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + created_at: { + column: 'created_at', + } + }; + } } diff --git a/server/src/models/ItemCategory.js b/server/src/models/ItemCategory.js index 26e7f2684..48b763efa 100644 --- a/server/src/models/ItemCategory.js +++ b/server/src/models/ItemCategory.js @@ -10,6 +10,10 @@ export default class ItemCategory extends TenantModel { return 'items_categories'; } + static get resourceable() { + return true; + } + /** * Timestamps columns. */ @@ -37,4 +41,43 @@ export default class ItemCategory extends TenantModel { }, }; } + + static get fields() { + return { + name: { + column: 'name', + }, + description: { + column: 'description', + }, + parent_category_id: { + column: 'parent_category_id', + relation: 'items_categories.id', + relationColumn: 'items_categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + cost_method: { + column: 'cost_method', + }, + created_at: { + column: 'created_at', + }, + }; + } } diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js index 71c285825..b22fe610a 100644 --- a/server/src/models/ManualJournal.js +++ b/server/src/models/ManualJournal.js @@ -50,4 +50,41 @@ export default class ManualJournal extends TenantModel { } }; } + + /** + * Model defined fields. + */ + static get fields() { + return { + date: { + column: 'date', + }, + journal_number: { + column: 'journal_number', + }, + reference: { + column: 'reference', + }, + status: { + column: 'status', + }, + amount: { + column: 'amount', + }, + description: { + column: 'description', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + journal_type: { + column: 'journal_type', + }, + created_at: { + column: 'created_at', + }, + }; + } } diff --git a/server/src/repositories/ExpenseRepository.ts b/server/src/repositories/ExpenseRepository.ts index a7502961f..4c76e418b 100644 --- a/server/src/repositories/ExpenseRepository.ts +++ b/server/src/repositories/ExpenseRepository.ts @@ -34,11 +34,12 @@ export default class ExpenseRepository extends TenantRepository { * Inserts a new expense object. * @param {IExpense} expense - */ - async create(expense: IExpense): Promise { + async create(expenseInput: IExpense): Promise { const { Expense } = this.models; - await Expense.query().insert({ ...expense }); - + const expense = await Expense.query().insert({ ...expenseInput }); this.flushCache(); + + return expense; } /** diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts index ab4b0b8d9..0d124d24b 100644 --- a/server/src/repositories/ViewRepository.ts +++ b/server/src/repositories/ViewRepository.ts @@ -1,3 +1,4 @@ +import { IView } from 'interfaces'; import { View } from 'models'; import TenantRepository from 'repositories/TenantRepository'; @@ -17,7 +18,6 @@ export default class ViewRepository extends TenantRepository { this.models = this.tenancy.models(tenantId); this.cache = this.tenancy.cache(tenantId); - this.repositories = this.tenancy.cache(tenantId); } /** @@ -27,17 +27,41 @@ export default class ViewRepository extends TenantRepository { getById(id: number) { const { View } = this.models; return this.cache.get(`customView.id.${id}`, () => { - return View.query().findById(id); + return View.query().findById(id) + .withGraphFetched('columns') + .withGraphFetched('roles'); }); } /** * Retrieve all views of the given resource id. */ - allByResource() { - const resourceId = 1; - return this.cache.get(`customView.resource.id.${resourceId}`, () => { - return View.query().where('resource_id', resourceId); + allByResource(resourceModel: string) { + const { View } = this.models; + return this.cache.get(`customView.resourceModel.${resourceModel}`, () => { + return View.query().where('resource_model', resourceModel) + .withGraphFetched('columns') + .withGraphFetched('roles'); }); } + + /** + * Inserts a new view to the storage. + * @param {IView} view + */ + async insert(view: IView): Promise { + const insertedView = await View.query().insertGraph({ ...view }); + this.flushCache(); + + return insertedView; + } + + + + /** + * Flushes repository cache. + */ + flushCache() { + this.cache.delStartWith('customView'); + } } \ No newline at end of file diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index e758662d7..4571218ba 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -3,7 +3,7 @@ import { difference } from 'lodash'; import { kebabCase } from 'lodash' import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; -import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces'; +import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces'; import { EventDispatcher, EventDispatcherInterface, @@ -260,17 +260,6 @@ export default class AccountsService { return foundAccounts.length > 0; } - public async getAccountByType(tenantId: number, accountTypeKey: string) { - const { AccountType, Account } = this.tenancy.models(tenantId); - const accountType = await AccountType.query() - .findOne('key', accountTypeKey); - - const account = await Account.query() - .findOne('account_type_id', accountType.id); - - return account; - } - /** * Throws error if the account was prefined. * @param {IAccount} account @@ -468,9 +457,11 @@ export default class AccountsService { * @param {number} tenantId * @param {IAccountsFilter} accountsFilter */ - public async getAccountsList(tenantId: number, filter: IAccountsFilter) { + public async getAccountsList( + tenantId: number, + filter: IAccountsFilter, + ): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> { const { Account } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter); this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter }); @@ -478,6 +469,10 @@ export default class AccountsService { builder.withGraphFetched('type'); dynamicList.buildQuery()(builder); }); - return accounts; + + return { + accounts, + filterMeta: dynamicList.getResponseMeta(), + }; } } diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 399c3cc6b..be63d6f93 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -7,9 +7,12 @@ import { ICustomerNewDTO, ICustomerEditDTO, ICustomer, + IPaginationMeta, + ICustomersFilter } from 'interfaces'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class CustomersService { @@ -19,12 +22,15 @@ export default class CustomersService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + /** * Converts customer to contact DTO. * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO * @returns {IContactDTO} */ - customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) { + private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) { return { ...omit(customerDTO, ['customerType']), contactType: customerDTO.customerType, @@ -39,7 +45,7 @@ export default class CustomersService { * @param {ICustomerNewDTO} customerDTO * @return {Promise} */ - async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { + public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { const contactDTO = this.customerToContactDTO(customerDTO) const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); @@ -59,7 +65,7 @@ export default class CustomersService { * @param {number} tenantId * @param {ICustomerEditDTO} customerDTO */ - async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { + public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { const contactDTO = this.customerToContactDTO(customerDTO); return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer'); } @@ -70,7 +76,7 @@ export default class CustomersService { * @param {number} customerId * @return {Promise} */ - async deleteCustomer(tenantId: number, customerId: number) { + public async deleteCustomer(tenantId: number, customerId: number) { const { Contact } = this.tenancy.models(tenantId); await this.getCustomerByIdOrThrowError(tenantId, customerId); @@ -88,10 +94,34 @@ export default class CustomersService { * @param {number} tenantId * @param {number} customerId */ - async getCustomer(tenantId: number, customerId: number) { + public async getCustomer(tenantId: number, customerId: number) { return this.contactService.getContact(tenantId, customerId, 'customer'); } + /** + * Retrieve customers paginated list. + * @param {number} tenantId - Tenant id. + * @param {ICustomersFilter} filter - Cusotmers filter. + */ + public async getCustomersList( + tenantId: number, + filter: ICustomersFilter + ): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { Contact } = this.tenancy.models(tenantId); + const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, filter); + + const { results, pagination } = await Contact.query().onBuild((query) => { + query.modify('customer'); + dynamicList.buildQuery()(query); + }); + + return { + customers: results, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + /** * Writes customer opening balance journal entries. * @param {number} tenantId diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts index f8135dea3..60fe3af38 100644 --- a/server/src/services/DynamicListing/DynamicListService.ts +++ b/server/src/services/DynamicListing/DynamicListService.ts @@ -1,6 +1,6 @@ import { Service, Inject } from "typedi"; import validator from 'is-my-json-valid'; -import { Router, Request, Response, NextFunction } from 'express'; +import { Request, Response, NextFunction } from 'express'; import { ServiceError } from 'exceptions'; import { DynamicFilter, @@ -12,8 +12,13 @@ import { validateFieldKeyExistance, validateFilterRolesFieldsExistance, } from 'lib/ViewRolesBuilder'; +import { + IDynamicListFilterDTO, + IFilterRole, + IDynamicListService, + IModel, +} from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; -import { IDynamicListFilterDTO, IFilterRole, IDynamicListService } from 'interfaces'; const ERRORS = { VIEW_NOT_FOUND: 'view_not_found', @@ -32,11 +37,11 @@ export default class DynamicListService implements IDynamicListService { * @param {number} viewId * @return {Promise} */ - private async getCustomViewOrThrowError(tenantId: number, viewId: number) { + private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) { const { viewRepository } = this.tenancy.repositories(tenantId); const view = await viewRepository.getById(viewId); - if (!view || view.resourceModel !== 'Account') { + if (!view || view.resourceModel !== model.name) { throw new ServiceError(ERRORS.VIEW_NOT_FOUND); } return view; @@ -49,9 +54,9 @@ export default class DynamicListService implements IDynamicListService { * @throws {ServiceError} */ private validateSortColumnExistance(model: any, columnSortBy: string) { - const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy); + const notExistsField = validateFieldKeyExistance(model, columnSortBy); - if (notExistsField) { + if (!notExistsField) { throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND); } } @@ -62,8 +67,10 @@ export default class DynamicListService implements IDynamicListService { * @param {IFilterRole[]} filterRoles * @throws {ServiceError} */ - private validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) { - const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles); + private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) { + const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles); + + console.log(invalidFieldsKeys); if (invalidFieldsKeys.length > 0) { throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); @@ -96,23 +103,21 @@ export default class DynamicListService implements IDynamicListService { * Dynamic listing. * @param {number} tenantId * @param {IModel} model - * @param {IAccountsFilter} filter + * @param {IDynamicListFilterDTO} filter */ - async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) { - const { viewRoleRepository } = this.tenancy.repositories(tenantId); - const dynamicFilter = new DynamicFilter(model.tableName); + public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) { + const dynamicFilter = new DynamicFilter(model); // Custom view filter roles. if (filter.customViewId) { - const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId); - const viewRoles = await viewRoleRepository.allByView(view.id); + const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model); - const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression); + const viewFilter = new DynamicFilterViews(view); dynamicFilter.setFilter(viewFilter); } // Sort by the given column. if (filter.columnSortBy) { - this.validateSortColumnExistance(model, filter.columnSortBy);; + this.validateSortColumnExistance(model, filter.columnSortBy); const sortByFilter = new DynamicFilterSortBy( filter.columnSortBy, filter.sortOrder @@ -138,7 +143,7 @@ export default class DynamicListService implements IDynamicListService { * @param {Response} res * @param {NextFunction} next */ - handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) { + public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { if (error.errorType === 'sort_column_not_found') { return res.boom.badRequest(null, { @@ -147,8 +152,8 @@ export default class DynamicListService implements IDynamicListService { } 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', code: 100 }], + }); } if (error.errorType === 'filter_roles_fields_not_found') { return res.boom.badRequest(null, { diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index f671e0b43..6cf0fc9cf 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -9,7 +9,7 @@ import { ServiceError } from "exceptions"; import TenancyService from 'services/Tenancy/TenancyService'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalCommands from 'services/Accounting/JournalCommands'; -import { IExpense, IAccount, IExpenseDTO, IExpensesService, ISystemUser } from 'interfaces'; +import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; @@ -442,17 +442,23 @@ export default class ExpensesService implements IExpensesService { * @param {IExpensesFilter} expensesFilter * @return {IExpense[]} */ - public async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) { + public async getExpensesList( + tenantId: number, + expensesFilter: IExpensesFilter + ): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { Expense } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter); this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter }); - const expenses = await Expense.query().onBuild((builder) => { + const { results, pagination } = await Expense.query().onBuild((builder) => { builder.withGraphFetched('paymentAccount'); - builder.withGraphFetched('user'); - dynamicFilter.buildQuery()(builder); - }); - return expenses; + }).pagination(expensesFilter.page - 1, expensesFilter.pageSize); + + return { + expenses: results, + pagination, filterMeta: + dynamicFilter.getResponseMeta(), + }; } } \ No newline at end of file diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index cbc8a0ff2..9be35319b 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -8,7 +8,6 @@ import { IItemCategoriesFilter, ISystemUser, } from "interfaces"; -import ItemCategory from "models/ItemCategory"; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -21,6 +20,7 @@ const ERRORS = { SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', + CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS' }; export default class ItemCategoriesService implements IItemCategoriesService { @@ -202,6 +202,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) { this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId }); await this.getItemCategoryOrThrowError(tenantId, itemCategoryId); + await this.unassociateItemsWithCategories(tenantId, itemCategoryId); const { ItemCategory } = this.tenancy.models(tenantId); await ItemCategory.query().findById(itemCategoryId).delete(); @@ -233,10 +234,16 @@ export default class ItemCategoriesService implements IItemCategoriesService { const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter); const itemCategories = await ItemCategory.query().onBuild((query) => { - query.orderBy('createdAt', 'ASC'); dynamicList.buildQuery()(query); }); - return itemCategories; + return { itemCategories, filterMeta: dynamicList.getResponseMeta() }; + } + + private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]) { + const { Item } = this.tenancy.models(tenantId); + const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId]; + + await Item.query().whereIn('id', ids).patch({ category_id: null }); } /** @@ -247,7 +254,8 @@ export default class ItemCategoriesService implements IItemCategoriesService { public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) { this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds }); await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds); - + await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds); + await ItemCategory.query().whereIn('id', itemCategoriesIds).delete(); this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds }); } diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 8428826e2..4b195aeb2 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -4,7 +4,6 @@ import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from "exceptions"; -import { Item } from "models"; const ERRORS = { NOT_FOUND: 'NOT_FOUND', @@ -17,6 +16,9 @@ const ERRORS = { INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', + + ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', + ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS' } @Service() @@ -222,6 +224,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] trying to delete item.', { tenantId, itemId }); await this.getItemOrThrowError(tenantId, itemId); + await this.validateHasNoInvoicesOrBills(tenantId, itemId); await Item.query().findById(itemId).delete(); this.logger.info('[items] deleted successfully.', { tenantId, itemId }); @@ -269,6 +272,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds }); await this.validateItemsIdsExists(tenantId, itemsIds); + await this.validateHasNoInvoicesOrBills(tenantId, itemsIds); await Item.query().whereIn('id', itemsIds).delete(); this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds }); @@ -283,14 +287,39 @@ export default class ItemsService implements IItemsService { const { Item } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter); - const items = await Item.query().onBuild((builder) => { + const { results, pagination } = await Item.query().onBuild((builder) => { builder.withGraphFetched('inventoryAccount'); builder.withGraphFetched('sellAccount'); builder.withGraphFetched('costAccount'); builder.withGraphFetched('category'); dynamicFilter.buildQuery()(builder); - }); - return items; + }).pagination( + itemsFilter.page - 1, + itemsFilter.pageSize, + ); + return { items: results, pagination, filterMeta: dynamicFilter.getResponseMeta() }; + } + + /** + * Validates the given item or items have no associated invoices or bills. + * @param {number} tenantId - Tenant id. + * @param {number|number[]} itemId - Item id. + * @throws {ServiceError} + */ + private async validateHasNoInvoicesOrBills(tenantId: number, itemId: number[]|number) { + const { ItemEntry } = this.tenancy.models(tenantId); + + const ids = Array.isArray(itemId) ? itemId : [itemId]; + const foundItemEntries = await ItemEntry.query() + .whereIn('item_id', ids) + .whereIn('reference_type', ['SaleInvoice', 'Bill']); + + if (foundItemEntries.length > 0) { + throw new ServiceError(ids.length > 1 ? + ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS : + ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS + ); + } } } \ No newline at end of file diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index 614107497..e30e05bcc 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -9,6 +9,7 @@ import { ISystemUser, IManualJournal, IManualJournalEntryDTO, + IPaginationMeta, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; @@ -227,7 +228,7 @@ export default class ManualJournalsService implements IManuaLJournalsService { } /** - * + * Transform DTO to model. * @param {IManualJournalEntryDTO[]} entries */ private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) { @@ -396,16 +397,23 @@ export default class ManualJournalsService implements IManuaLJournalsService { * @param {number} tenantId * @param {IManualJournalsFilter} filter */ - public async getManualJournals(tenantId: number, filter: IManualJournalsFilter) { + public async getManualJournals( + tenantId: number, + filter: IManualJournalsFilter + ): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { ManualJournal } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter); this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter }); - const manualJournal = await ManualJournal.query().onBuild((builder) => { + const { results, pagination } = await ManualJournal.query().onBuild((builder) => { dynamicList.buildQuery()(builder); - }); - return manualJournal; + }).pagination(filter.page - 1, filter.pageSize); + + return { + manualJournals: results, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; } /** diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index 4389f2267..f3825488c 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -1,13 +1,14 @@ import { Inject, Service } from 'typedi'; import { omit, sumBy } from 'lodash'; import moment from 'moment'; -import { IBillPaymentOTD, IBillPayment } from 'interfaces'; +import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import JournalPosterService from 'services/Sales/JournalPosterService'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; /** @@ -25,6 +26,9 @@ export default class BillPaymentsService { @Inject() journalService: JournalPosterService; + @Inject() + dynamicListService: DynamicListingService; + /** * Creates a new bill payment transcations and store it to the storage * with associated bills entries and journal transactions. @@ -39,7 +43,7 @@ export default class BillPaymentsService { * @param {number} tenantId - Tenant id. * @param {BillPaymentDTO} billPayment - Bill payment object. */ - async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { + public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const billPayment = { @@ -102,7 +106,7 @@ export default class BillPaymentsService { * @param {BillPaymentDTO} billPayment * @param {IBillPayment} oldBillPayment */ - async editBillPayment( + public async editBillPayment( tenantId: number, billPaymentId: number, billPaymentDTO, @@ -171,7 +175,7 @@ export default class BillPaymentsService { * @param {Integer} billPaymentId - The given bill payment id. * @return {Promise} */ - async deleteBillPayment(tenantId: number, billPaymentId: number) { + public async deleteBillPayment(tenantId: number, billPaymentId: number) { const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const billPayment = await BillPayment.query().where('id', billPaymentId).first(); @@ -203,7 +207,7 @@ export default class BillPaymentsService { * @param {BillPayment} billPayment * @param {Integer} billPaymentId */ - async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { + private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { const { AccountTransaction, Account } = this.tenancy.models(tenantId); const paymentAmount = sumBy(billPayment.entries, 'payment_amount'); @@ -252,6 +256,35 @@ export default class BillPaymentsService { ]); } + /** + * Retrieve bill payment paginted and filterable list. + * @param {number} tenantId + * @param {IBillPaymentsFilter} billPaymentsFilter + */ + public async listBillPayments( + tenantId: number, + billPaymentsFilter: IBillPaymentsFilter, + ): Promise<{ billPayments: IBillPayment, pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { BillPayment } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, BillPayment, billPaymentsFilter); + + 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); + }).pagination( + billPaymentsFilter.page - 1, + billPaymentsFilter.pageSize, + ); + + return { + billPayments: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + /** * Retrieve bill payment with associated metadata. * @param {number} billPaymentId - The bill payment id. diff --git a/server/src/services/Resource/ResourceService.js b/server/src/services/Resource/ResourceService.js deleted file mode 100644 index 21f15beff..000000000 --- a/server/src/services/Resource/ResourceService.js +++ /dev/null @@ -1,5 +0,0 @@ - - -export default class ResourceService { - -} \ No newline at end of file diff --git a/server/src/services/Resource/ResourceService.ts b/server/src/services/Resource/ResourceService.ts new file mode 100644 index 000000000..cd2364b46 --- /dev/null +++ b/server/src/services/Resource/ResourceService.ts @@ -0,0 +1,78 @@ +import { Service, Inject } from 'typedi'; +import { camelCase, upperFirst } from 'lodash' +import { IModel } from 'interfaces'; +import resourceFieldsKeys from 'data/ResourceFieldsKeys'; +import TenancyService from 'services/Tenancy/TenancyService'; + +@Service() +export default class ResourceService { + @Inject() + tenancy: TenancyService; + + /** + * + * @param {string} resourceName + */ + getResourceFieldsRelations(modelName: string) { + const fieldsRelations = resourceFieldsKeys[modelName]; + + if (!fieldsRelations) { + throw new Error('Fields relation not found in thte given resource model.'); + } + return fieldsRelations; + } + + /** + * Transform resource to model name. + * @param {string} resourceName + */ + private resourceToModelName(resourceName: string): string { + return upperFirst(camelCase(resourceName)); + } + + /** + * Retrieve model from resource name in specific tenant. + * @param {number} tenantId + * @param {string} resourceName + */ + public getModel(tenantId: number, resourceName: string) { + const models = this.tenancy.models(tenantId); + const modelName = this.resourceToModelName(resourceName); + + return models[modelName]; + } + + getModelFields(Model: IModel) { + const fields = Object.keys(Model.fields); + + return fields.sort((a, b) => { + if (a < b) { return -1; } + if (a > b) { return 1; } + return 0; + }); + } + + /** + * + * @param {string} resourceName + */ + getResourceFields(Model: IModel) { + console.log(Model); + + if (Model.resourceable) { + return this.getModelFields(Model); + } + return []; + } + + /** + * + * @param {string} resourceName + */ + getResourceColumns(Model: IModel) { + if (Model.resourceable) { + return this.getModelFields(Model); + } + return []; + } +} \ No newline at end of file diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 57d85c3b7..f0f27a262 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -10,6 +10,7 @@ import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository'; import CustomerRepository from 'repositories/CustomerRepository'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; /** @@ -27,6 +28,9 @@ export default class PaymentReceiveService { @Inject() journalService: JournalPosterService; + @Inject() + dynamicListService: DynamicListingService; + @Inject('logger') logger: any; @@ -37,7 +41,7 @@ export default class PaymentReceiveService { * @param {number} tenantId - Tenant id. * @param {IPaymentReceive} paymentReceive */ - async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { + public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { const { PaymentReceive, PaymentReceiveEntry, @@ -107,7 +111,7 @@ export default class PaymentReceiveService { * @param {IPaymentReceive} paymentReceive - * @param {IPaymentReceive} oldPaymentReceive - */ - async editPaymentReceive( + public async editPaymentReceive( tenantId: number, paymentReceiveId: number, paymentReceive: any, @@ -242,7 +246,7 @@ export default class PaymentReceiveService { * @param {number} tenantId - Tenant id. * @param {Integer} paymentReceiveId - Payment receive id. */ - async getPaymentReceive(tenantId: number, paymentReceiveId: number) { + public async getPaymentReceive(tenantId: number, paymentReceiveId: number) { const { PaymentReceive } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() .where('id', paymentReceiveId) @@ -250,6 +254,30 @@ export default class PaymentReceiveService { .first(); return paymentReceive; } + + /** + * Retrieve payment receives paginated and filterable list. + * @param {number} tenantId + * @param {IPaymentReceivesFilter} paymentReceivesFilter + */ + public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) { + const { PaymentReceive } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter); + + const { results, pagination } = await PaymentReceive.query().onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('depositAccount'); + dynamicFilter.buildQuery()(builder); + }).pagination( + paymentReceivesFilter.page - 1, + paymentReceivesFilter.pageSize, + ); + return { + paymentReceives: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } /** * Retrieve the payment receive details with associated invoices. @@ -310,7 +338,7 @@ export default class PaymentReceiveService { * @param {IPaymentReceive} paymentReceive * @param {Number} paymentReceiveId */ - async recordPaymentReceiveJournalEntries( + private async recordPaymentReceiveJournalEntries( tenantId: number, paymentReceive: any, paymentReceiveId?: number @@ -370,7 +398,7 @@ export default class PaymentReceiveService { * @param {Array} revertInvoices * @return {Promise} */ - async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) { + private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) { const { SaleInvoice } = this.tenancy.models(tenantId); const opers: Promise[] = []; @@ -392,7 +420,7 @@ export default class PaymentReceiveService { * @param {Array} newPaymentReceiveEntries * @return */ - async saveChangeInvoicePaymentAmount( + private async saveChangeInvoicePaymentAmount( tenantId: number, paymentReceiveEntries: [], newPaymentReceiveEntries: [], diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index ac00708c1..8ec782d8f 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -1,8 +1,10 @@ import { omit, difference, sumBy, mixin } from 'lodash'; import { Service, Inject } from 'typedi'; +import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces'; import HasItemsEntries from 'services/Sales/HasItemsEntries'; import { formatDateFields } from 'utils'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; /** * Sale estimate service. @@ -19,6 +21,9 @@ export default class SaleEstimateService { @Inject('logger') logger: any; + @Inject() + dynamicListService: DynamicListingService; + /** * Creates a new estimate with associated entries. * @async @@ -208,4 +213,32 @@ export default class SaleEstimateService { }); return foundEstimates.length > 0; } + + /** + * Retrieves estimates filterable and paginated list. + * @param {number} tenantId + * @param {IEstimatesFilter} estimatesFilter + */ + public async estimatesList( + tenantId: number, + estimatesFilter: IEstimatesFilter + ): Promise<{ salesEstimates: ISaleEstimate[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { SaleEstimate } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleEstimate, estimatesFilter); + + const { results, pagination } = await SaleEstimate.query().onBuild(builder => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + dynamicFilter.buildQuery()(builder); + }).pagination( + estimatesFilter.page - 1, + estimatesFilter.pageSize, + ); + + return { + salesEstimates: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } } \ No newline at end of file diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 877b0bf01..1dc960e5b 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,11 +1,16 @@ import { Service, Inject } from 'typedi'; import { omit, sumBy, difference, pick, chain } from 'lodash'; -import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; import JournalPoster from 'services/Accounting/JournalPoster'; import HasItemsEntries from 'services/Sales/HasItemsEntries'; import InventoryService from 'services/Inventory/Inventory'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; /** @@ -26,6 +31,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost { @Inject('logger') logger: any; + @Inject() + dynamicListService: DynamicListingService; + + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Creates a new sale invoices and store it to the storage * with associated to entries and journal transactions. @@ -34,7 +45,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {ISaleInvoice} saleInvoiceDTO - * @return {ISaleInvoice} */ - async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { + public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); @@ -94,7 +105,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {Number} saleInvoiceId - * @param {ISaleInvoice} saleInvoice - */ - async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { + public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); @@ -152,7 +163,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @async * @param {Number} saleInvoiceId - The given sale invoice id. */ - async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { + public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { const { SaleInvoice, ItemEntry, @@ -215,7 +226,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {number} saleInvoiceId - * @param {boolean} override - */ - recordInventoryTranscactions( + private recordInventoryTranscactions( tenantId: number, saleInvoice, saleInvoiceId: number, @@ -243,7 +254,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {string} transactionType * @param {number} transactionId */ - async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) { + private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) { const { InventoryTransaction } = this.tenancy.models(tenantId); const opers: Promise<[]>[] = []; @@ -280,7 +291,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @async * @param {Number} saleInvoiceId */ - async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) { + public async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) { const { SaleInvoice } = this.tenancy.models(tenantId); return SaleInvoice.query() .where('id', saleInvoiceId) @@ -405,4 +416,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost { journal.saveBalance(), ]); } + + /** + * Retrieve sales invoices filterable and paginated list. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter): + Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { SaleInvoice } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter); + + this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter }); + const { results, pagination } = await SaleInvoice.query().onBuild((builder) => { + builder.withGraphFetched('entries'); + builder.withGraphFetched('customer'); + dynamicFilter.buildQuery()(builder); + }).pagination( + salesInvoicesFilter.page - 1, + salesInvoicesFilter.pageSize, + ); + return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() }; + } } diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index df726b1d2..96a481136 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -4,12 +4,17 @@ import JournalPosterService from 'services/Sales/JournalPosterService'; import HasItemEntries from 'services/Sales/HasItemsEntries'; import TenancyService from 'services/Tenancy/TenancyService'; import { formatDateFields } from 'utils'; +import { IFilterMeta, IPaginationMeta } from 'interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class SalesReceiptService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + @Inject() journalService: JournalPosterService; @@ -22,7 +27,7 @@ export default class SalesReceiptService { * @param {ISaleReceipt} saleReceipt * @return {Object} */ - async createSaleReceipt(tenantId: number, saleReceiptDTO: any) { + public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) { const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e)); @@ -55,7 +60,7 @@ export default class SalesReceiptService { * @param {ISaleReceipt} saleReceipt * @return {void} */ - async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) { + public async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) { const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e)); @@ -88,7 +93,7 @@ export default class SalesReceiptService { * @param {Integer} saleReceiptId * @return {void} */ - async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { + public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const deleteSaleReceiptOper = SaleReceipt.query() .where('id', saleReceiptId) @@ -160,4 +165,35 @@ export default class SalesReceiptService { return saleReceipt; } + + /** + * Retrieve sales receipts paginated and filterable list. + * @param {number} tenantId + * @param {ISaleReceiptFilter} salesReceiptsFilter + */ + public async salesReceiptsList( + tenantId: number, + salesReceiptsFilter: ISaleReceiptFilter, + ): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { SaleReceipt } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleReceipt, salesReceiptsFilter); + + this.logger.info('[sale_receipt] try to get sales receipts list.', { tenantId }); + const { results, pagination } = await SaleReceipt.query().onBuild((builder) => { + builder.withGraphFetched('depositAccount'); + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + + dynamicFilter.buildQuery()(builder); + }).pagination( + salesReceiptsFilter.page - 1, + salesReceiptsFilter.pageSize, + ); + + return { + salesReceipts: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } } diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index 8c3b2479d..2d4dfe160 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -27,7 +27,6 @@ export default class HasTenancyService { singletonService(tenantId: number, key: string, callback: Function) { const container = this.tenantContainer(tenantId); const Logger = Container.get('logger'); - const hasServiceInstnace = container.has(key); if (!hasServiceInstnace) { @@ -74,12 +73,24 @@ export default class HasTenancyService { }); } + /** + * Sets i18n locals function. + * @param {number} tenantId + * @param locals + */ + setI18nLocals(tenantId: number, locals: any) { + return this.singletonService(tenantId, 'i18n', () => { + return locals; + }) + } + /** * Retrieve i18n locales methods. * @param {number} tenantId - Tenant id. */ i18n(tenantId: number) { return this.singletonService(tenantId, 'i18n', () => { + throw new Error('I18n locals is not set yet.'); }); } diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts index 6af82e033..cec769496 100644 --- a/server/src/services/Views/ViewsService.ts +++ b/server/src/services/Views/ViewsService.ts @@ -1,20 +1,24 @@ import { Service, Inject } from "typedi"; -import { pick, difference } from 'lodash'; +import { difference } from 'lodash'; import { ServiceError } from 'exceptions'; import { IViewsService, IViewDTO, IView, - IViewRole, - IViewHasColumn, + IViewEditDTO, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; +import ResourceService from "services/Resource/ResourceService"; import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder'; const ERRORS = { VIEW_NOT_FOUND: 'VIEW_NOT_FOUND', VIEW_PREDEFINED: 'VIEW_PREDEFINED', - INVALID_LOGIC_EXPRESSION: 'INVALID_LOGIC_EXPRESSION', + VIEW_NAME_NOT_UNIQUE: 'VIEW_NAME_NOT_UNIQUE', + LOGIC_EXPRESSION_INVALID: 'INVALID_LOGIC_EXPRESSION', + RESOURCE_FIELDS_KEYS_NOT_FOUND: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', + RESOURCE_COLUMNS_KEYS_NOT_FOUND: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', + RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND' }; @Service() @@ -25,29 +29,131 @@ export default class ViewsService implements IViewsService { @Inject('logger') logger: any; + @Inject() + resourceService: ResourceService; + /** * Listing resource views. - * @param {number} tenantId - * @param {string} resourceModel + * @param {number} tenantId - + * @param {string} resourceModel - */ - public async listViews(tenantId: number, resourceModel: string) { - const { View } = this.tenancy.models(tenantId); - return View.query().where('resource_model', resourceModel); - } + public async listResourceViews(tenantId: number, resourceModel: string): Promise { + this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel }); - validateResourceFieldsExistance() { - - } - - validateResourceColumnsExistance() { - - } - - getView(tenantId: number, viewId: number) { + // Validate the resource model name is valid. + this.getResourceModelOrThrowError(tenantId, resourceModel); + const { viewRepository } = this.tenancy.repositories(tenantId); + return viewRepository.allByResource(resourceModel); } /** + * Validate model resource conditions fields existance. + * @param {string} resourceName + * @param {IViewRoleDTO[]} viewRoles + */ + private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) { + const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel); + + const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey); + const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); + + if (notFoundFieldsKeys.length > 0) { + throw new ServiceError(ERRORS.RESOURCE_FIELDS_KEYS_NOT_FOUND); + } + return notFoundFieldsKeys; + } + + /** + * Validates model resource columns existance. + * @param {string} resourceName + * @param {IViewColumnDTO[]} viewColumns + */ + private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) { + const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel); + + const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey); + const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); + + if (notFoundFieldsKeys.length > 0) { + throw new ServiceError(ERRORS.RESOURCE_COLUMNS_KEYS_NOT_FOUND); + } + return notFoundFieldsKeys; + } + + /** + * Retrieve the given view details with associated conditions and columns. + * @param {number} tenantId - Tenant id. + * @param {number} viewId - View id. + */ + public getView(tenantId: number, viewId: number): Promise { + this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); + return this.getViewOrThrowError(tenantId, viewId); + } + + /** + * Retrieve view or throw not found error. + * @param {number} tenantId - Tenant id. + * @param {number} viewId - View id. + */ + private async getViewOrThrowError(tenantId: number, viewId: number): Promise { + const { viewRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); + const view = await viewRepository.getById(viewId); + + if (!view) { + this.logger.info('[view] view not found.', { tenantId, viewId }); + throw new ServiceError(ERRORS.VIEW_NOT_FOUND); + } + return view; + } + + /** + * Retrieve resource model from resource name or throw not found error. + * @param {number} tenantId + * @param {number} resourceModel + */ + private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel { + const ResourceModel = this.resourceService.getModel(tenantId, resourceModel); + + if (!ResourceModel || !ResourceModel.resourceable) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); + } + return ResourceModel; + } + + /** + * Validates view name uniqiness in the given resource. + * @param {number} tenantId + * @param {stirng} resourceModel + * @param {string} viewName + * @param {number} notViewId + */ + private async validateViewNameUniquiness( + tenantId: number, + resourceModel: string, + viewName: string, + notViewId?: number + ): void { + const { View } = this.tenancy.models(tenantId); + const foundViews = await View.query() + .where('resource_model', resourceModel) + .where('name', viewName) + .onBuild((builder) => { + if (notViewId) { + builder.whereNot('id', notViewId); + } + }); + + if (foundViews.length > 0) { + throw new ServiceError(ERRORS.VIEW_NAME_NOT_UNIQUE); + } + } + + /** + * Creates a new custom view to specific resource. + * ----–––––– * Precedures. * ----–––––– * - Validate resource fields existance. @@ -60,116 +166,78 @@ export default class ViewsService implements IViewsService { * @param {number} tenantId - Tenant id. * @param {IViewDTO} viewDTO - View DTO. */ - async newView(tenantId: number, viewDTO: IViewDTO): Promise { - const { View, ViewColumn, ViewRole } = this.tenancy.models(tenantId); - + public async newView(tenantId: number, viewDTO: IViewDTO): Promise { + const { viewRepository } = this.tenancy.repositories(tenantId); this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO }); + + // Validate the resource name is exists and resourcable. + const ResourceModel = this.getResourceModelOrThrowError(tenantId, viewDTO.resourceModel); + + // Validate view name uniquiness. + await this.validateViewNameUniquiness(tenantId, viewDTO.resourceModel, viewDTO.name); + + // Validate the given fields keys exist on the storage. + this.validateResourceRolesFieldsExistance(ResourceModel, viewDTO.roles); + + // Validate the given columnable fields keys exists on the storage. + this.validateResourceColumnsExistance(ResourceModel, viewDTO.columns); + // Validates the view conditional logic expression. if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) { - throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION); + throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } // Save view details. - const view = await View.query().insert({ - name: viewDTO.name, + const view = await viewRepository.insert({ predefined: false, + name: viewDTO.name, rolesLogicExpression: viewDTO.logicExpression, + resourceModel: viewDTO.resourceModel, + roles: viewDTO.roles, + columns: viewDTO.columns, }); - this.logger.info('[views] inserted to the storage.', { tenantId, viewDTO }); - - // Save view roles async operations. - const saveViewRolesOpers = []; - - viewDTO.roles.forEach((role) => { - const saveViewRoleOper = ViewRole.query().insert({ - ...pick(role, ['fieldKey', 'comparator', 'value', 'index']), - viewId: view.id, - }); - saveViewRolesOpers.push(saveViewRoleOper); - }); - - viewDTO.columns.forEach((column) => { - const saveViewColumnOper = ViewColumn.query().insert({ - viewId: view.id, - index: column.index, - }); - saveViewRolesOpers.push(saveViewColumnOper); - }); - this.logger.info('[views] roles and columns inserted to the storage.', { tenantId, viewDTO }); - - await Promise.all(saveViewRolesOpers); + this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO }); + return view; } /** + * Edits view details, roles and columns on the storage. + * -------- + * Precedures. + * -------- + * - Validate view existance. + * - Validate view resource fields existance. + * - Validate view resource columns existance. + * - Validate view logic expression. + * - Delete old view columns and roles. + * - Re-save view columns and roles. * * @param {number} tenantId * @param {number} viewId * @param {IViewEditDTO} */ - async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO) { - const { View, ViewRole, ViewColumn } = req.models; - const view = await View.query().where('id', viewId) - .withGraphFetched('roles.field') - .withGraphFetched('columns') - .first(); + public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise { + const { View } = this.tenancy.models(tenantId); + this.logger.info('[view] trying to edit custom view.', { tenantId, viewId }); - const errorReasons = []; - const fieldsSlugs = viewEditDTO.roles.map((role) => role.field_key); - const resourceFieldsKeys = resource.fields.map((f) => f.key); - const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field])); - const columnsKeys = viewEditDTO.columns.map((c) => c.key); + // Retrieve view details or throw not found error. + const view = await this.getViewOrThrowError(tenantId, viewId); - // The difference between the stored resource fields and submit fields keys. - const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); + // Validate the resource name is exists and resourcable. + const ResourceModel = this.getResourceModelOrThrowError(tenantId, view.resourceModel); - // Validate not found resource fields keys. - if (notFoundFields.length > 0) { - errorReasons.push({ - type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields, - }); - } - // The difference between the stored resource fields and the submit columns keys. - const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); + // Validate view name uniquiness. + await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId); + + // Validate the given fields keys exist on the storage. + this.validateResourceRolesFieldsExistance(ResourceModel, view.roles); + + // Validate the given columnable fields keys exists on the storage. + this.validateResourceColumnsExistance(ResourceModel, view.columns); - // Validate not found view columns. - if (notFoundColumns.length > 0) { - errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns }); - } // Validates the view conditional logic expression. - if (!validateViewRoles(viewEditDTO.roles, viewEditDTO.logicExpression)) { - errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); + if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) { + throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } - - const viewRolesIds = view.roles.map((r) => r.id); - const viewColumnsIds = view.columns.map((c) => c.id); - - const formUpdatedRoles = viewEditDTO.roles.filter((r) => r.id); - const formInsertRoles = viewEditDTO.roles.filter((r) => !r.id); - - const formRolesIds = formUpdatedRoles.map((r) => r.id); - - const formUpdatedColumns = viewEditDTO.columns.filter((r) => r.id); - const formInsertedColumns = viewEditDTO.columns.filter((r) => !r.id); - const formColumnsIds = formUpdatedColumns.map((r) => r.id); - - const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds); - const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds); - - const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds); - const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds); - - // Validate the not found view roles ids. - if (notFoundViewRolesIds.length) { - errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds }); - } - // Validate the not found view columns ids. - if (notFoundViewColumnsIds.length) { - errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const asyncOpers = []; - // Save view details. await View.query() .where('id', view.id) @@ -177,78 +245,15 @@ export default class ViewsService implements IViewsService { name: viewEditDTO.name, roles_logic_expression: viewEditDTO.logicExpression, }); - - // Update view roles. - if (formUpdatedRoles.length > 0) { - formUpdatedRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const updateOper = ViewRole.query() - .where('id', role.id) - .update({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - }); - asyncOpers.push(updateOper); - }); - } - // Insert a new view roles. - if (formInsertRoles.length > 0) { - formInsertRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const insertOper = ViewRole.query() - .insert({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Delete view roles. - if (rolesIdsShouldDeleted.length > 0) { - const deleteOper = ViewRole.query() - .whereIn('id', rolesIdsShouldDeleted) - .delete(); - asyncOpers.push(deleteOper); - } - // Insert a new view columns to the storage. - if (formInsertedColumns.length > 0) { - formInsertedColumns.forEach((column) => { - const fieldModel = resourceFieldsKeysMap.get(column.key); - const insertOper = ViewColumn.query() - .insert({ - field_id: fieldModel.id, - index: column.index, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Update the view columns on the storage. - if (formUpdatedColumns.length > 0) { - formUpdatedColumns.forEach((column) => { - const updateOper = ViewColumn.query() - .where('id', column.id) - .update({ - index: column.index, - }); - asyncOpers.push(updateOper); - }); - } - // Delete the view columns from the storage. - if (columnsIdsShouldDelete.length > 0) { - const deleteOper = ViewColumn.query() - .whereIn('id', columnsIdsShouldDelete) - .delete(); - asyncOpers.push(deleteOper); - } - await Promise.all(asyncOpers); + this.logger.info('[view] edited successfully.', { tenantId, viewId }); } /** * Retrieve views details of the given id or throw not found error. + * @private * @param {number} tenantId * @param {number} viewId + * @return {Promise} */ private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise { const { View } = this.tenancy.models(tenantId); @@ -267,6 +272,7 @@ export default class ViewsService implements IViewsService { * Deletes the given view with associated roles and columns. * @param {number} tenantId - Tenant id. * @param {number} viewId - View id. + * @return {Promise} */ public async deleteView(tenantId: number, viewId: number): Promise { const { View } = this.tenancy.models(tenantId);