feat: Sorting manual journals and items.

This commit is contained in:
Ahmed Bouhuolia
2020-04-19 17:00:28 +02:00
parent cdee562ae7
commit 090c744f57
11 changed files with 237 additions and 132 deletions

View File

@@ -9,6 +9,7 @@ exports.seed = (knex) => {
{ id: 2, name: 'items' },
{ id: 3, name: 'expenses' },
{ id: 4, name: 'manual_journals' },
{ id: 5, name: 'items_categories' },
]);
});
};

View File

@@ -80,6 +80,7 @@ exports.seed = (knex) => {
id: 10,
resource_id: 2,
label_name: 'Name',
key: 'name',
data_type: 'textbox',
predefined: 1,
columnable: true,
@@ -88,6 +89,7 @@ exports.seed = (knex) => {
id: 11,
resource_id: 2,
label_name: 'Type',
key: 'type',
data_type: 'textbox',
predefined: 1,
columnable: true,

View File

@@ -13,9 +13,15 @@ import Resource from '@/models/Resource';
import View from '@/models/View';
import {
mapViewRolesToConditionals,
validateViewRoles,
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
import FilterRoles from '@/lib/FilterRoles';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
export default {
/**
@@ -78,6 +84,8 @@ export default {
}
const filter = {
filter_roles: [],
page: 1,
page_size: 10,
...req.query,
};
if (filter.stringified_filter_roles) {
@@ -85,7 +93,6 @@ export default {
}
const errorReasons = [];
const viewConditionals = [];
const manualJournalsResource = await Resource.query()
.where('name', 'manual_journals')
.withGraphFetched('fields')
@@ -109,27 +116,52 @@ export default {
builder.first();
});
const resourceFieldsKeys = manualJournalsResource.fields.map((c) => c.key);
const dynamicFilter = new DynamicFilter(ManualJournal.tableName);
// Dynamic filter with view roles.
if (view && view.roles.length > 0) {
viewConditionals.push(
...mapViewRolesToConditionals(view.roles),
const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression,
);
if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) {
if (!viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
}
dynamicFilter.setFilter(viewFilter);
}
// Validate the accounts resource fields.
const filterRoles = new FilterRoles(Resource.tableName,
filter.filter_roles.map((role) => ({ ...role, columnKey: role.fieldKey })),
manualJournalsResource.fields);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 });
// Dynamic filter with filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
manualJournalsResource.fields,
);
dynamicFilter.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'MANUAL.JOURNAL.HAS.NO.FIELDS', code: 500 });
}
}
// Dynamic filter with column sort order.
if (filter.column_sort_order) {
if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
// Manual journals.
const manualJournals = await ManualJournal.query();
const manualJournals = await ManualJournal.query().onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({
manualJournals,

View File

@@ -1,9 +1,23 @@
import express from 'express';
import { check, param, validationResult } from 'express-validator';
import {
check,
param,
validationResult,
query,
} from 'express-validator';
import asyncMiddleware from '../middleware/asyncMiddleware';
import ItemCategory from '@/models/ItemCategory';
import Authorization from '@/http/middleware/authorization';
import JWTAuth from '@/http/middleware/jwtAuth';
import Resource from '@/models/Resource';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
import {
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
export default {
/**
@@ -15,40 +29,25 @@ export default {
router.use(JWTAuth);
router.post(
'/:id',
// permit('create', 'edit'),
router.post('/:id',
this.editCategory.validation,
asyncMiddleware(this.editCategory.handler)
);
asyncMiddleware(this.editCategory.handler));
router.post(
'/',
// permit('create'),
router.post('/',
this.newCategory.validation,
asyncMiddleware(this.newCategory.handler)
);
asyncMiddleware(this.newCategory.handler));
router.delete(
'/:id',
// permit('create', 'edit', 'delete'),
router.delete('/:id',
this.deleteItem.validation,
asyncMiddleware(this.deleteItem.handler)
);
asyncMiddleware(this.deleteItem.handler));
router.get(
'/:id',
// permit('view'),
router.get('/:id',
this.getCategory.validation,
asyncMiddleware(this.getCategory.handler)
);
asyncMiddleware(this.getCategory.handler));
router.get(
'/',
// permit('view'),
router.get('/',
this.getList.validation,
asyncMiddleware(this.getList.handler)
);
asyncMiddleware(this.getList.handler));
return router;
},
@@ -58,29 +57,21 @@ export default {
*/
newCategory: {
validation: [
check('name')
.exists()
.trim()
.escape(),
check('name').exists().trim().escape(),
check('parent_category_id')
.optional({ nullable: true, checkFalsy: true })
.isNumeric()
.toInt(),
check('description')
.optional()
.trim()
.escape()
check('description').optional().trim().escape()
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error',
...validationErrors
code: 'validation_error', ...validationErrors,
});
}
const { user } = req;
const form = { ...req.body };
@@ -97,7 +88,7 @@ export default {
}
const category = await ItemCategory.query().insert({
...form,
user_id: user.id
user_id: user.id,
});
return res.status(200).send({ category });
}
@@ -109,18 +100,12 @@ export default {
editCategory: {
validation: [
param('id').toInt(),
check('name')
.exists()
.trim()
.escape(),
check('name').exists().trim().escape(),
check('parent_category_id')
.optional({ nullable: true, checkFalsy: true })
.isNumeric()
.toInt(),
check('description')
.optional()
.trim()
.escape()
check('description').optional().trim().escape(),
],
async handler(req, res) {
const { id } = req.params;
@@ -129,7 +114,7 @@ export default {
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error',
...validationErrors
...validationErrors,
});
}
@@ -162,7 +147,7 @@ export default {
.update({ ...form });
return res.status(200).send({ id: updateItemCategory });
}
},
},
/**
@@ -170,9 +155,7 @@ export default {
*/
deleteItem: {
validation: [
param('id')
.exists()
.toInt()
param('id').exists().toInt()
],
async handler(req, res) {
const { id } = req.params;
@@ -196,9 +179,81 @@ export default {
* Retrieve the list of items.
*/
getList: {
validation: [],
validation: [
query('column_sort_order').optional().trim().escape(),
query('sort_order').optional().isInt(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
],
async handler(req, res) {
const categories = await ItemCategory.query();
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const categoriesResource = await Resource.query()
.where('name', 'items_categories')
.withGraphFetched('fields')
.first();
if (!categoriesResource) {
return res.status(400).send({ errors: [
{ type: 'ITEMS.CATEGORIES.RESOURCE.NOT.FOUND', code: 200, }
]});
}
const filter = {
column_sort_order: '',
sort_order: '',
filter_roles: [],
...req.query,
};
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const errorReasons = [];
const resourceFieldsKeys = categoriesResource.fields.map((c) => c.key);
const dynamicFilter = new DynamicFilter(ItemCategory.tableName);
// Dynamic filter with filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
categoriesResource.fields,
);
categoriesResource.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 });
}
}
// Dynamic filter with column sort order.
if (filter.column_sort_order) {
if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const categories = await ItemCategory.query().onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
builder.select([
'*',
ItemCategory.relatedQuery('items').count().as('count'),
]);
});
return res.status(200).send({ categories });
}

View File

@@ -1,6 +1,5 @@
import express from 'express';
import { check, query, oneOf, validationResult } from 'express-validator';
import moment from 'moment';
import { check, query, validationResult } from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import jwtAuth from '@/http/middleware/jwtAuth';
@@ -13,9 +12,15 @@ import Authorization from '@/http/middleware/authorization';
import View from '@/models/View';
import {
mapViewRolesToConditionals,
validateViewRoles,
mapFilterRolesToDynamicFilter,
} from '@/lib/ViewRolesBuilder';
import FilterRoles from '@/lib/FilterRoles';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
export default {
@@ -64,7 +69,9 @@ export default {
check('sell_account_id').exists().isInt().toInt(),
check('inventory_account_id')
.if(check('type').equals('inventory'))
.exists().isInt().toInt(),
.exists()
.isInt()
.toInt(),
check('category_id').optional().isInt().toInt(),
check('custom_fields').optional().isArray({ min: 1 }),
@@ -159,7 +166,11 @@ export default {
editItem: {
validation: [
check('name').exists(),
check('type').exists().trim().escape().isIn(['product', 'service']),
check('type')
.exists()
.trim()
.escape()
.isIn(['product', 'service']),
check('cost_price').exists().isNumeric(),
check('sell_price').exists().isNumeric(),
check('cost_account_id').exists().isInt(),
@@ -281,7 +292,7 @@ export default {
]});
}
const filter = {
column_sort_order: 'created_at',
column_sort_order: '',
sort_order: '',
page: 1,
page_size: 10,
@@ -304,20 +315,45 @@ export default {
builder.withGraphFetched('columns');
builder.first();
});
const resourceFieldsKeys = itemsResource.fields.map((c) => c.key);
const dynamicFilter = new DynamicFilter(Item.tableName);
// Dynamic filter with view roles.
if (view && view.roles.length > 0) {
viewConditions.push(
...mapViewRolesToConditionals(view.roles),
const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression,
);
if (!validateViewRoles(viewConditions, view.rolesLogicExpression)) {
if (!viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
}
dynamicFilter.setFilter(viewFilter);
}
const filterConditions = new FilterRoles(Item.tableName,
filter.filter_roles.map((role) => ({ ...role, columnKey: role.fieldKey })),
itemsResource.fields,
);
if (filterConditions.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 });
// Dynamic filter with filter roles.
if (filter.filter_roles.length > 0) {
// Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
itemsResource.fields,
);
dynamicFilter.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 });
}
}
// Dynamic filter with column sort order.
if (filter.column_sort_order) {
if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_order,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
@@ -328,14 +364,7 @@ export default {
builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('category');
builder.modify('sortBy', filter.column_sort_order, filter.sort_order);
if (viewConditions.length > 0) {
builder.modify('viewRolesBuilder', viewConditions, view.rolesLogicExpression);
}
if (filter.filter_roles.length > 0) {
filterConditions.buildQuery()(builder);
}
dynamicFilter.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size);
return res.status(200).send({

View File

@@ -17,7 +17,7 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
...role,
index: index + 1,
columnKey: role.field_key,
comparator: role.comparator === 'AND' ? '&&' : '||',
condition: role.comparator === 'AND' ? '&&' : '||',
}));
this.resourceFields = resourceFields;
}

View File

@@ -86,7 +86,7 @@ export function buildSortColumnJoin(tableName, sortColumnKey) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation);
}
}
};
}
/**

View File

@@ -24,8 +24,8 @@ export default class ItemCategory extends BaseModel {
relation: Model.HasManyRelation,
modelClass: Item.default,
join: {
from: 'items_categories.itemId',
to: 'items.id',
from: 'items_categories.id',
to: 'items.categoryId',
},
},
};