Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2622-route_tickets_refactor
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Carlos Jimenez Ruiz 2020-12-21 12:39:26 +01:00
commit 482cbd7195
187 changed files with 1858 additions and 352 deletions

6
.gitignore vendored
View File

@ -1,11 +1,7 @@
coverage
node_modules
dist
e2e/dms/*/
!e2e/dms/c4c
!e2e/dms/c81
!e2e/dms/ecc
!e2e/dms/a87
storage
npm-debug.log
.eslintcache
datasources.*.json

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('updateFile', {
@ -84,66 +85,46 @@ module.exports = Self => {
};
async function uploadNewFile(ctx, dms, myOptions) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const TempContainer = models.TempContainer;
const DmsContainer = models.DmsContainer;
const fileOptions = {};
const tempContainer = await getContainer('temp');
const makeUpload = await models.Container.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const tempContainer = await TempContainer.container('dms');
const makeUpload = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const keys = Object.values(makeUpload.files);
const files = keys.map(file => file[0]);
const file = files[0];
const uploadedFile = files[0];
if (file) {
const oldExtension = storageConnector.getFileExtension(dms.file);
const newExtension = storageConnector.getFileExtension(file.name);
if (uploadedFile) {
const oldExtension = DmsContainer.getFileExtension(dms.file);
const newExtension = DmsContainer.getFileExtension(uploadedFile.name);
const fileName = `${dms.id}.${newExtension}`;
try {
if (oldExtension != newExtension) {
const pathHash = storageConnector.getPathHash(dms.id);
const pathHash = DmsContainer.getHash(dms.id);
await models.Container.removeFile(pathHash, dms.file);
await DmsContainer.removeFile(pathHash, dms.file);
}
} catch (err) {}
const updatedDms = await dms.updateAttributes({
contentType: file.type,
contentType: uploadedFile.type,
file: fileName
}, myOptions);
const pathHash = storageConnector.getPathHash(updatedDms.id);
const container = await getContainer(pathHash);
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
const srcFile = path.join(file.client.root, file.container, file.name);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`;
const destinationPath = `${container.client.root}/${pathHash}/${updatedDms.file}`;
const pathHash = DmsContainer.getHash(updatedDms.id);
const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, updatedDms.file);
fs.rename(originPath, destinationPath);
await fs.move(srcFile, dstFile, {
overwrite: true
});
return updatedDms;
}
}
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
async function getContainer(name) {
const models = Self.app.models;
let container;
try {
container = await models.Container.getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models.Container.createContainer({
name: name
});
} else throw err;
}
return container;
}
};

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('uploadFile', {
@ -46,8 +47,9 @@ module.exports = Self => {
});
Self.uploadFile = async(ctx, options) => {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const TempContainer = models.TempContainer;
const DmsContainer = models.DmsContainer;
const fileOptions = {};
const args = ctx.args;
@ -62,28 +64,33 @@ module.exports = Self => {
myOptions.transaction = tx;
}
let srcFile;
try {
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId, myOptions);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
// Upload file to temporary path
const tempContainer = await getContainer('temp');
const uploaded = await models.Container.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const tempContainer = await TempContainer.container('dms');
const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const files = Object.values(uploaded.files).map(file => {
return file[0];
});
const addedDms = [];
for (const file of files) {
const newDms = await createDms(ctx, file, myOptions);
const pathHash = storageConnector.getPathHash(newDms.id);
const container = await getContainer(pathHash);
for (const uploadedFile of files) {
const newDms = await createDms(ctx, uploadedFile, myOptions);
const pathHash = DmsContainer.getHash(newDms.id);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`;
const destinationPath = `${container.client.root}/${pathHash}/${newDms.file}`;
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
await fs.rename(originPath, destinationPath);
const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, newDms.file);
await fs.move(srcFile, dstFile, {
overwrite: true
});
addedDms.push(newDms);
}
@ -92,13 +99,16 @@ module.exports = Self => {
return addedDms;
} catch (e) {
if (tx) await tx.rollback();
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e;
}
};
async function createDms(ctx, file, myOptions) {
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}}, myOptions);
const args = ctx.args;
@ -115,33 +125,9 @@ module.exports = Self => {
}, myOptions);
let fileName = file.name;
const extension = storageConnector.getFileExtension(fileName);
const extension = models.DmsContainer.getFileExtension(fileName);
fileName = `${newDms.id}.${extension}`;
return newDms.updateAttribute('file', fileName, myOptions);
}
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
async function getContainer(name) {
const models = Self.app.models;
let container;
try {
container = await models.Container.getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models.Container.createContainer({
name: name
});
} else throw err;
}
return container;
}
};

View File

@ -1,8 +1,9 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethod('download', {
Self.remoteMethodCtx('download', {
description: 'Get the user image',
accessType: 'READ',
accepts: [
@ -49,15 +50,9 @@ module.exports = Self => {
}
});
Self.download = async function(collection, size, id) {
Self.download = async function(ctx, collection, size, id) {
const models = Self.app.models;
const filter = {
where: {
name: collection},
include: {
relation: 'readRole'
}
};
const filter = {where: {name: collection}};
const imageCollection = await models.ImageCollection.findOne(filter);
const entity = await models[imageCollection.model].findById(id, {
fields: ['id', imageCollection.property]
@ -69,28 +64,23 @@ module.exports = Self => {
if (!image) return false;
const imageRole = imageCollection.readRole().name;
const hasRole = await models.Account.hasRole(id, imageRole);
if (!hasRole)
const hasReadRole = models.ImageCollection.hasReadRole(ctx, collection);
if (!hasReadRole)
throw new UserError(`You don't have enough privileges`);
let file;
let env = process.env.NODE_ENV;
if (env && env != 'development') {
file = {
path: `/var/lib/salix/image/${collection}/${size}/${image.name}.png`,
contentType: 'image/png',
name: `${image.name}.png`
};
} else {
file = {
path: `${process.cwd()}/storage/image/${collection}/${size}/${image.name}.png`,
contentType: 'image/png',
name: `${image.name}.png`
};
}
const container = await models.ImageContainer.getContainer(collection);
const rootPath = container.client.root;
const fileSrc = path.join(rootPath, collection, size);
const file = {
path: `${fileSrc}/${image.name}.png`,
contentType: 'image/png',
name: `${image.name}.png`
};
if (!fs.existsSync(file.path)) return [];
await fs.access(file.path);
let stream = fs.createReadStream(file.path);
const stream = fs.createReadStream(file.path);
return [stream, file.contentType, `filename="${file.name}"`];
};
};

View File

@ -3,10 +3,12 @@ const app = require('vn-loopback/server/server');
describe('image download()', () => {
const collection = 'user';
const size = '160x160';
const employeeId = 1;
const ctx = {req: {accessToken: {userId: employeeId}}};
it('should return the image content-type of the user', async() => {
const userId = 9;
const image = await app.models.Image.download(collection, size, userId);
const image = await app.models.Image.download(ctx, collection, size, userId);
const contentType = image[1];
expect(contentType).toEqual('image/png');
@ -14,7 +16,7 @@ describe('image download()', () => {
it(`should return false if the user doesn't have image`, async() => {
const userId = 110;
const image = await app.models.Image.download(collection, size, userId);
const image = await app.models.Image.download(ctx, collection, size, userId);
expect(image).toBeFalse();
});

View File

@ -0,0 +1,129 @@
const app = require('vn-loopback/server/server');
describe('image upload()', () => {
describe('as buyer', () => {
const buyerId = 35;
const workerId = 106;
const itemId = 4;
it('should try to upload a file for the collection "catalog" and throw a privileges error', async() => {
const ctx = {req: {accessToken: {userId: buyerId}},
args: {
id: workerId,
collection: 'user'
}
};
let error;
try {
await app.models.Image.upload(ctx);
} catch (err) {
error = err;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
it('should call to the TempContainer upload method for the collection "catalog"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: buyerId}},
args: {
id: itemId,
collection: 'catalog'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
});
describe('as marketing', () => {
const marketingId = 51;
const workerId = 106;
const itemId = 4;
it('should be able to call to the TempContainer upload method for the collection "user"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: marketingId}},
args: {
id: workerId,
collection: 'user'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
it('should be able to call to the TempContainer upload method for the collection "catalog"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: marketingId}},
args: {
id: itemId,
collection: 'catalog'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
});
describe('as hhrr', () => {
const hhrrId = 37;
const workerId = 106;
const itemId = 4;
it('should upload a file for the collection "user" and call to the TempContainer upload method', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: hhrrId}},
args: {
id: itemId,
collection: 'user'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
it('should try to upload a file for the collection "catalog" and throw a privilege error', async() => {
const ctx = {req: {accessToken: {userId: hhrrId}},
args: {
id: workerId,
collection: 'catalog'
}
};
let error;
try {
await app.models.Image.upload(ctx);
} catch (err) {
error = err;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
});
});

View File

@ -0,0 +1,70 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('upload', {
description: 'Uploads a file and inserts into dms model',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The entity id',
required: true
},
{
arg: 'collection',
type: 'string',
description: 'The collection name',
required: true
},
{
arg: 'fileName',
type: 'string',
description: 'The file name',
required: true
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/upload`,
verb: 'POST'
}
});
Self.upload = async ctx => {
const models = Self.app.models;
const TempContainer = models.TempContainer;
const fileOptions = {};
const args = ctx.args;
let srcFile;
try {
const hasWriteRole = await models.ImageCollection.hasWriteRole(ctx, args.collection);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
if (process.env.NODE_ENV == 'test')
throw new UserError(`You can't upload images on the test instance`);
// Upload file to temporary path
const tempContainer = await TempContainer.container(args.collection);
const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const [uploadedFile] = Object.values(uploaded.files).map(file => {
return file[0];
});
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
await models.Image.registerImage(args.collection, srcFile, args.fileName, args.id);
} catch (e) {
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e;
}
};
};

View File

@ -17,9 +17,6 @@
"Company": {
"dataSource": "vn"
},
"Container": {
"dataSource": "storage"
},
"Continent": {
"dataSource": "vn"
},
@ -35,6 +32,9 @@
"Delivery": {
"dataSource": "vn"
},
"DmsContainer": {
"dataSource": "dmsStorage"
},
"Image": {
"dataSource": "vn"
},
@ -44,12 +44,18 @@
"ImageCollectionSize": {
"dataSource": "vn"
},
"ImageContainer": {
"dataSource": "imageStorage"
},
"Language": {
"dataSource": "vn"
},
"Province": {
"dataSource": "vn"
},
"TempContainer": {
"dataSource": "tempStorage"
},
"UserConfig": {
"dataSource": "vn"
},

View File

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

View File

@ -0,0 +1,10 @@
{
"name": "DmsContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -14,12 +14,12 @@ module.exports = Self => {
};
Self.getFile = async function(id) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const DmsContainer = models.DmsContainer;
const dms = await Self.findById(id);
const pathHash = storageConnector.getPathHash(dms.id);
const pathHash = DmsContainer.getHash(dms.id);
try {
await models.Container.getFile(pathHash, dms.file);
await DmsContainer.getFile(pathHash, dms.file);
} catch (e) {
if (e.code != 'ENOENT')
throw e;
@ -30,7 +30,7 @@ module.exports = Self => {
throw error;
}
const stream = models.Container.downloadStream(pathHash, dms.file);
const stream = DmsContainer.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`];
};

View File

@ -0,0 +1,64 @@
module.exports = Self => {
/**
* Checks if current user has
* read privileges over a collection
*
* @param {object} ctx - Request context
* @param {interger} name - Collection name
* @param {object} options - Query options
* @return {boolean} True for user with read privileges
*/
Self.hasReadRole = async(ctx, name, options) => {
const collection = await Self.findOne({where: {name}}, {
include: {
relation: 'readRole'
}
}, options);
return await hasRole(ctx, collection, options);
};
/**
* Checks if current user has
* write privileges over a collection
*
* @param {object} ctx - Request context
* @param {string} name - Collection name
* @param {object} options - Query options
* @return {boolean} True for user with write privileges
*/
Self.hasWriteRole = async(ctx, name, options) => {
const collection = await Self.findOne({
include: {
relation: 'writeRole'
},
where: {name}
}, options);
return await hasRole(ctx, collection, options);
};
/**
* Checks if current user has
* read or write privileges
* @param {Object} ctx - Context
* @param {Object} collection - Collection [read/write]
* @param {Object} options - Query options
*/
async function hasRole(ctx, collection, options) {
const models = Self.app.models;
const myUserId = ctx.req.accessToken.userId;
const readRole = collection.readRole() && collection.readRole().name;
const writeRole = collection.writeRole() && collection.writeRole().name;
const requiredRole = readRole || writeRole;
const hasRequiredRole = await models.Account.hasRole(myUserId, requiredRole, options);
const isRoot = await models.Account.hasRole(myUserId, 'root', options);
if (isRoot || hasRequiredRole)
return true;
return false;
}
};

View File

@ -48,7 +48,12 @@
"type": "belongsTo",
"model": "Role",
"foreignKey": "readRoleFk"
}
},
"writeRole": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "writeRoleFk"
}
},
"acls": [
{

View File

@ -0,0 +1,10 @@
{
"name": "ImageContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -4,12 +4,9 @@ const path = require('path');
module.exports = Self => {
require('../methods/image/download')(Self);
require('../methods/image/upload')(Self);
Self.getPath = function() {
return '/var/lib/salix/image';
};
Self.registerImage = async(collectionName, file, srcFilePath) => {
Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
const myOptions = {transaction: tx};
@ -33,13 +30,10 @@ module.exports = Self => {
}
}, myOptions);
const fileName = file.split('.')[0];
const rootPath = Self.getPath();
const data = {
name: fileName,
collectionFk: collectionName
};
const newImage = await Self.upsertWithWhere(data, {
name: fileName,
collectionFk: collectionName,
@ -47,7 +41,10 @@ module.exports = Self => {
}, myOptions);
// Resizes and saves the image
const container = await models.ImageContainer.container(collectionName);
const rootPath = container.client.root;
const collectionDir = path.join(rootPath, collectionName);
const file = `${fileName}.png`;
const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file);
@ -83,7 +80,7 @@ module.exports = Self => {
if (!model)
throw new Error('Matching model not found');
const item = await model.findById(fileName, null, myOptions);
const item = await model.findById(entityId, null, myOptions);
if (item) {
await item.updateAttribute(
collection.property,

View File

@ -0,0 +1,10 @@
{
"name": "TempContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) VALUES ('Image', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee')

View File

@ -0,0 +1,13 @@
INSERT INTO account.role (id, name, description)
VALUES
(74, 'userPhotos', 'Privilegios para subir fotos de usuario'),
(75, 'catalogPhotos', 'Privilegios para subir fotos del catálogo');
INSERT INTO account.roleInherit (role, inheritsFrom)
VALUES
(37, (SELECT id FROM account.role WHERE name = 'userPhotos')),
(51, (SELECT id FROM account.role WHERE name = 'userPhotos')),
(51, (SELECT id FROM account.role WHERE name = 'catalogPhotos')),
(35, (SELECT id FROM account.role WHERE name = 'catalogPhotos'));
CALL account.role_sync();

View File

@ -0,0 +1,27 @@
ALTER TABLE `hedera`.`imageCollection`
ADD writeRoleFk INT UNSIGNED NULL DEFAULT 1;
ALTER TABLE `hedera`.`imageCollection`
ADD CONSTRAINT role_id_writeRoleFk
FOREIGN KEY (writeRoleFk) REFERENCES account.role (id)
ON UPDATE CASCADE;
ALTER TABLE `hedera`.`imageCollection` modify readRoleFk INT UNSIGNED default 1 null;
ALTER TABLE `hedera`.`imageCollection`
ADD CONSTRAINT role_id_readRoleFk
FOREIGN KEY (readRoleFk) REFERENCES account.role (id)
ON UPDATE CASCADE;
UPDATE hedera.imageCollection t SET t.writeRoleFk = (
SELECT id FROM `account`.`role` WHERE name = 'catalogPhotos'
)
WHERE t.name = 'catalog';
UPDATE hedera.imageCollection t SET t.writeRoleFk = (
SELECT id FROM `account`.`role` WHERE name = 'userPhotos'
)
WHERE t.name = 'user';
UPDATE hedera.imageCollection t SET t.writeRoleFk = 9
WHERE t.name IN ('link', 'news');

View File

@ -447,6 +447,25 @@ INSERT INTO `imageCollection` VALUES (1,'catalog','Artículo',3840,2160,'Item','
/*!40000 ALTER TABLE `imageCollection` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `imageCollectionSize`
--
LOCK TABLES `imageCollectionSize` WRITE;
/*!40000 ALTER TABLE `imageCollectionSize` DISABLE KEYS */;
INSERT INTO `imageCollectionSize` (`id`, `collectionFk`, `width`, `height`, `crop`)
VALUES
(2, 1, 50, 50, 1),
(3, 1, 200, 200, 1),
(5, 5, 200, 200, 1),
(6, 1, 70, 70, 1),
(8, 5, 50, 50, 1),
(9, 1, 1600, 900, 0),
(13, 6, 160, 160, 1),
(14, 6, 520, 520, 1),
(15, 6, 1600, 1600, 1);
/*!40000 ALTER TABLE `imageCollectionSize` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `tpvError`
--

View File

@ -30,7 +30,7 @@ UPDATE `account`.`role` SET id = 100 WHERE id = 0;
CALL `account`.`role_sync`;
INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`, `image`)
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', 'e7723f0b24ff05b32ed09d95196f2f29'
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd'
FROM `account`.`role` WHERE id <> 20
ORDER BY id;
@ -772,23 +772,23 @@ INSERT INTO `vn`.`intrastat`(`id`, `description`, `taxClassFk`, `taxCodeFk`)
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`)
VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 67, 1, NULL, 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 66, 1, NULL, 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 65, 1, NULL, 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, 69, 2, NULL, 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 74, 2, NULL, 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 62, 2, NULL, 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 64, 2, NULL, 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 75, 1, NULL, 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 76, 1, NULL, 0),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 77, 1, NULL, 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, 78, 2, NULL, 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 82, 2, NULL, 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 83, 2, NULL, 0),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 84, 2, NULL, 0),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 88, 2, NULL, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 88, 2, NULL, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, 88, 2, NULL, 0);
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '1', 1, NULL, 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '2', 1, NULL, 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '3', 1, NULL, 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, '4', 2, NULL, 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '5', 2, NULL, 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '6', 2, NULL, 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '7', 2, NULL, 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '8', 1, NULL, 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '9', 1, NULL, 0),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '10', 1, NULL, 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, '11', 2, NULL, 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '12', 2, NULL, 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '13', 2, NULL, 0),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, '', 2, NULL, 0);
INSERT INTO `vn`.`priceFixed`(`id`, `itemFk`, `rate0`, `rate1`, `rate2`, `rate3`, `started`, `ended`, `bonus`, `warehouseFk`, `created`)
VALUES
@ -2155,7 +2155,7 @@ INSERT INTO `vn`.`campaign`(`code`, `dated`)
INSERT INTO `hedera`.`image`(`collectionFk`, `name`)
VALUES
('user', 'e7723f0b24ff05b32ed09d95196f2f29');
('user', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd');
INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height`)
VALUES

View File

@ -38,7 +38,7 @@ describe('Travel thermograph path', () => {
it('should select the file to upload', async() => {
let currentDir = process.cwd();
let filePath = `${currentDir}/e2e/dms/ecc/3.jpeg`;
let filePath = `${currentDir}/storage/dms/ecc/3.jpeg`;
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),

View File

@ -46,7 +46,7 @@ export function directive($timeout) {
$element.on('click', function(event) {
if (event.defaultPrevented) return;
let src = $attrs.zoomImage || $attrs.src;
let src = $element[0].getAttribute('zoom-image') || $element[0].src;
if (src)
createContainers(src);
else

View File

@ -5,6 +5,24 @@
vn-descriptor-content {
display: block;
.photo {
position: relative;
& > img[ng-src] {
min-height: 16em;
display: block;
height: 100%;
width: 100%;
}
vn-float-button {
position: absolute;
margin: 1em;
bottom: 0;
right: 0
}
}
& > vn-spinner {
display: block;
height: 40px;

View File

@ -13,3 +13,4 @@ import './section';
import './summary';
import './topbar/topbar';
import './user-popover';
import './upload-photo';

View File

@ -109,14 +109,15 @@ vn-layout {
}
}
}
img {
width: 40px;
border-radius: 50%;
}
.buttonAccount {
.buttonAccount {
background: none;
border: none;
}
img {
width: 40px;
border-radius: 50%;
}
}
@media screen and (max-width: $mobile-width) {
& > vn-topbar {
& > .start > .logo {

View File

@ -0,0 +1,39 @@
<vn-dialog class="edit"
vn-id="dialog"
on-accept="$ctrl.onUploadAccept()"
message="Upload new photo">
<tpl-body class="upload-photo">
<vn-horizontal ng-show="file.value" class="photo vn-mb-md">
<div><img vn-id="photo" ng-src=""/></div>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="File name"
ng-model="$ctrl.newPhoto.fileName"
required="true">
</vn-input-file>
</vn-horizontal>
<vn-horizontal>
<vn-input-file vn-id="file"
vn-one
label="File"
ng-model="$ctrl.newPhoto.files"
on-change="$ctrl.updatePhotoPreview(value)"
accept="{{$ctrl.allowedContentTypes}}"
required="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Upload</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,105 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
/**
* Small card with basing entity information and actions.
*/
export default class UploadPhoto extends Component {
/**
* Opens the dialog and sets the default data
* @param {*} collection - Collection name
* @param {*} id - Entity id
*/
show(collection, id) {
this.newPhoto = {
id: id,
collection: collection,
fileName: id
};
this.$.dialog.show();
this.getAllowedContentTypes();
}
getAllowedContentTypes() {
this.$http.get('ImageContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});
}
get contentTypesInfo() {
return this.$t('ContentTypesInfo', {
allowedContentTypes: this.allowedContentTypes
});
}
/**
* Updates the image preview
*
* @param {string} value
*/
updatePhotoPreview(value) {
if (value && value[0]) {
const reader = new FileReader();
reader.onload = e => this.$.photo.src = e.target.result;
reader.readAsDataURL(value[0]);
}
}
/**
* Dialog response handler
*
* @return {boolean} Response
*/
onUploadAccept() {
try {
if (!this.newPhoto.files)
throw new Error(`Select an image`);
this.makeRequest();
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
/**
* Performs a cancellable request.
*
*/
makeRequest() {
if (this.canceler) this.canceler.resolve();
this.canceler = this.$q.defer();
const options = {
method: 'POST',
url: `Images/upload`,
params: this.newPhoto,
headers: {'Content-Type': undefined},
timeout: this.canceler.promise,
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.newPhoto.files
};
this.$http(options)
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
.then(() => this.emit('response'))
.finally(() => this.canceler = null);
}
}
ngModule.vnComponent('vnUploadPhoto', {
controller: UploadPhoto,
template: require('./index.html'),
bindings: {
data: '<'
}
});

View File

@ -0,0 +1,57 @@
import './index.js';
describe('Salix', () => {
describe('Component vnUploadPhoto', () => {
let controller;
let $scope;
let $httpBackend;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-upload-photo></vn-upload-photo>');
controller = $componentController('vnUploadPhoto', {$element, $scope});
controller.newPhoto = {};
}));
afterEach(() => {
$scope.$destroy();
});
describe('onUploadAccept()', () => {
it('should throw an error message containing "Select an image"', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.onUploadAccept();
expect(controller.vnApp.showError).toHaveBeenCalledWith('Select an image');
});
it('should call to the makeRequest() method', () => {
jest.spyOn(controller, 'makeRequest');
controller.newPhoto.files = [0];
controller.onUploadAccept();
expect(controller.makeRequest).toHaveBeenCalledWith();
});
});
describe('makeRequest()', () => {
it('should make an http query and then emit a response event', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller, 'emit');
controller.newPhoto.files = [{name: 'hola'}];
$httpBackend.expectRoute('POST', 'Images/upload').respond(200);
controller.makeRequest();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect(controller.emit).toHaveBeenCalledWith('response');
});
});
});
});

View File

@ -0,0 +1,3 @@
Upload new photo: Subir una nueva foto
Select an image: Selecciona una imagen
File name: Nombre del fichero

View File

@ -0,0 +1,39 @@
@import "./variables";
.upload-photo {
.photo {
position: relative;
margin: 0 auto;
text-align: center;
& > div {
border: 3px solid $color-primary;
max-width: 256px;
max-height: 256px;
border-radius: 50%;
overflow: hidden
}
& > div > img[ng-src] {
width: 256px;
height: 256px;
display: block;
height: 100%;
width: 100%;
}
}
& > vn-spinner {
display: block;
height: 40px;
padding: $spacing-md;
}
vn-input-file {
max-width: 256px;
div.control {
overflow: hidden
}
}
}

View File

@ -14,7 +14,7 @@
<vn-vertical class="user-popover vn-pa-md">
<div class="profile-card vn-pb-md">
<img
ng-src="{{$ctrl.getImageUrl($root.user.id)}}"
ng-src="{{::$root.imagePath('user', '160x160', $root.user.id)}}"
on-error-src/>
<div class="vn-pl-sm">
<div>

View File

@ -78,10 +78,6 @@ class Controller {
this.$.companies.refresh();
this.$.popover.show(event.target);
}
getImageUrl(userId) {
return '/api/Images/user/160x160/' + userId + '/download?access_token=' + this.vnToken.token;
}
}
Controller.$inject = ['$scope', '$translate', 'vnConfig', 'vnAuth', 'vnToken'];

View File

@ -2,14 +2,16 @@ import './index.js';
describe('Salix', () => {
describe('Component vnUserPopover', () => {
const userId = 9;
let controller;
let $scope;
let $root;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, $httpBackend) => {
$httpBackend.expectGET('UserConfigs/getUserConfig');
$root = $rootScope;
$scope = $rootScope.$new();
controller = $componentController('vnUserPopover', {$scope});
}));
@ -60,9 +62,10 @@ describe('Salix', () => {
describe('getImageUrl()', () => {
it('should return de url image', () => {
const url = controller.getImageUrl();
const url = $root.imagePath('user', '160x160', userId);
expect(url).toBeDefined();
expect(url).toEqual(`/api/Images/user/160x160/${userId}/download?access_token=null`);
});
});
});

View File

@ -7,9 +7,14 @@ export const appName = 'salix';
const ngModule = ng.module('salix', ['vnCore']);
export default ngModule;
run.$inject = ['$window', '$rootScope', 'vnAuth', 'vnApp', '$state'];
export function run($window, $rootScope, vnAuth, vnApp, $state) {
$rootScope.imagePath = appConfig.imagePath;
run.$inject = ['$window', '$rootScope', 'vnAuth', 'vnApp', 'vnToken', '$state'];
export function run($window, $rootScope, vnAuth, vnApp, vnToken, $state) {
$rootScope.imagePath = (collection, size, id) => {
if (!collection || !size || !id) return;
const basePath = `/api/Images/${collection}/${size}/${id}`;
return `${basePath}/download?access_token=${vnToken.token}`;
};
$window.validations = {};
vnApp.name = appName;

View File

@ -0,0 +1,57 @@
const md5 = require('md5');
module.exports = function(Self) {
Self.setup = function() {
Self.super_.setup.call(this);
this.remoteMethod('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
};
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
Self.container = async function(name) {
const models = Self.app.models;
let container;
try {
container = await models[this.modelName].getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models[this.modelName].createContainer({
name: name
});
} else throw err;
}
return container;
};
Self.getHash = function(id) {
return md5(id.toString()).substring(0, 3);
};
Self.getFileExtension = function(fileName) {
return fileName.split('.').pop().toLowerCase();
};
Self.allowedContentTypes = async function() {
const connector = this.dataSource.connector;
const allowedContentTypes = connector.allowedContentTypes;
return allowedContentTypes;
};
};

View File

@ -0,0 +1,12 @@
{
"name": "Container",
"base": "VnModel",
"acls": [
{
"property": "status",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -162,5 +162,6 @@
"You need to fill sage information before you check verified data": "Debes rellenar la información de sage antes de marcar datos comprobados",
"ASSIGN_ZONE_FIRST": "Asigna una zona primero",
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas",
"Sorts whole route": "Reordena ruta entera"
}

View File

@ -1,18 +0,0 @@
const uuid = require('uuid/v1');
const md5 = require('md5');
module.exports = app => {
const storageConnector = app.dataSources.storage.connector;
storageConnector.getFilename = function(file) {
return `${uuid()}.${storageConnector.getFileExtension(file.name)}`;
};
storageConnector.getFileExtension = function(fileName) {
return fileName.split('.').pop().toLowerCase();
};
storageConnector.getPathHash = function(id) {
return md5(id.toString()).substring(0, 3);
};
};

View File

@ -17,11 +17,11 @@
"connectTimeout": 40000,
"acquireTimeout": 20000
},
"storage": {
"name": "storage",
"tempStorage": {
"name": "tempStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./e2e/dms",
"root": "./storage/tmp",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
@ -36,5 +36,37 @@
"image/jpeg",
"image/jpg"
]
},
"dmsStorage": {
"name": "dmsStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"image/jpeg",
"image/jpg"
]
},
"imageStorage": {
"name": "imageStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/image",
"maxFileSize": "52428800",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg"
]
}
}

View File

@ -49,5 +49,8 @@
},
"Application": {
"dataSource": "vn"
},
"Container": {
"dataSource": "vn"
}
}

View File

@ -13,7 +13,7 @@ module.exports = Self => {
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const storageConnector = Self.app.dataSources.dmsStorage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

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

View File

@ -26,7 +26,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('clientDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -62,7 +62,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `clientDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('clientDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -69,7 +69,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `clientDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -70,8 +70,8 @@
</vn-td>
<vn-td shrink >
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::buy.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::buy.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', buy.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', buy.itemFk)}}"
vn-click-stop
on-error-src/>
</vn-td>

View File

@ -33,8 +33,10 @@ module.exports = Self => {
// Exit loop
if (!image) return clearInterval(timer);
const fileName = `${image.itemFk}.png`;
const filePath = path.join(tempPath, fileName);
const srcFile = image.url.split('/').pop();
const fileName = srcFile.split('.')[0];
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);
const writeStream = fs.createWriteStream(filePath);
writeStream.on('open', () => {
@ -57,7 +59,7 @@ module.exports = Self => {
writeStream.on('finish', async function() {
try {
await models.Image.registerImage('catalog', fileName, filePath);
await models.Image.registerImage('catalog', filePath, fileName, image.itemFk);
await image.destroy();
} catch (error) {
await errorHandler(image.itemFk, error, filePath);

View File

@ -1,5 +1,5 @@
<vn-portal slot="menu">
<vn-item-descriptor item="$ctrl.item"></vn-item-descriptor>
<vn-item-descriptor item="$ctrl.item" card-reload="$ctrl.reload()"></vn-item-descriptor>
<vn-left-menu source="card"></vn-left-menu>
</vn-portal>
<ui-view></ui-view>

View File

@ -16,19 +16,15 @@
</vn-item>
</slot-menu>
<slot-before>
<div style="position: relative" text-center>
<img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{$ctrl.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{$ctrl.item.image}}"
on-error-src
/>
<a href="//verdnatura.es/#!form=admin/items&filter={{$ctrl.item.id}}" target="_blank">
<vn-float-button
icon="edit"
style="position: absolute; margin: 1em; bottom: 0; right: 0;"
vn-visible-by="marketing, buyer">
</vn-float-button>
</a>
<div class="photo" text-center>
<img vn-id="photo"
ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}"
zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}"
on-error-src/>
<vn-float-button ng-click="uploadPhoto.show('catalog', $ctrl.item.id)"
icon="edit"
vn-visible-by="catalogPhotos">
</vn-float-button>
</div>
<vn-horizontal class="item-state">
<vn-one>
@ -102,4 +98,10 @@
</vn-confirm>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
</vn-worker-descriptor-popover>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>

View File

@ -3,6 +3,11 @@ import Descriptor from 'salix/components/descriptor';
import './style.scss';
class Controller extends Descriptor {
constructor($element, $, $rootScope) {
super($element, $);
this.$rootScope = $rootScope;
}
get item() {
return this.entity;
}
@ -65,13 +70,27 @@ class Controller extends Descriptor {
this.$http.post(`Items/${this.item.id}/clone`)
.then(res => this.$state.go('item.card.tags', {id: res.data.id}));
}
onUploadResponse() {
const timestamp = new Date().getTime();
const src = this.$rootScope.imagePath('catalog', '200x200', this.item.id);
const zoomSrc = this.$rootScope.imagePath('catalog', '1600x900', this.item.id);
const newSrc = `${src}&t=${timestamp}`;
const newZoomSrc = `${zoomSrc}&t=${timestamp}`;
this.$.photo.setAttribute('src', newSrc);
this.$.photo.setAttribute('zoom-image', newZoomSrc);
}
}
Controller.$inject = ['$element', '$scope', '$rootScope'];
ngModule.vnComponent('vnItemDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
item: '<',
dated: '<'
dated: '<',
cardReload: '&'
}
});

View File

@ -35,8 +35,8 @@
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::item.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
@ -44,7 +44,7 @@
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id | zeroFill:6}}
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>

View File

@ -11,8 +11,8 @@
<vn-horizontal>
<vn-one>
<img style="width: 100%; display: block;"
ng-src="{{::$root.imagePath}}/catalog/200x200/{{$ctrl.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{$ctrl.item.image}}" on-error-src/>
ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}"
zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}" on-error-src/>
<vn-horizontal class="item-state">
<vn-one>
<p translate>Visible</p>

View File

@ -5,11 +5,11 @@
<vn-card>
<div class="image">
<div ng-if="::item.hex != null" class="item-color-background">
<div class="item-color" style="background-color: #{{::item.hex}}"></div>
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div>
<img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{::item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::item.image}}"
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/>
</div>
<div class="description">

View File

@ -32,8 +32,8 @@
<vn-tr ng-repeat="row in $ctrl.rows">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::row.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::row.item.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', row.item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', row.item.id)}}"
on-error-src/>
</vn-td>
<vn-td number>

View File

@ -26,6 +26,7 @@
label="Vehicle"
ng-model="$ctrl.route.vehicleFk"
url="Vehicles"
where="{warehouseFk: $ctrl.vnConfig.warehouseFk}"
show-field="numberPlate">
</vn-autocomplete>
<vn-autocomplete

View File

@ -0,0 +1,168 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethodCtx('consumption', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
}, {
arg: 'search',
type: 'String',
description: `If it's and integer searchs by id, otherwise it searchs by name`
}, {
arg: 'itemId',
type: 'Number',
description: 'Item id'
}, {
arg: 'categoryId',
type: 'Number',
description: 'Category id'
}, {
arg: 'typeId',
type: 'Number',
description: 'Item type id',
}, {
arg: 'buyerId',
type: 'Number',
description: 'Buyer id'
}, {
arg: 'from',
type: 'Date',
description: `The from date filter`
}, {
arg: 'to',
type: 'Date',
description: `The to date filter`
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/consumption`,
verb: 'GET'
}
});
Self.consumption = async(ctx, filter) => {
const conn = Self.dataSource.connector;
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'i.id': value}
: {'i.name': {like: `%${value}%`}};
case 'itemId':
return {'i.id': value};
case 'description':
return {'i.description': {like: `%${value}%`}};
case 'categoryId':
return {'it.categoryFk': value};
case 'typeId':
return {'it.id': value};
case 'buyerId':
return {'it.workerFk': value};
case 'from':
return {'t.shipped': {gte: value}};
case 'to':
return {'t.shipped': {lte: value}};
}
});
filter = mergeFilters(filter, {where});
let stmts = [];
let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.entry');
stmt = new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.entry
(INDEX (id))
ENGINE = MEMORY
SELECT
e.id,
e.ref,
e.supplierFk,
t.shipped
FROM vn.entry e
JOIN vn.travel t ON t.id = e.travelFk
JOIN buy b ON b.id = b.entryFk
JOIN item i ON i.id = b.itemFk
JOIN itemType it ON it.id = i.typeFk`);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makeGroupBy('e.id'));
stmt.merge(conn.makeLimit(filter));
stmts.push(stmt);
const entriesIndex = stmts.push('SELECT * FROM tmp.entry') - 1;
stmt = new ParameterizedSQL(
`SELECT
b.id AS buyId,
b.itemFk,
b.entryFk,
b.quantity,
CAST(b.buyingValue AS DECIMAL(10,2)) AS price,
CAST(SUM(b.buyingValue*b.quantity)AS DECIMAL(10,2)) AS total,
i.id,
i.description,
i.name AS itemName,
i.subName,
i.size AS itemSize,
i.typeFk AS itemTypeFk,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
i.tag8,
i.value8,
i.tag9,
i.value9,
i.tag10,
i.value10,
it.id,
it.workerFk,
it.categoryFk,
it.code AS itemTypeCode
FROM buy b
JOIN tmp.entry e ON e.id = b.entryFk
JOIN item i ON i.id = b.itemFk
JOIN itemType it ON it.id = i.typeFk`
);
stmt.merge('WHERE b.quantity > 0');
stmt.merge(conn.makeGroupBy('b.id'));
stmt.merge(conn.makeOrderBy(filter.order));
const buysIndex = stmts.push(stmt) - 1;
stmts.push(`DROP TEMPORARY TABLE tmp.entry`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
const entries = result[entriesIndex];
const buys = result[buysIndex];
const entriesMap = new Map();
for (let entry of entries)
entriesMap.set(entry.id, entry);
for (let buy of buys) {
const entry = entriesMap.get(buy.entryFk);
if (entry) {
if (!entry.buys) entry.buys = [];
entry.buys.push(buy);
}
}
return entries;
};
};

View File

@ -0,0 +1,36 @@
const app = require('vn-loopback/server/server');
describe('supplier consumption() filter', () => {
it('should return a list of entries from the supplier 2', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {}};
const filter = {
where: {
supplierFk: 2
},
order: 'itemTypeFk, itemName, itemSize'
};
const result = await app.models.Supplier.consumption(ctx, filter);
expect(result.length).toEqual(6);
});
it('should return a list of entries from the item id 1 and supplier 1', async() => {
const ctx = {req: {accessToken: {userId: 9}},
args: {
itemId: 1
}
};
const filter = {
where: {
supplierFk: 1
},
order: 'itemTypeFk, itemName, itemSize'
};
const result = await app.models.Supplier.consumption(ctx, filter);
const expectedItemId = 1;
const firstRowBuys = result[0].buys[0];
expect(firstRowBuys.itemFk).toEqual(expectedItemId);
});
});

View File

@ -5,6 +5,7 @@ module.exports = Self => {
require('../methods/supplier/filter')(Self);
require('../methods/supplier/getSummary')(Self);
require('../methods/supplier/updateFiscalData')(Self);
require('../methods/supplier/consumption')(Self);
Self.validatesPresenceOf('name', {
message: 'The social name cannot be empty'

View File

@ -0,0 +1,68 @@
<div class="search-panel">
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-textfield vn-focus
vn-one
label="General search"
ng-model="filter.search"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Item id"
ng-model="filter.itemId">
</vn-textfield>
<vn-autocomplete
vn-one
ng-model="filter.buyerId"
url="Clients/activeWorkersWithRole"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'employee'}"
label="Buyer">
<tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="filter.typeId"
url="ItemTypes"
show-field="name"
value-field="id"
label="Type"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{category.name}}
</div>
</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-one
url="ItemCategories"
label="Category"
show-field="name"
value-field="id"
ng-model="filter.categoryId">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From"
ng-model="filter.from">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="filter.to">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
ngModule.vnComponent('vnSupplierConsumptionSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
});

View File

@ -0,0 +1,7 @@
Item id: Id artículo
From: Desde
To: Hasta
Campaign: Campaña
allSaints: Día de todos los Santos
valentinesDay: Día de San Valentín
mothersDay: Día de la madre

View File

@ -0,0 +1,84 @@
<vn-crud-model vn-id="model"
url="Suppliers/consumption"
link="{supplierFk: $ctrl.$params.id}"
limit="20"
user-params="::$ctrl.filterParams"
data="entries"
order="itemTypeFk, itemName, itemSize">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
panel="vn-supplier-consumption-search-panel"
suggested-filter="$ctrl.filterParams"
info="Search by item id or name"
model="model"
auto-state="false">
</vn-searchbar>
</vn-portal>
<vn-data-viewer model="model">
<vn-card class="vn-pa-lg vn-w-lg">
<section class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button disabled="!model.userParams.from || !model.userParams.to"
icon="picture_as_pdf"
ng-click="$ctrl.showReport()"
vn-tooltip="Open as PDF">
</vn-button>
<vn-button disabled="!model.userParams.from || !model.userParams.to"
icon="email"
ng-click="confirm.show()"
vn-tooltip="Send to email">
</vn-button>
</vn-tool-bar>
</section>
<vn-table model="model"
ng-repeat="entry in entries"
ng-if="entry.buys">
<vn-thead>
<vn-tr>
<vn-th field="entryFk" expand>Entry </vn-th>
<vn-td expand>{{::entry.id}}</vn-td>
<vn-th field="data">Date</vn-th>
<vn-td>{{::entry.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-th field="ref">Reference</vn-th>
<vn-td vn-tooltip="{{::entry.ref}}">{{::entry.ref}}</vn-td>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="buy in entry.buys">
<vn-td expand>
<span>
{{::buy.itemName}}
</span>
</vn-td>
<vn-td expand>
<vn-fetched-tags
max-length="6"
item="::buy">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::buy.quantity | dashIfEmpty}}</vn-td>
<vn-td number>{{::buy.price | dashIfEmpty}}</vn-td>
<vn-td number>{{::buy.total | dashIfEmpty}}</vn-td>
<vn-td></vn-td>
</vn-tr>
</vn-tbody>
<vn-tfoot>
<vn-tr>
<vn-td>
<vn-label-value
label="Total entry"
value="{{$ctrl.getTotal(entry)}}">
</vn-label-value>
</vn-td>
</vn-tr>
</vn-tfoot>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-confirm
vn-id="confirm"
question="Please, confirm"
message="The consumption report will be sent"
on-accept="$ctrl.sendEmail()">
</vn-confirm>

View File

@ -0,0 +1,65 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $, vnReport, vnEmail) {
super($element, $);
this.vnReport = vnReport;
this.vnEmail = vnEmail;
this.setDefaultFilter();
}
setDefaultFilter() {
const minDate = new Date();
minDate.setHours(0, 0, 0, 0);
minDate.setMonth(minDate.getMonth() - 2);
const maxDate = new Date();
maxDate.setHours(23, 59, 59, 59);
this.filterParams = {
from: minDate,
to: maxDate
};
}
get reportParams() {
const userParams = this.$.model.userParams;
return Object.assign({
authorization: this.vnToken.token,
recipientId: this.supplier.id
}, userParams);
}
showReport() {
this.vnReport.show('supplier-campaign-metrics', this.reportParams);
}
sendEmail() {
this.vnEmail.send('supplier-campaign-metrics', this.reportParams);
}
getTotal(entry) {
if (entry.buys) {
let total = 0;
for (let buy of entry.buys)
total += buy.total;
return total;
}
}
}
Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];
ngModule.vnComponent('vnSupplierConsumption', {
template: require('./index.html'),
controller: Controller,
bindings: {
supplier: '<'
},
require: {
card: '^vnSupplierCard'
}
});

View File

@ -0,0 +1,72 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Supplier', () => {
describe('Component vnSupplierConsumption', () => {
let $scope;
let controller;
let $httpParamSerializer;
let $httpBackend;
beforeEach(ngModule('supplier'));
beforeEach(inject(($componentController, $rootScope, _$httpParamSerializer_, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpParamSerializer = _$httpParamSerializer_;
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-supplier-consumption></vn-supplier-consumption');
controller = $componentController('vnSupplierConsumption', {$element, $scope});
controller.$.model = crudModel;
controller.supplier = {
id: 2
};
}));
describe('showReport()', () => {
it('should call the window.open function', () => {
jest.spyOn(window, 'open').mockReturnThis();
const now = new Date();
controller.$.model.userParams = {
from: now,
to: now
};
controller.showReport();
const expectedParams = {
recipientId: 2,
from: now,
to: now
};
const serializedParams = $httpParamSerializer(expectedParams);
const path = `api/report/supplier-campaign-metrics?${serializedParams}`;
expect(window.open).toHaveBeenCalledWith(path);
});
});
describe('sendEmail()', () => {
it('should make a GET query sending the report', () => {
const now = new Date();
controller.$.model.userParams = {
from: now,
to: now
};
const expectedParams = {
recipientId: 2,
from: now,
to: now
};
const serializedParams = $httpParamSerializer(expectedParams);
const path = `email/supplier-campaign-metrics?${serializedParams}`;
$httpBackend.expect('GET', path).respond({});
controller.sendEmail();
$httpBackend.flush();
});
});
});
});

View File

@ -0,0 +1,2 @@
Total entry: Total entrada

View File

@ -10,4 +10,6 @@ import './basic-data';
import './fiscal-data';
import './contact';
import './log';
import './consumption';
import './consumption-search-panel';
import './billing-data';

View File

@ -13,7 +13,8 @@
{"state": "supplier.card.fiscalData", "icon": "account_balance"},
{"state": "supplier.card.billingData", "icon": "icon-payment"},
{"state": "supplier.card.contact", "icon": "contact_phone"},
{"state": "supplier.card.log", "icon": "history"}
{"state": "supplier.card.log", "icon": "history"},
{"state": "supplier.card.consumption", "icon": "show_chart"}
]
},
"routes": [
@ -77,6 +78,15 @@
"supplier": "$ctrl.supplier"
}
},
{
"url": "/consumption?q",
"state": "supplier.card.consumption",
"component": "vn-supplier-consumption",
"description": "Consumption",
"params": {
"supplier": "$ctrl.supplier"
}
},
{
"url": "/billing-data",
"state": "supplier.card.billingData",

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

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

View File

@ -25,7 +25,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('ticketDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -67,7 +67,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `ticketDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -16,7 +16,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('ticketDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -69,7 +69,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `ticketDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -19,8 +19,8 @@
<vn-card>
<div class="image">
<img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{::sale.item.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::sale.item.image}}"
ng-src="{{::$root.imagePath('catalog', '200x200', sale.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/>
</div>
<div class="description">

View File

@ -93,8 +93,8 @@
</vn-td>
<vn-td shrink>
<img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{sale.image}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{sale.image}}"
ng-src="{{::$root.imagePath('catalog', '50x50', sale.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/>
</vn-td>
<vn-td number>

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,5 +1,4 @@
module.exports = Self => {
require('../methods/travel-thermograph/allowedContentTypes')(Self);
require('../methods/travel-thermograph/getThermographTemperatures')(Self);
};

View File

@ -21,7 +21,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});
@ -46,7 +46,7 @@ class Controller extends Section {
warehouseId: warehouseId,
companyId: companyId,
dmsTypeId: dmsTypeId,
description: this.$t('FileDescription', {
description: this.$t('TravelFileDescription', {
travelId: this.travel.id
}).toUpperCase()
};

View File

@ -53,7 +53,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['application/pdf', 'image/png', 'image/jpg'];
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -79,7 +79,7 @@ describe('Worker', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -8,7 +8,7 @@ Upload file: Subir fichero
Edit file: Editar fichero
Upload: Subir
File: Fichero
FileDescription: Travel id {{travelId}}
TravelFileDescription: Travel id {{travelId}}
ContentTypesInfo: 'Tipos de archivo permitidos: {{allowedContentTypes}}'
Are you sure you want to continue?: ¿Seguro que quieres continuar?
Add thermograph: Añadir termógrafo

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,7 +1,6 @@
module.exports = Self => {
require('../methods/worker-dms/downloadFile')(Self);
require('../methods/worker-dms/removeFile')(Self);
require('../methods/worker-dms/allowedContentTypes')(Self);
require('../methods/worker-dms/filter')(Self);
Self.isMine = async function(ctx, dmsId) {

View File

@ -1,6 +1,18 @@
<vn-descriptor-content
module="worker"
description="$ctrl.worker.firstName +' '+ $ctrl.worker.lastName">
<slot-before>
<div class="photo" text-center>
<img vn-id="photo"
ng-src="{{$root.imagePath('user', '520x520', $ctrl.worker.id)}}"
zoom-image="{{$root.imagePath('user', '1600x1600', $ctrl.worker.id)}}"
on-error-src/>
<vn-float-button ng-click="uploadPhoto.show('user', $ctrl.worker.id)"
icon="edit"
vn-visible-by="userPhotos">
</vn-float-button>
</div>
</slot-before>
<slot-body>
<div class="attributes">
<vn-label-value
@ -42,4 +54,10 @@
<div ng-transclude="btnThree"></div>
</div>
</slot-body>
</vn-descriptor-content>
</vn-descriptor-content>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor {
constructor($element, $, $rootScope) {
super($element, $);
this.$rootScope = $rootScope;
}
get worker() {
return this.entity;
}
@ -48,8 +53,21 @@ class Controller extends Descriptor {
return this.getData(`Workers/${this.id}`, {filter})
.then(res => this.entity = res.data);
}
onUploadResponse() {
const timestamp = new Date().getTime();
const src = this.$rootScope.imagePath('user', '520x520', this.worker.id);
const zoomSrc = this.$rootScope.imagePath('user', '1600x900', this.worker.id);
const newSrc = `${src}&t=${timestamp}`;
const newZoomSrc = `${zoomSrc}&t=${timestamp}`;
this.$.photo.setAttribute('src', newSrc);
this.$.photo.setAttribute('zoom-image', newZoomSrc);
}
}
Controller.$inject = ['$element', '$scope', '$rootScope'];
ngModule.vnComponent('vnWorkerDescriptor', {
template: require('./index.html'),
controller: Controller,

View File

@ -26,7 +26,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('workerDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -64,7 +64,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `workerDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
}
getAllowedContentTypes() {
this.$http.get('WorkerDms/allowedContentTypes').then(res => {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});

View File

@ -70,7 +70,7 @@ describe('Worker', () => {
describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `WorkerDms/allowedContentTypes`).respond(expectedResponse);
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes();
$httpBackend.flush();

View File

@ -60,7 +60,7 @@
</vn-icon-button>
</vn-td>
<vn-td shrink>
<vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})"
<vn-icon-button ui-sref="worker.card.dms.edit({dmsId: {{::document.dmsFk}}})"
icon="edit"
title="{{'Edit file' | translate}}">
</vn-icon-button>

View File

@ -43,6 +43,6 @@
"pool": true
},
"storage": {
"root": "./e2e/dms"
"root": "./storage/dms"
}
}

View File

@ -0,0 +1,8 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,6 @@
[
{
"filename": "supplier-campaign-metrics.pdf",
"component": "supplier-campaign-metrics"
}
]

View File

@ -0,0 +1,8 @@
subject: Informe de consumo
title: Informe de consumo
dear: Estimado cliente
description: Tal y como nos ha solicitado nos complace
relacionarle a continuación el consumo que nos consta en su cuenta para las
fechas comprendidas entre <strong>{0}</strong> y <strong>{1}</strong>.
Espero le sea de utilidad para preparar su pedido.<br/><br/>
Al mismo tiempo aprovecho la ocasión para saludarle cordialmente.

Some files were not shown because too many files have changed in this diff Show More