added dms tasks #1499, #1469, #1468, #1484
gitea/salix/dev There was a failure building this commit Details

This commit is contained in:
Joan Sanchez 2019-06-06 13:59:11 +02:00
parent 217b08cd0a
commit c28734ff61
73 changed files with 2851 additions and 324 deletions

View File

@ -2,7 +2,7 @@ const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
module.exports = Self => {
Self.remoteMethodCtx('download', {
Self.remoteMethodCtx('downloadFile', {
description: 'Download a document',
accessType: 'READ',
accepts: [
@ -29,15 +29,15 @@ module.exports = Self => {
}
],
http: {
path: `/:id/download`,
path: `/:id/downloadFile`,
verb: 'GET'
}
});
Self.download = async function(ctx, id) {
const userId = ctx.req.accessToken.userId;
Self.downloadFile = async function(ctx, id) {
const env = process.env.NODE_ENV;
const document = await Self.findById(id, {
const models = Self.app.models;
const dms = await Self.findById(id, {
include: {
relation: 'dmsType',
scope: {
@ -48,18 +48,17 @@ module.exports = Self => {
}
}
});
const readRole = document.dmsType().readRole().name;
const hasRequiredRole = await Self.app.models.Account.hasRole(userId, readRole);
if (!hasRequiredRole)
const hasReadRole = await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
if (!hasReadRole)
throw new UserError(`You don't have enough privileges`);
if (env && env != 'development') {
const path = `/${document.companyFk}/${document.dmsType().path}/${document.file}`;
const path = `/${dms.companyFk}/${dms.dmsType().path}/${dms.file}`;
file = {
path: `/var/lib/salix/dms/${path}`,
contentType: 'application/octet-stream',
name: document.file
name: dms.file
};
} else {
file = {

View File

@ -0,0 +1,46 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
module.exports = Self => {
Self.remoteMethodCtx('removeFile', {
description: 'Makes a logical delete moving a file to a trash folder',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/removeFile`,
verb: 'POST'
}
});
Self.removeFile = async(ctx, id) => {
const models = Self.app.models;
const dms = await models.Dms.findById(id);
const dmsType = await models.DmsType.findById(dms.dmsTypeFk);
const trashDmsType = await models.DmsType.findOne({
where: {
code: 'trash'
}
});
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, dms.dmsTypeFk);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
const file = await models.Container.getFile(dmsType.path, dms.file);
const originPath = `${file.client.root}/${dmsType.path}/${file.name}`;
const destinationPath = `${file.client.root}/${trashDmsType.path}/${file.name}`;
await fs.rename(originPath, destinationPath);
return dms.updateAttribute('dmsTypeFk', trashDmsType.id);
};
};

View File

@ -1,11 +1,11 @@
const app = require('vn-loopback/server/server');
describe('dms download()', () => {
describe('dms downloadFile()', () => {
let dmsId = 1;
it('should return a response for an employee with text content-type', async() => {
let workerFk = 107;
let ctx = {req: {accessToken: {userId: workerFk}}};
const result = await app.models.Dms.download(ctx, dmsId);
let workerId = 107;
let ctx = {req: {accessToken: {userId: workerId}}};
const result = await app.models.Dms.downloadFile(ctx, dmsId);
expect(result[1]).toEqual('text/plain');
});
@ -15,7 +15,7 @@ describe('dms download()', () => {
let ctx = {req: {accessToken: {userId: clientId}}};
let error;
await app.models.Dms.download(ctx, dmsId).catch(e => {
await app.models.Dms.downloadFile(ctx, dmsId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);

View File

@ -0,0 +1,19 @@
const app = require('vn-loopback/server/server');
describe('dms removeFile()', () => {
let dmsId = 1;
it(`should return an error for a user without enough privileges`, async() => {
let clientId = 101;
let ctx = {req: {accessToken: {userId: clientId}}};
let error;
await app.models.Dms.removeFile(ctx, dmsId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('dms uploadFile()', () => {
it(`should return an error for a user without enough privileges`, async() => {
let clientId = 101;
let ticketDmsTypeId = 14;
let ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: ticketDmsTypeId}};
let error;
await app.models.Dms.uploadFile(ctx).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,118 @@
const fs = require('fs-extra');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('uploadFile', {
description: 'Uploads a file and inserts into dms model',
accessType: 'WRITE',
accepts: [{
arg: 'options',
type: 'object'
},
{
arg: 'warehouseId',
type: 'Number',
description: ''
},
{
arg: 'companyId',
type: 'Number',
description: ''
},
{
arg: 'dmsTypeId',
type: 'Number',
description: ''
},
{
arg: 'reference',
type: 'String',
description: ''
},
{
arg: 'description',
type: 'String',
description: ''
},
{
arg: 'hasFile',
type: 'Boolean',
description: ''
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/uploadFile`,
verb: 'POST'
}
});
Self.uploadFile = async(ctx, options = {}) => {
const models = Self.app.models;
const storageConnector = Self.app.dataSources.storage.connector;
const myUserId = ctx.req.accessToken.userId;
const myWorker = await models.Worker.findOne({where: {userFk: myUserId}});
const args = ctx.args;
const fileOptions = {};
const hasParentTransaction = options && options.transaction;
if (!options.transaction)
options.transaction = await Self.beginTransaction({});
try {
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
const uploaded = await models.Container.upload('temp', ctx.req, ctx.result, fileOptions);
const files = Object.values(uploaded.files).map(file => {
return file[0];
});
const dmsType = await models.DmsType.findById(args.dmsTypeId);
const promises = [];
files.forEach(file => {
const newDms = Self.create({
workerFk: myWorker.id,
dmsTypeFk: args.dmsTypeId,
companyFk: args.companyId,
warehouseFk: args.warehouseId,
reference: args.reference,
description: args.description,
hasFile: args.hasFile
}, options).then(newDms => {
const extension = storageConnector.getFileExtension(file.name);
const fileName = `${newDms.id}.${extension}`;
return newDms.updateAttribute('file', fileName, options);
}).then(dms => {
return models.Container.getContainer('temp').then(container => {
const originPath = `${container.client.root}/${container.name}/${file.name}`;
const destinationPath = `${container.client.root}/${dmsType.path}/${dms.file}`;
return fs.rename(originPath, destinationPath).then(() => {
return dms;
});
});
});
promises.push(newDms);
});
const resolvedPromise = await Promise.all(promises);
if (!hasParentTransaction)
await options.transaction.commit();
return resolvedPromise;
} catch (e) {
if (!hasParentTransaction)
await options.transaction.rollback();
throw e;
}
};
};

View File

@ -43,6 +43,9 @@
},
"DmsType": {
"dataSource": "vn"
},
"Container": {
"dataSource": "storage"
}
}

View File

@ -0,0 +1,13 @@
{
"name": "Container",
"base": "VnModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}

View File

@ -1,3 +1,5 @@
module.exports = Self => {
require('../methods/dms/download')(Self);
require('../methods/dms/downloadFile')(Self);
require('../methods/dms/uploadFile')(Self);
require('../methods/dms/removeFile')(Self);
};

62
back/models/dmsType.js Normal file
View File

@ -0,0 +1,62 @@
module.exports = Self => {
/**
* Checks if current user has
* read privileges over a dms
*
* @param {Object} ctx - Request context
* @param {Interger} id - DmsType id
* @return {Boolean} True for user with read privileges
*/
Self.hasReadRole = async(ctx, id) => {
const models = Self.app.models;
const dmsType = await models.DmsType.findById(id, {
include: {
relation: 'readRole'
}
});
return await hasRole(ctx, dmsType);
};
/**
* Checks if current user has
* write privileges over a dms
*
* @param {Object} ctx - Request context
* @param {Interger} id - DmsType id
* @return {Boolean} True for user with write privileges
*/
Self.hasWriteRole = async(ctx, id) => {
const models = Self.app.models;
const dmsType = await models.DmsType.findById(id, {
include: {
relation: 'writeRole'
}
});
return await hasRole(ctx, dmsType);
};
/**
* Checks if current user has
* read or write privileges
* @param {Object} ctx - Context
* @param {Object} dmsType - Dms type [read/write]
*/
async function hasRole(ctx, dmsType) {
const models = Self.app.models;
const myUserId = ctx.req.accessToken.userId;
const readRole = dmsType.readRole() && dmsType.readRole().name;
const writeRole = dmsType.writeRole() && dmsType.writeRole().name;
const requiredRole = readRole || writeRole;
const hasRequiredRole = await models.Account.hasRole(myUserId, requiredRole);
const isRoot = await models.Account.hasRole(myUserId, 'root');
if (isRoot || hasRequiredRole)
return true;
return false;
}
};

View File

@ -1,3 +1,12 @@
INSERT INTO `salix`.`ACL` ( `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ( 'ClientDms', 'remove', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` ( `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ( 'ClientDms', '*', 'READ', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Route', 'updateVolume', 'WRITE', 'ALLOW', 'ROLE', 'deliveryBoss');
INSERT INTO `salix`.`ACL` ( `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Dms', 'removeFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Dms', 'uploadFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Dms', 'downloadFile', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'uploadFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('ClientDms', 'removeFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('ClientDms', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'uploadFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('TicketDms', 'removeFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('TicketDms', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'updateVolume', 'WRITE', 'ALLOW', 'ROLE', 'deliveryBoss');

View File

@ -4,10 +4,6 @@ ALTER TABLE `vn2008`.`clientes_gestdoc`
DROP PRIMARY KEY,
ADD PRIMARY KEY (`gest_doc_id`);
ALTER TABLE `vn2008`.`clientes_gestdoc`
DROP INDEX `fk_clientes_gestdoc_1_idx` ;
ALTER TABLE `vn2008`.`clientes_gestdoc`
ADD INDEX `fk_clientes_gestdoc_1_idx` (`Id_Cliente` ASC);

View File

@ -0,0 +1,4 @@
UPDATE `vn2008`.`gesttip` SET `writeRoleFk`='1', `readRoleFk`='1' WHERE `id`='5';
UPDATE `vn2008`.`gesttip` SET `writeRoleFk`='1', `readRoleFk`='1' WHERE `id`='12';
UPDATE `vn2008`.`gesttip` SET `writeRoleFk`='1', `readRoleFk`='1' WHERE `id`='14';
UPDATE `vn2008`.`gesttip` SET `writeRoleFk`='1', `readRoleFk`='1' WHERE `id`='13';

View File

@ -1,9 +0,0 @@
ALTER TABLE `vn2008`.`clientes_gestdoc`
ADD INDEX `fk_clientes_gestdoc_1_idx` (`Id_Cliente` ASC);
ALTER TABLE `vn2008`.`clientes_gestdoc`
ADD CONSTRAINT `fk_clientes_gestdoc_3`
FOREIGN KEY (`Id_Cliente`)
REFERENCES `vn2008`.`Clientes` (`id_cliente`)
ON DELETE RESTRICT
ON UPDATE CASCADE;

View File

@ -1439,16 +1439,16 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c
(2, 'Doc oficial', 'oficial', NULL, NULL, 'officialDoc'),
(3, 'Laboral', 'laboral', NULL, NULL, 'hhrrData'),
(4, 'Albaranes recibidos', 'entradas', NULL, NULL, 'deliveryNote'),
(5, 'Otros', 'otros', 1, NULL, 'miscellaneous'),
(5, 'Otros', 'otros', 1, 1, 'miscellaneous'),
(6, 'Pruebas', 'pruebas', NULL, NULL, 'tests'),
(7, 'IAE Clientes', 'IAE_Clientes', NULL, NULL, 'economicActivitiesTax'),
(8, 'Fiscal', 'fiscal', NULL, NULL, 'fiscal'),
(9, 'Vehiculos', 'vehiculos', NULL, NULL, 'vehicles'),
(10, 'Plantillas', 'plantillas', NULL, NULL, 'templates'),
(11, 'Contratos', 'contratos', NULL, NULL, 'contracts'),
(12, 'ley de pagos', 'ley pagos', NULL, NULL, 'paymentsLaw'),
(13, 'Basura', 'basura', NULL, NULL, 'trash'),
(14, 'Ticket', 'tickets', 1, NULL, 'ticket'),
(12, 'ley de pagos', 'ley pagos', 1, 1, 'paymentsLaw'),
(13, 'Basura', 'basura', 1, 1, 'trash'),
(14, 'Ticket', 'tickets', 1, 1, 'ticket'),
(15, 'Presupuestos', 'Presupuestos', NULL, NULL, 'budgets'),
(16, 'Logistica', 'logistica', NULL, NULL, 'logistics'),
(17, 'cmr', 'cmr', NULL, NULL, 'cmr'),

View File

@ -16,7 +16,6 @@ vn-check {
}
md-checkbox {
margin-bottom: 0.8em;
width: 20px;
margin-bottom: 0.8em
}
}

View File

@ -41,6 +41,7 @@ import './chip';
import './color-legend';
import './input-number';
import './input-time';
import './input-file';
import './treeview';
import './treeview/child';
import './calendar';

View File

@ -0,0 +1,39 @@
<div class="container"
ng-class="{selected: $ctrl.hasFocus}">
<div class="textField">
<div class="leftIcons" ng-transclude="leftIcons"></div>
<div class="infix">
<section class="value" ng-click="$ctrl.openFileSelector()" translate>
{{$ctrl.value}}
</section>
<input
class="mdl-textfield__input"
type="file"
name="{{::$ctrl.name}}"
ng-model="$ctrl.files"
vn-validation="{{$ctrl.rule}}"
ng-disabled="$ctrl.disabled"
ng-readonly="$ctrl.readonly"
ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false"
tabindex="{{$ctrl.input.tabindex}}"
accept="{{$ctrl.accept}}"/>
<label class="label" translate>{{$ctrl.label}}</label>
</div>
<div class="underline"></div>
<div class="selected underline"></div>
<div class="suffix">
<i class="material-icons"
ng-if="::$ctrl.hasInfo"
vn-tooltip="{{::$ctrl.info}}">
info_outline
</i>
<vn-icon-button
icon="cloud_upload"
vn-tooltip="Select a file"
ng-click="$ctrl.openFileSelector()">
</vn-icon-button>
</div>
<div class="rightIcons" ng-transclude="rightIcons"></div>
</div>
</div>

View File

@ -0,0 +1,132 @@
import ngModule from '../../module';
import Input from '../../lib/input';
import './style.scss';
export default class InputFile extends Input {
constructor($element, $scope, $attrs, vnTemplate) {
super($element, $scope);
this.element = $element[0];
this.hasFocus = false;
this._multiple = false;
this._value = 'Select a file';
vnTemplate.normalizeInputAttrs($attrs);
this.registerEvents();
}
/**
* Registers all event emitters
*/
registerEvents() {
this.input.addEventListener('change', event => {
const target = event.target;
const fileNames = Array.from(target.files).map(file => {
return file.name;
});
const names = fileNames.join(', ');
const label = this.element.querySelector('.value');
label.innerHTML = names;
this.files = target.files;
this.emit('change', {files: target.files, event});
});
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
}
/**
* Gets current value
*/
get files() {
return this._files;
}
/**
* Sets input value
*
* @param {Number} value - Value
*/
set files(value) {
this._files = value;
this.hasValue = !(value === null || value === undefined || value === '');
if (this.hasValue)
this.element.classList.add('not-empty');
else
this.element.classList.remove('not-empty');
}
/**
* Gets if multiple file selection
*/
get multiple() {
return this._multiple;
}
/**
* Sets multiple file selection
*
* @param {Boolean} value - True if is multiple
*/
set multiple(value) {
this._multiple = value;
if (value)
this.input.multiple = true;
else
this.input.multiple = false;
}
/**
* Returns a list of valid file types
*/
get accept() {
return this._accept;
}
/**
* Sets a list of valid file types
*
* @param {String} value - Valid file types
*/
set accept(value) {
this._accept = value;
}
/**
* Fires file selection explorer event
*/
openFileSelector() {
this.input.click();
}
}
InputFile.$inject = ['$element', '$scope', '$attrs', 'vnTemplate'];
ngModule.component('vnInputFile', {
template: require('./index.html'),
controller: InputFile,
transclude: {
leftIcons: '?tLeftIcons',
rightIcons: '?tRightIcons'
},
bindings: {
label: '@?',
name: '@?',
disabled: '<?',
multiple: '<?',
accept: '@?',
rule: '@?',
files: '=model',
validate: '&',
onChange: '&',
onClear: '&'
}
});

View File

@ -0,0 +1,38 @@
import './index.js';
fdescribe('Component vnInputFile', () => {
let $scope;
let $attrs;
let $timeout;
let $element;
let controller;
beforeEach(ngModule('vnCore'));
beforeEach(angular.mock.inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$attrs = {};
$element = angular.element('<div><input type="file"><div class="infix"><div class="rightIcons"></div></div>');
controller = $componentController('vnInputFile', {$element, $scope, $attrs, $timeout, $transclude: () => {}});
controller.input = $element[0].querySelector('input');
controller.validate = () => {};
}));
describe('files() setter', () => {
it(`should set a value, and then add the class 'not-empty'`, () => {
controller.files = [{name: 'MyFile'}];
let classes = controller.element.classList.toString();
expect(classes).toContain('not-empty');
});
it(`should set an empty value, and then remove the class 'not-empty'`, () => {
controller.files = null;
let classes = controller.element.classList.toString();
expect(classes).not.toContain('not-empty');
});
});
});

View File

@ -0,0 +1,15 @@
@import "variables";
@import '../textfield/style.scss';
vn-input-file {
@extend vn-textfield;
.value {
color: $color-font-secondary;
cursor: pointer;
padding: 4px 0;
outline: 0
}
input {
display: none !important
}
}

View File

@ -184,6 +184,10 @@ InputNumber.$inject = ['$element', '$scope', '$attrs', 'vnTemplate'];
ngModule.component('vnInputNumber', {
template: require('./index.html'),
controller: InputNumber,
transclude: {
leftIcons: '?tLeftIcons',
rightIcons: '?tRightIcons'
},
bindings: {
label: '@?',
name: '@?',

View File

@ -1,5 +1,6 @@
import ngModule from '../../module';
import Input from '../../lib/input';
import './style.scss';
/**
* Draw checkbox with a drop-down and multi options

View File

@ -0,0 +1,9 @@
vn-multi-check {
md-checkbox {
margin-bottom: 0.8em;
.md-label {
margin: 0
}
}
}

View File

@ -29,6 +29,10 @@ vn-textfield {
}
}
.suffix vn-icon-button {
padding: 0
}
t-left-icons {
padding-right: 0.5em
}
@ -47,6 +51,7 @@ vn-textfield {
i.clear {
visibility: hidden;
cursor: pointer;
outline: 0;
&:hover {
color: #222;

View File

@ -49,4 +49,5 @@ Loading: Cargando
Fields to show: Campos a mostrar
Create new one: Crear nuevo
Toggle: Desplegar/Plegar
Check all: Seleccionar todo
Check all: Seleccionar todo
Select a file: Selecciona un fichero

View File

@ -1,3 +1,4 @@
const uuid = require('uuid/v1');
module.exports = function(app) {
let models = app.models();
@ -27,9 +28,19 @@ module.exports = function(app) {
app.enableAuth();
// eslint-disable-next-line new-cap
let router = app.loopback.Router();
router.get('/status', app.loopback.status());
app.use(router);
const storageConnector = app.dataSources.storage.connector;
storageConnector.getFilename = function(file) {
return `${uuid()}.${storageConnector.getFileExtension(file.name)}`;
};
storageConnector.getFileExtension = function(fileName) {
return fileName.split('.').pop();
};
/*
let ds = app.dataSources.auth;
//ds.automigrate(function() {

View File

@ -14,5 +14,20 @@
"multipleStatements": true,
"connectTimeout": 20000,
"acquireTimeout": 20000
},
"storage": {
"name": "storage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "/mnt/storage/dms",
"maxFileSize": "10485760",
"allowedContentTypes": [
"application/pdf",
"application/zip",
"application/rar",
"image/png",
"image/jpeg",
"image/jpg"
]
}
}

View File

@ -0,0 +1,33 @@
module.exports = Self => {
Self.remoteMethodCtx('removeFile', {
description: 'Removes a client document',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
},
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/removeFile`,
verb: 'POST'
}
});
Self.removeFile = async(ctx, id) => {
const models = Self.app.models;
const targetClientDms = await models.ClientDms.findById(id);
const targetDms = await models.Dms.findById(targetClientDms.dmsFk);
const trashDmsType = await models.DmsType.findOne({where: {code: 'trash'}});
await models.Dms.removeFile(ctx, targetClientDms.dmsFk);
await targetClientDms.destroy();
return targetDms.updateAttribute('dmsTypeFk', trashDmsType.id);
};
};

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('ClientDms removeFile()', () => {
const clientDmsFk = 3;
it(`should return an error for a user without enough privileges`, async() => {
let clientId = 101;
let ctx = {req: {accessToken: {userId: clientId}}};
let error;
await app.models.ClientDms.removeFile(ctx, clientDmsFk).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,19 @@
const app = require('vn-loopback/server/server');
describe('Client uploadFile()', () => {
it(`should return an error for a user without enough privileges`, async() => {
let clientId = 101;
let currentUserId = 102;
let paymentLawTypeId = 12;
let ctx = {req: {accessToken: {userId: currentUserId}}, args: {dmsTypeId: paymentLawTypeId}};
let error;
await app.models.Client.uploadFile(ctx, clientId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,77 @@
module.exports = Self => {
Self.remoteMethodCtx('uploadFile', {
description: 'Upload and attach a file to a client',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The client id',
http: {source: 'path'}
},
{
arg: 'warehouseId',
type: 'Number',
description: ''
},
{
arg: 'companyId',
type: 'Number',
description: ''
},
{
arg: 'dmsTypeId',
type: 'Number',
description: ''
},
{
arg: 'reference',
type: 'String',
description: ''
},
{
arg: 'description',
type: 'String',
description: ''
},
{
arg: 'hasFile',
type: 'Boolean',
description: ''
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/uploadFile`,
verb: 'POST'
}
});
Self.uploadFile = async(ctx, id) => {
const models = Self.app.models;
const transaction = await Self.beginTransaction({});
const options = {transaction};
const promises = [];
try {
const uploadedFiles = await models.Dms.uploadFile(ctx, options);
uploadedFiles.forEach(dms => {
const newClientDms = models.ClientDms.create({
clientFk: id,
dmsFk: dms.id
}, options);
promises.push(newClientDms);
});
const resolvedPromises = await Promise.all(promises);
await transaction.commit();
return resolvedPromises;
} catch (err) {
await transaction.rollback();
throw err;
}
};
};

View File

@ -1,35 +0,0 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('removes', {
description: 'Delete an client dms',
accessType: 'WRITE',
accepts: {
arg: 'dmsId',
type: 'number',
required: true,
description: 'dms identifier',
},
returns: {
type: 'string',
root: true
},
http: {
path: `/removes`,
verb: 'POST'
}
});
Self.removes = async dmsId => {
if (!dmsId)
throw new UserError('There is nothing to delete');
let targetClientDms = await Self.app.models.ClientDms.findOne({where: {dmsFk: dmsId}});
let targetDms = await Self.app.models.Dms.findById(dmsId);
let trashDmsType = await Self.app.models.DmsType.findOne({where: {code: 'trash'}});
await targetClientDms.destroy();
await targetDms.updateAttribute('dmsTypeFk', trashDmsType.id);
};
};

View File

@ -1,29 +0,0 @@
const app = require('vn-loopback/server/server');
describe('Client dms stuff', () => {
let dmsToRestore;
let dmsTypeToRestore;
afterAll(async done => {
await app.models.ClientDms.create(dmsToRestore);
await dmsTypeToRestore.save();
done();
});
it('should delete a dms from a client and update the dmsType to trash', async() => {
const dmsId = 2;
dmsToRestore = await app.models.ClientDms.findOne({where: {dmsFk: dmsId}});
dmsTypeToRestore = await app.models.Dms.findById(dmsToRestore.dmsFk);
await app.models.ClientDms.removes(dmsId);
let destroyedDms = await app.models.ClientDms.findOne({where: {dmsFk: dmsId}});
let alteredDmsType = await app.models.Dms.findById(dmsId);
expect(destroyedDms).toBeNull();
expect(alteredDmsType.dmsTypeFk).toEqual(13);
});
});

View File

@ -1,3 +1,3 @@
module.exports = Self => {
require('../methods/dms/removes')(Self);
require('../methods/client-dms/removeFile')(Self);
};

View File

@ -20,6 +20,7 @@ module.exports = Self => {
require('../methods/client/getTransactions')(Self);
require('../methods/client/confirmTransaction')(Self);
require('../methods/client/canBeInvoiced')(Self);
require('../methods/client/uploadFile')(Self);
// Validations

View File

@ -0,0 +1,64 @@
<mg-ajax path="/api/dms/upload" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.dms">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" margin-medium enctype="multipart/form-data">
<div compact>
<vn-card pad-large>
<vn-horizontal>
<vn-textfield vn-one vn-focus
label="Reference"
field="$ctrl.dms.reference">
</vn-textfield>
<vn-autocomplete vn-one
label="Company"
field="$ctrl.dms.companyId"
url="/api/Companies"
show-field="code"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
label="Warehouse"
field="$ctrl.dms.warehouseId"
url="/api/Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete vn-one
label="Type"
field="$ctrl.dms.dmsTypeId"
url="/api/DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea vn-one
label="Description"
field="$ctrl.dms.description">
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file vn-one
label="File"
model="$ctrl.dms.files"
on-change="$ctrl.onFileChange(files)"
accept=".pdf, .png, .jpg, .jpeg, application/zip, application/rar">
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check
label="Attached file"
field="$ctrl.dms.hasFile">
</vn-check>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit label="Upload"></vn-submit>
<vn-button ui-sref="client.card.dms.index" label="Cancel"></vn-button>
</vn-button-bar>
</div>
</form>

View File

@ -0,0 +1,97 @@
import ngModule from '../../module';
import './style.scss';
class Controller {
constructor($scope, $http, $state, $translate, vnApp) {
this.$ = $scope;
this.$http = $http;
this.$state = $state;
this.$translate = $translate;
this.vnApp = vnApp;
this.dms = {
files: [],
hasFile: false
};
}
get client() {
return this._client;
}
set client(value) {
this._client = value;
if (value)
this.setDefaultParams();
}
setDefaultParams() {
const params = {filter: {
where: {code: 'paymentsLaw'}
}};
this.$http.get('/api/DmsTypes/findOne', {params}).then(res => {
const dmsType = res.data && res.data;
const companyId = window.localStorage.defaultCompanyFk;
const warehouseId = window.localStorage.defaultWarehouseFk;
const defaultParams = {
reference: this.client.id,
warehouseId: warehouseId,
companyId: companyId,
dmsTypeId: dmsType.id,
description: this.$translate.instant('ClientFileDescription', {
dmsTypeName: dmsType.name,
clientId: this.client.id,
clientName: this.client.name
}).toUpperCase()
};
this.dms = Object.assign(this.dms, defaultParams);
});
}
onSubmit() {
const query = `/api/clients/${this.client.id}/uploadFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
this.$.watcher.updateOriginalData();
this.$state.go('client.card.dms.index');
}
});
}
onFileChange(files) {
if (files.length > 0) {
this.$.$applyAsync(() => {
this.dms.hasFile = true;
});
}
}
}
Controller.$inject = ['$scope', '$http', '$state', '$translate', 'vnApp'];
ngModule.component('vnClientDmsCreate', {
template: require('./index.html'),
controller: Controller,
bindings: {
client: '<'
}
});

View File

@ -0,0 +1,60 @@
import './index';
describe('Client', () => {
describe('Component vnClientDmsCreate', () => {
let controller;
let $scope;
let $httpBackend;
let $httpParamSerializer;
beforeEach(ngModule('client'));
beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnClientDmsCreate', {$scope});
controller._client = {id: 101, name: 'Bruce wayne'};
}));
describe('client() setter', () => {
it('should set the client data and then call setDefaultParams()', () => {
spyOn(controller, 'setDefaultParams');
controller.client = {
id: 15,
name: 'Bruce wayne'
};
expect(controller.client).toBeDefined();
expect(controller.setDefaultParams).toHaveBeenCalledWith();
});
});
describe('setDefaultParams()', () => {
it('should perform a GET query and define the dms property on controller', () => {
const params = {filter: {
where: {code: 'paymentsLaw'}
}};
let serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `/api/DmsTypes/findOne?${serializedParams}`).respond({id: 12, code: 'paymentsLaw'});
$httpBackend.expect('GET', `/api/DmsTypes/findOne?${serializedParams}`);
controller.setDefaultParams();
$httpBackend.flush();
expect(controller.dms).toBeDefined();
expect(controller.dms.reference).toEqual(101);
expect(controller.dms.dmsTypeId).toEqual(12);
});
});
describe('onFileChange()', () => {
it('should set dms hasFile property to true if has any files', () => {
const files = [{id: 1, name: 'MyFile'}];
controller.onFileChange(files);
$scope.$apply();
expect(controller.dms.hasFile).toBeTruthy();
});
});
});
});

View File

@ -0,0 +1 @@
ClientFileDescription: "{{dmsTypeName}} from client {{clientName}} id {{clientId}}"

View File

@ -0,0 +1,5 @@
Upload file: Subir fichero
Upload: Subir
File: Fichero
ClientFileDescription: "{{dmsTypeName}} del cliente {{clientName}} id {{clientId}}"
Attached file: Fichero adjunto

View File

@ -0,0 +1,7 @@
vn-ticket-request {
vn-textfield {
margin: 0!important;
max-width: 100px;
}
}

View File

@ -1,74 +0,0 @@
<vn-crud-model
vn-id="model"
url="/client/api/ClientDms"
link="{clientFk: $ctrl.$stateParams.id}"
filter="::$ctrl.filter"
limit="20"
data="$ctrl.clientDms">
</vn-crud-model>
<vn-vertical>
<vn-card pad-large>
<vn-vertical>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="dmsFk" default-order="DESC" number>File</vn-th>
<vn-th>Description</vn-th>
<vn-th>Reference</vn-th>
<vn-th number>Hard copy</vn-th>
<vn-th>Worker</vn-th>
<vn-th>Created</vn-th>
<vn-th></vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="document in $ctrl.clientDms">
<vn-td number>{{::document.dmsFk}}</vn-td>
<vn-td title = "{{::document.dms.description}}">
{{::document.dms.description}}</vn-td>
</vn-td>
<vn-td>{{::document.dms.reference}}</vn-td>
<vn-td number>{{::document.dms.hardCopyNumber | dashIfEmpty}}</vn-td>
<vn-td>
<span class="link"
ng-click="$ctrl.showWorkerDescriptor($event, document.dms.workerFk)">
{{::document.dms.worker.user.nickname | dashIfEmpty}}
</span>
</vn-td>
<vn-td>{{::document.dms.created | dateTime:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td center>
<vn-icon-button
vn-tooltip="Remove file"
icon="delete"
ng-click="$ctrl.showDeleteConfirm(document.dmsFk)"
tabindex="-1">
</vn-icon-button>
</vn-td>
<vn-td center>
<a
target="_blank"
href="api/dms/{{::document.dmsFk}}/download?access_token={{::$ctrl.accessToken}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download file' | translate}}">
</vn-icon-button>
</a>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-vertical>
<vn-pagination model="model"></vn-pagination>
</vn-card>
</vn-vertical>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-confirm
vn-id="confirm"
message="This file will be deleted"
question="Are you sure?"
on-response="$ctrl.deleteDms(response)">
</vn-confirm>

View File

@ -0,0 +1,95 @@
<vn-crud-model
vn-id="model"
url="/client/api/ClientDms"
link="{clientFk: $ctrl.$stateParams.id}"
filter="::$ctrl.filter"
limit="20"
data="$ctrl.clientDms">
</vn-crud-model>
<vn-vertical>
<vn-card pad-large>
<vn-vertical>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="dmsFk" default-order="DESC" shrink>Id</vn-th>
<vn-th field="dmsTypeFk" shrink>Type</vn-th>
<vn-th field="reference">Reference</vn-th>
<vn-th>Description</vn-th>
<vn-th field="hasFile" center>Attached file</vn-th>
<vn-th shrink>File</vn-th>
<vn-th>Employee</vn-th>
<vn-th field="created">Created</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="document in $ctrl.clientDms">
<vn-td number shrink>{{::document.dmsFk}}</vn-td>
<vn-td shrink>
<span title="{{::document.dms.dmsType.name}}">
{{::document.dms.dmsType.name}}
</span>
</vn-td>
<vn-td>
<span title="{{::document.dms.reference}}">
{{::document.dms.reference}}
</span>
</vn-td>
<vn-td>
<span title="{{::document.dms.description}}">
{{::document.dms.description}}
</span>
</vn-td>
<vn-td center>
<vn-check disabled="true"
field="document.dms.hasFile">
</vn-check>
</vn-td>
<vn-td shrink>{{::document.dms.file}}</vn-td>
<vn-td>
<span class="link"
ng-click="$ctrl.showWorkerDescriptor($event, document.dms.workerFk)">
{{::document.dms.worker.user.nickname | dashIfEmpty}}
</span></vn-td>
<vn-td>
{{::document.dms.created | dateTime:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td center shrink>
<a target="_blank"
vn-tooltip="Download file"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.accessToken}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download PDF' | translate}}">
</vn-icon-button>
</a>
<vn-icon-button
vn-tooltip="Remove file"
icon="delete"
ng-click="$ctrl.showDeleteConfirm($index)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-vertical>
<vn-pagination model="model"></vn-pagination>
</vn-card>
</vn-vertical>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<a ui-sref="client.card.dms.create"
vn-tooltip="Upload file"
vn-bind="+"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-confirm
vn-id="confirm"
message="This file will be deleted"
question="Are you sure you want to continue?"
on-response="$ctrl.deleteDms(response)">
</vn-confirm>

View File

@ -1,4 +1,4 @@
import ngModule from '../module';
import ngModule from '../../module';
import './style.scss';
class Controller {
@ -13,7 +13,15 @@ class Controller {
include: {
relation: 'dms',
scope: {
fields: ['dmsTypeFk', 'workerFk', 'file', 'created', 'description', 'reference', 'hardCopyNumber'],
fields: [
'dmsTypeFk',
'workerFk',
'reference',
'description',
'hasFile',
'file',
'created',
],
include: [{
relation: 'dmsType',
scope: {
@ -45,19 +53,18 @@ class Controller {
this.$.workerDescriptor.show();
}
showDeleteConfirm(dmsId) {
this.selectedDms = dmsId;
showDeleteConfirm(index) {
this.dmsIndex = index;
this.$.confirm.show();
}
deleteDms(response) {
if (response === 'ACCEPT') {
let dmsId = this.selectedDms;
let query = `/client/api/ClientDms/removes`;
this.$http.post(query, {dmsId}).then(() => {
const dmsFk = this.clientDms[this.dmsIndex].dmsFk;
const query = `/api/ClientDms/${dmsFk}/removeFile`;
this.$http.post(query).then(() => {
this.$.model.remove(this.dmsIndex);
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
this.$.model.refresh();
});
}
}
@ -65,7 +72,7 @@ class Controller {
Controller.$inject = ['$stateParams', '$scope', 'vnToken', '$http', 'vnApp', '$translate'];
ngModule.component('vnClientDms', {
ngModule.component('vnClientDmsIndex', {
template: require('./index.html'),
controller: Controller,
});

View File

@ -35,5 +35,5 @@ import './sample/create';
import './web-payment';
import './log';
import './sms';
import './dms';
import './dms/index';
import './dms/create';

View File

@ -25,7 +25,7 @@
{"state": "client.card.contact", "icon": "contact_phone"},
{"state": "client.card.sample.index", "icon": "mail"},
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
{"state": "client.card.dms", "icon": "cloud_download"}
{"state": "client.card.dms.index", "icon": "cloud_upload"}
]
}
],
@ -319,8 +319,23 @@
{
"url": "/dms",
"state": "client.card.dms",
"component": "vn-client-dms",
"abstract": true,
"component": "ui-view"
},
{
"url": "/index",
"state": "client.card.dms.index",
"component": "vn-client-dms-index",
"description": "File management"
},
{
"url": "/create",
"state": "client.card.dms.create",
"component": "vn-client-dms-create",
"description": "Upload file",
"params": {
"client": "$ctrl.client"
}
}
]
}

View File

@ -0,0 +1,33 @@
module.exports = Self => {
Self.remoteMethodCtx('removeFile', {
description: 'Removes a ticket document',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
},
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/removeFile`,
verb: 'POST'
}
});
Self.removeFile = async(ctx, id) => {
const models = Self.app.models;
const targetTicketDms = await models.TicketDms.findById(id);
const targetDms = await models.Dms.findById(targetTicketDms.dmsFk);
const trashDmsType = await models.DmsType.findOne({where: {code: 'trash'}});
await models.Dms.removeFile(ctx, targetTicketDms.dmsFk);
await targetTicketDms.destroy();
return targetDms.updateAttribute('dmsTypeFk', trashDmsType.id);
};
};

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('TicketDms removeFile()', () => {
const ticketDmsId = 1;
it(`should return an error for a user without enough privileges`, async() => {
let clientId = 101;
let ctx = {req: {accessToken: {userId: clientId}}};
let error;
await app.models.TicketDms.removeFile(ctx, ticketDmsId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,19 @@
const app = require('vn-loopback/server/server');
describe('Ticket uploadFile()', () => {
it(`should return an error for a user without enough privileges`, async() => {
let ticketId = 15;
let currentUserId = 101;
let ticketTypeId = 14;
let ctx = {req: {accessToken: {userId: currentUserId}}, args: {dmsTypeId: ticketTypeId}};
let error;
await app.models.Ticket.uploadFile(ctx, ticketId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,77 @@
module.exports = Self => {
Self.remoteMethodCtx('uploadFile', {
description: 'Upload and attach a document',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The ticket id',
http: {source: 'path'}
},
{
arg: 'warehouseId',
type: 'Number',
description: ''
},
{
arg: 'companyId',
type: 'Number',
description: ''
},
{
arg: 'dmsTypeId',
type: 'Number',
description: ''
},
{
arg: 'reference',
type: 'String',
description: ''
},
{
arg: 'description',
type: 'String',
description: ''
},
{
arg: 'hasFile',
type: 'Boolean',
description: ''
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/uploadFile`,
verb: 'POST'
}
});
Self.uploadFile = async(ctx, id) => {
const models = Self.app.models;
const transaction = await Self.beginTransaction({});
const options = {transaction};
const promises = [];
try {
const uploadedFiles = await models.Dms.uploadFile(ctx, options);
uploadedFiles.forEach(dms => {
const newTicketDms = models.TicketDms.create({
ticketFk: id,
dmsFk: dms.id
}, options);
promises.push(newTicketDms);
});
const resolvedPromises = await Promise.all(promises);
await transaction.commit();
return resolvedPromises;
} catch (err) {
await transaction.rollback();
throw err;
}
};
};

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/ticket-dms/removeFile')(Self);
};

View File

@ -3,8 +3,7 @@
"base": "Loggable",
"log": {
"model": "TicketLog",
"relation": "ticket",
"showField": "dmsFk"
"relation": "ticket"
},
"options": {
"mysql": {
@ -12,11 +11,6 @@
}
},
"properties": {
"ticketFk": {
"type": "Number",
"id": true,
"required": true
},
"dmsFk": {
"type": "Number",
"id": true,

View File

@ -23,4 +23,5 @@ module.exports = Self => {
require('../methods/ticket/updateEditableTicket')(Self);
require('../methods/ticket/checkEmptiness')(Self);
require('../methods/ticket/updateDiscount')(Self);
require('../methods/ticket/uploadFile')(Self);
};

View File

@ -0,0 +1,63 @@
<vn-watcher
vn-id="watcher"
data="$ctrl.dms">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" margin-medium enctype="multipart/form-data">
<div compact>
<vn-card pad-large>
<vn-horizontal>
<vn-textfield vn-one vn-focus
label="Reference"
field="$ctrl.dms.reference">
</vn-textfield>
<vn-autocomplete vn-one
label="Company"
field="$ctrl.dms.companyId"
url="/api/Companies"
show-field="code"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
label="Warehouse"
field="$ctrl.dms.warehouseId"
url="/api/Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete vn-one
label="Type"
field="$ctrl.dms.dmsTypeId"
url="/api/DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea vn-one
label="Description"
field="$ctrl.dms.description">
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file vn-one
label="File"
model="$ctrl.dms.files"
on-change="$ctrl.onFileChange(files)"
accept=".pdf, .png, .jpg, .jpeg, application/zip, application/rar">
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check
label="Attached file"
field="$ctrl.dms.hasFile">
</vn-check>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit label="Upload"></vn-submit>
<vn-button ui-sref="ticket.card.dms.index" label="Cancel"></vn-button>
</vn-button-bar>
</div>
</form>

View File

@ -0,0 +1,95 @@
import ngModule from '../../module';
import './style.scss';
class Controller {
constructor($scope, $http, $state, $translate, vnApp) {
this.$ = $scope;
this.$http = $http;
this.$state = $state;
this.$translate = $translate;
this.vnApp = vnApp;
this.dms = {
files: [],
hasFile: false
};
}
get ticket() {
return this._ticket;
}
set ticket(value) {
this._ticket = value;
if (value)
this.setDefaultParams();
}
setDefaultParams() {
const params = {filter: {
where: {code: 'ticket'}
}};
this.$http.get('/api/DmsTypes/findOne', {params}).then(res => {
const dmsTypeId = res.data && res.data.id;
const defaultParams = {
reference: this.ticket.id,
warehouseId: this.ticket.warehouseFk,
companyId: this.ticket.companyFk,
dmsTypeId: dmsTypeId,
description: this.$translate.instant('FileDescription', {
ticketId: this.ticket.id,
clientId: this.ticket.client.id,
clientName: this.ticket.client.name
}).toUpperCase()
};
this.dms = Object.assign(this.dms, defaultParams);
});
}
onSubmit() {
const query = `/api/tickets/${this.ticket.id}/uploadFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
this.$.watcher.updateOriginalData();
this.$state.go('ticket.card.dms.index');
}
});
}
onFileChange(files) {
if (files.length > 0) {
this.$.$applyAsync(() => {
this.dms.hasFile = true;
});
}
}
}
Controller.$inject = ['$scope', '$http', '$state', '$translate', 'vnApp'];
ngModule.component('vnTicketDmsCreate', {
template: require('./index.html'),
controller: Controller,
bindings: {
ticket: '<'
}
});

View File

@ -0,0 +1,65 @@
import './index';
describe('Ticket', () => {
describe('Component vnTicketDmsCreate', () => {
let controller;
let $scope;
let $httpBackend;
let $httpParamSerializer;
beforeEach(ngModule('ticket'));
beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnTicketDmsCreate', {$scope});
controller._ticket = {
id: 15,
client: {id: 101, name: 'Bruce wayne'},
warehouseFk: 1,
companyFk: 1
};
}));
describe('client() setter', () => {
it('should set the ticket data and then call setDefaultParams()', () => {
spyOn(controller, 'setDefaultParams');
controller.ticket = {
id: 15,
name: 'Bruce wayne'
};
expect(controller.ticket).toBeDefined();
expect(controller.setDefaultParams).toHaveBeenCalledWith();
});
});
describe('setDefaultParams()', () => {
it('should perform a GET query and define the dms property on controller', () => {
const params = {filter: {
where: {code: 'ticket'}
}};
let serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `/api/DmsTypes/findOne?${serializedParams}`).respond({id: 14, code: 'ticket'});
$httpBackend.expect('GET', `/api/DmsTypes/findOne?${serializedParams}`);
controller.setDefaultParams();
$httpBackend.flush();
expect(controller.dms).toBeDefined();
expect(controller.dms.reference).toEqual(15);
expect(controller.dms.dmsTypeId).toEqual(14);
});
});
describe('onFileChange()', () => {
it('should set dms hasFile property to true if has any files', () => {
const files = [{id: 1, name: 'MyFile'}];
controller.onFileChange(files);
$scope.$apply();
expect(controller.dms.hasFile).toBeTruthy();
});
});
});
});

View File

@ -0,0 +1 @@
FileDescription: Ticket id {{ticketId}} from client {{clientName}} id {{clientId}}

View File

@ -0,0 +1,5 @@
Upload file: Subir fichero
Upload: Subir
File: Fichero
FileDescription: Ticket id {{ticketId}} del cliente {{clientName}} id {{clientId}}
Attached file: Fichero adjunto

View File

@ -0,0 +1,7 @@
vn-ticket-request {
vn-textfield {
margin: 0!important;
max-width: 100px;
}
}

View File

@ -1,51 +0,0 @@
<vn-crud-model
vn-id="model"
url="/ticket/api/TicketDms"
link="{ticketFk: $ctrl.$stateParams.id}"
filter="::$ctrl.filter"
limit="20"
data="$ctrl.ticketDms">
</vn-crud-model>
<vn-vertical>
<vn-card pad-large>
<vn-vertical>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="dmsFk" default-order="DESC" number>Id</vn-th>
<vn-th>Type</vn-th>
<vn-th>Employee</vn-th>
<vn-th>Created</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="document in $ctrl.ticketDms">
<vn-td number>{{::document.dmsFk}}</vn-td>
<vn-td>{{::document.dms.dmsType.name}}</vn-td>
<vn-td>
<span class="link"
ng-click="$ctrl.showWorkerDescriptor($event, document.dms.workerFk)">
{{::document.dms.worker.user.nickname | dashIfEmpty}}
</span></vn-td>
<vn-td>{{::document.dms.created | dateTime:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td center>
<a
target="_blank"
href="api/dms/{{::document.dmsFk}}/download?access_token={{::$ctrl.accessToken}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download PDF' | translate}}">
</vn-icon-button>
</a>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-vertical>
<vn-pagination model="model"></vn-pagination>
</vn-card>
</vn-vertical>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -0,0 +1,95 @@
<vn-crud-model
vn-id="model"
url="/ticket/api/TicketDms"
link="{ticketFk: $ctrl.$stateParams.id}"
filter="::$ctrl.filter"
limit="20"
data="$ctrl.ticketDms">
</vn-crud-model>
<vn-vertical>
<vn-card pad-large>
<vn-vertical>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="dmsFk" default-order="DESC" shrink>Id</vn-th>
<vn-th field="dmsTypeFk" shrink>Type</vn-th>
<vn-th field="reference">Reference</vn-th>
<vn-th>Description</vn-th>
<vn-th field="hasFile" center>Attached file</vn-th>
<vn-th shrink>File</vn-th>
<vn-th>Employee</vn-th>
<vn-th field="created">Created</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="document in $ctrl.ticketDms">
<vn-td number shrink>{{::document.dmsFk}}</vn-td>
<vn-td shrink>
<span title="{{::document.dms.dmsType.name}}">
{{::document.dms.dmsType.name}}
</span>
</vn-td>
<vn-td>
<span title="{{::document.dms.reference}}">
{{::document.dms.reference}}
</span>
</vn-td>
<vn-td>
<span title="{{::document.dms.description}}">
{{::document.dms.description}}
</span>
</vn-td>
<vn-td center>
<vn-check disabled="true"
field="document.dms.hasFile">
</vn-check>
</vn-td>
<vn-td shrink>{{::document.dms.file}}</vn-td>
<vn-td>
<span class="link"
ng-click="$ctrl.showWorkerDescriptor($event, document.dms.workerFk)">
{{::document.dms.worker.user.nickname | dashIfEmpty}}
</span></vn-td>
<vn-td>
{{::document.dms.created | dateTime:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td center shrink>
<a target="_blank"
vn-tooltip="Download file"
href="api/dms/{{::document.dmsFk}}/downloadFile?access_token={{::$ctrl.accessToken}}">
<vn-icon-button
icon="cloud_download"
title="{{'Download PDF' | translate}}">
</vn-icon-button>
</a>
<vn-icon-button
vn-tooltip="Remove file"
icon="delete"
ng-click="$ctrl.showDeleteConfirm($index)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-vertical>
<vn-pagination model="model"></vn-pagination>
</vn-card>
</vn-vertical>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<a ui-sref="ticket.card.dms.create"
vn-tooltip="Upload file"
vn-bind="+"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-confirm
vn-id="confirm"
message="This file will be deleted"
question="Are you sure you want to continue?"
on-response="$ctrl.deleteDms(response)">
</vn-confirm>

View File

@ -1,16 +1,27 @@
import ngModule from '../module';
import ngModule from '../../module';
import './style.scss';
class Controller {
constructor($stateParams, $scope, vnToken) {
constructor($stateParams, $scope, $http, $translate, vnToken, vnApp) {
this.$stateParams = $stateParams;
this.$ = $scope;
this.$http = $http;
this.$translate = $translate;
this.accessToken = vnToken.token;
this.vnApp = vnApp;
this.filter = {
include: {
relation: 'dms',
scope: {
fields: ['dmsTypeFk', 'workerFk', 'file', 'created'],
fields: [
'dmsTypeFk',
'workerFk',
'reference',
'description',
'hasFile',
'file',
'created',
],
include: [{
relation: 'dmsType',
scope: {
@ -41,11 +52,27 @@ class Controller {
this.$.workerDescriptor.workerFk = workerFk;
this.$.workerDescriptor.show();
}
showDeleteConfirm(index) {
this.dmsIndex = index;
this.$.confirm.show();
}
deleteDms(response) {
if (response === 'ACCEPT') {
const dmsFk = this.ticketDms[this.dmsIndex].dmsFk;
const query = `/api/TicketDms/${dmsFk}/removeFile`;
this.$http.post(query).then(() => {
this.$.model.remove(this.dmsIndex);
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
});
}
}
}
Controller.$inject = ['$stateParams', '$scope', 'vnToken'];
Controller.$inject = ['$stateParams', '$scope', '$http', '$translate', 'vnToken', 'vnApp'];
ngModule.component('vnTicketDms', {
ngModule.component('vnTicketDmsIndex', {
template: require('./index.html'),
controller: Controller,
});

View File

@ -0,0 +1,3 @@
Type: Tipo
File management: Gestión documental
Are you sure you want to continue?: ¿Seguro que quieres continuar?

View File

@ -1,2 +0,0 @@
Type: Tipo
File management: Gestión documental

View File

@ -31,5 +31,5 @@ import './request/index';
import './request/create';
import './log';
import './weekly';
import './dms';
import './dms/index';
import './dms/create';

View File

@ -19,7 +19,7 @@
{"state": "ticket.card.picture", "icon": "image"},
{"state": "ticket.card.log", "icon": "history"},
{"state": "ticket.card.request.index", "icon": "icon-100"},
{"state": "ticket.card.dms", "icon": "cloud_download"}
{"state": "ticket.card.dms.index", "icon": "cloud_download"}
],
"keybindings": [
{"key": "t", "state": "ticket.index"}
@ -216,8 +216,23 @@
{
"url": "/dms",
"state": "ticket.card.dms",
"component": "vn-ticket-dms",
"abstract": true,
"component": "ui-view"
},
{
"url": "/index",
"state": "ticket.card.dms.index",
"component": "vn-ticket-dms-index",
"description": "File management"
},
{
"url": "/create",
"state": "ticket.card.dms.create",
"component": "vn-ticket-dms-create",
"description": "Upload file",
"params": {
"ticket": "$ctrl.ticket"
}
}
]
}

1273
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
"loopback": "^3.25.0",
"loopback-boot": "^2.27.1",
"loopback-component-explorer": "^6.3.1",
"loopback-component-storage": "^3.6.0",
"loopback-connector-mysql": "^5.3.1",
"loopback-connector-remote": "^3.4.1",
"loopback-context": "^3.4.0",