This commit is contained in:
Javi Gallego 2020-01-21 14:00:25 +01:00
commit 5978a4d6b7
2659 changed files with 176574 additions and 125831 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
print/node_modules
front/node_modules
services

View File

@ -1,6 +0,0 @@
{
"salixHost": "localhost",
"salixPort": "3306",
"salixUser": "root",
"salixPassword": "root"
}

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -1,12 +1,12 @@
extends: [eslint:recommended, google, plugin:jasmine/recommended]
parserOptions:
ecmaVersion: 2017
ecmaVersion: 2018
sourceType: "module"
plugins:
- jasmine
env:
jasmine: true
rules:
indent: [error, 4]
require-jsdoc: 0
no-undef: 0
max-len: 0
@ -20,3 +20,15 @@ rules:
no-console: 0
no-warning-comments: 0
no-empty: [error, allowEmptyCatch: true]
complexity: 0
max-depth: 0
comma-dangle: 0
bracketSpacing: 0
space-infix-ops: 1
no-invalid-this: 0
space-before-function-paren: [error, never]
prefer-const: 0
curly: [error, multi-or-nest]
indent: [error, 4]
arrow-parens: [error, as-needed]
jasmine/no-focused-tests: 0

13
.gitignore vendored
View File

@ -1,4 +1,13 @@
coverage
node_modules
build
dist
e2e/dms/*/
!e2e/dms/c4c
!e2e/dms/c81
!e2e/dms/ecc
npm-debug.log
docker-compose.yml
.eslintcache
datasources.*.json
print.*.json
db.json
junit.xml

8
.vscode/launch.json vendored
View File

@ -4,7 +4,13 @@
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"name": "Attach",
"restart": true,
"timeout": 50000
}, {
"type": "node",
"request": "attach",
"name": "Attach by process ID",
"processId": "${command:PickProcess}"
}
]

View File

@ -2,5 +2,9 @@
{
// Carácter predeterminado de final de línea.
"files.eol": "\n",
"vsicons.presets.angular": false
"vsicons.presets.angular": false,
"eslint.autoFixOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

View File

@ -1,13 +1,38 @@
FROM node:8.9.4
FROM debian:stretch-slim
ENV TZ Europe/Madrid
COPY . /app
COPY ../loopback /loopback
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
ca-certificates \
gnupg2 \
libfontconfig \
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
&& apt-get install -y --no-install-recommends \
nodejs \
&& apt-get purge -y --auto-remove \
gnupg2 \
&& rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2
WORKDIR /app
WORKDIR /salix
COPY package.json package-lock.json ./
COPY loopback/package.json loopback/
COPY print/package.json print/
RUN npm install --only=prod
RUN npm --prefix ./print install --only=prod ./print
RUN npm install
RUN npm -g install pm2
COPY loopback loopback
COPY back back
COPY modules modules
COPY print print
COPY \
LICENSE \
README.md \
./
CMD ["pm2-docker", "./server/server.js"]
CMD ["pm2-runtime", "./back/process.yml"]
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=10s \
CMD curl -f http://localhost:3000/api/Applications/status || exit 1

170
Jenkinsfile vendored
View File

@ -1,60 +1,146 @@
#!/usr/bin/env groovy
def branchName = "${env.BRANCH_NAME}";
def branchProduction = "master"
def branchTest = "test";
env.BRANCH_NAME = branchName;
env.TAG = "${env.BUILD_NUMBER}";
env.salixUser="${env.salixUser}";
env.salixPassword="${env.salixPassword}";
env.salixHost = "${env.productionSalixHost}";
env.salixPort = "${env.productionSalixPort}";
switch (branchName){
case branchTest:
env.NODE_ENV = "test";
env.salixHost = "${env.testSalixHost}";
env.salixPort = "${env.testSalixPort}";
break;
case branchProduction:
env.DOCKER_HOST = "tcp://172.16.255.29:2375";
env.NODE_ENV = "production"
break;
pipeline {
agent any
options {
disableConcurrentBuilds()
}
node
{
stage ('Print environment variables'){
echo "Branch ${branchName}, Build ${env.TAG}, salixHost ${env.salixHost}, NODE_ENV ${env.NODE_ENV} en docker Host ${env.DOCKER_HOST}"
environment {
PROJECT_NAME = 'salix'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
}
stages {
stage('Checkout') {
checkout scm
steps {
script {
if (!env.GIT_COMMITTER_EMAIL) {
env.COMMITTER_EMAIL = sh(
script: 'git --no-pager show -s --format="%ae"',
returnStdout: true
).trim()
} else {
env.COMMITTER_EMAIL = env.GIT_COMMITTER_EMAIL;
}
stage ('install modules'){
sh "npm install"
switch (env.BRANCH_NAME) {
case 'master':
env.NODE_ENV = 'production'
break
case 'test':
env.NODE_ENV = 'test'
break
}
}
stage ('build Project'){
sh "gulp build"
configFileProvider([
configFile(fileId: "salix.groovy",
variable: 'GROOVY_FILE')
]) {
load env.GROOVY_FILE
}
stage ("docker")
{
stage ("install modules loopback service")
{
sh "cd ./services/loopback && npm install"
sh 'printenv'
}
}
stage('Install') {
environment {
NODE_ENV = ""
}
steps {
nodejs('node-lts') {
sh 'npm install --no-audit --prefer-offline'
sh 'gulp install --ci'
}
}
}
stage('Test') {
environment {
NODE_ENV = ""
}
parallel {
stage('Frontend') {
steps {
nodejs('node-lts') {
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=1'
}
}
}
stage('Backend') {
steps {
nodejs('node-lts') {
sh 'gulp backTestDockerOnce --junit --random'
}
}
}
}
}
stage('Build') {
when { anyOf {
branch 'test'
branch 'master'
}}
environment {
CREDS = credentials('docker-registry')
}
steps {
nodejs('node-lts') {
sh 'gulp build'
}
stage ("Stopping/Removing Docker")
{
sh "docker-compose down --rmi 'all'"
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh 'docker-compose build --parallel'
sh 'docker-compose push'
}
}
stage('Deploy') {
when { anyOf {
branch 'test'
branch 'master'
}}
steps {
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
}
}
stage('Database') {
when { anyOf {
branch 'test'
branch 'master'
}}
steps {
configFileProvider([
configFile(fileId: "config.${env.NODE_ENV}.ini",
variable: 'MYSQL_CONFIG')
]) {
sh 'cp "$MYSQL_CONFIG" db/config.$NODE_ENV.ini'
}
stage ("Generar dockers")
{
sh "docker-compose up -d --build"
sh 'db/import-changes.sh -f $NODE_ENV'
}
}
}
post {
always {
script {
if (!['master', 'test'].contains(env.BRANCH_NAME)) {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
}
}
if (!env.COMMITTER_EMAIL) return
try {
mail(
to: env.COMMITTER_EMAIL,
subject: "Pipeline: ${env.JOB_NAME} (${env.BUILD_NUMBER}): ${currentBuild.currentResult}",
body: "Check status at ${env.BUILD_URL}"
)
} catch (e) {
echo e.toString()
}
}
}
}
}

View File

@ -8,13 +8,24 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications.
* Node.js = 8.9.4
* NGINX
* Visual Studio Code
* Node.js = 10.15.3 LTS
* Docker
In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command.
```
ext install dbaeumer.vscode-eslint
```
You will need to install globally the following items.
```
$ npm install -g karma-cli gulp webpack nodemon
# npm install -g jest gulp-cli nodemon
```
## Linux Only Prerequisites
Your user must be on the docker group to use it so you will need to run this command:
```
$ sudo usermod -a -G docker yourusername
```
## Getting Started // Installing
@ -32,12 +43,6 @@ Launch application in developer environment.
$ gulp
```
Also you can run backend and frontend as separately gulp tasks (including NGINX).
```
$ gulp client
$ gulp services
```
Manually reset fixtures.
```
$ gulp docker
@ -47,12 +52,12 @@ $ gulp docker
For client-side unit tests run from project's root.
```
$ karma start
$ jest
```
For server-side unit tests run from project's root.
```
$ npm run test
$ gulp backTest
```
For end-to-end tests run from project's root.
@ -68,6 +73,6 @@ $ gulp e2e
* [loopback](https://loopback.io/)
* [docker](https://www.docker.com/)
* [gulp.js](https://gulpjs.com/)
* [Karma](https://karma-runner.github.io/)
* [jest](https://jestjs.io/)
* [Jasmine](https://jasmine.github.io/)
* [Nightmare](http://www.nightmarejs.org/)

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@babel/preset-env',
],
};

View File

@ -0,0 +1,45 @@
module.exports = Self => {
Self.remoteMethod('acl', {
description: 'Get the user information and permissions',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
}
],
returns: {
type: 'Object',
root: true
},
http: {
path: `/acl`,
verb: 'GET'
}
});
Self.acl = async function(ctx) {
let userId = ctx.req.accessToken.userId;
let models = Self.app.models;
let user = await models.Account.findById(userId, {
fields: ['id', 'name', 'nickname', 'email']
});
let roles = await models.RoleMapping.find({
fields: ['roleId'],
where: {
principalId: userId,
principalType: 'USER'
},
include: [{
relation: 'role',
scope: {
fields: ['name']
}
}]
});
return {roles, user};
};
};

View File

@ -0,0 +1,67 @@
const md5 = require('md5');
module.exports = Self => {
Self.remoteMethod('login', {
description: 'Login a user with username/email and password',
accepts: [
{
arg: 'user',
type: 'String',
description: 'The user name or email',
required: true
}, {
arg: 'password',
type: 'String',
description: 'The user name or email'
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/login`,
verb: 'POST'
}
});
Self.login = async function(user, password) {
let token;
let usesEmail = user.indexOf('@') !== -1;
let User = Self.app.models.User;
let loginInfo = {password};
if (usesEmail)
loginInfo.email = user;
else
loginInfo.username = user;
try {
token = await User.login(loginInfo, 'user');
} catch (err) {
if (err.code != 'LOGIN_FAILED' || usesEmail)
throw err;
let filter = {where: {name: user}};
let instance = await Self.findOne(filter);
if (!instance || instance.password !== md5(password || ''))
throw err;
let where = {id: instance.id};
let userData = {
id: instance.id,
username: user,
password: password,
email: instance.email,
created: instance.created,
updated: instance.updated
};
await User.upsertWithWhere(where, userData);
token = await User.login(loginInfo, 'user');
}
return {token: token.id};
};
};

View File

@ -0,0 +1,25 @@
module.exports = Self => {
Self.remoteMethod('logout', {
description: 'Logout a user with access token',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
}
],
returns: {
type: 'Boolean',
root: true
},
http: {
path: `/logout`,
verb: 'POST'
}
});
Self.logout = async function(ctx) {
await Self.app.models.User.logout(ctx.req.accessToken.id);
return true;
};
};

View File

@ -0,0 +1,33 @@
const app = require('vn-loopback/server/server');
describe('account login()', () => {
describe('when credentials are correct', () => {
it('should return the token', async() => {
let response = await app.models.Account.login('employee', 'nightmare');
expect(response.token).toBeDefined();
});
it('should return the token if the user doesnt exist but the client does', async() => {
let response = await app.models.Account.login('PetterParker', 'nightmare');
expect(response.token).toBeDefined();
});
});
describe('when credentials are incorrect', () => {
it('should throw a 401 error', async() => {
let error;
try {
await app.models.Account.login('IDontExist', 'TotallyWrongPassword');
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(401);
expect(error.code).toBe('LOGIN_FAILED');
});
});
});

View File

@ -0,0 +1,42 @@
const app = require('vn-loopback/server/server');
describe('account logout()', () => {
it('should logout and remove token after valid login', async() => {
let loginResponse = await app.models.Account.login('employee', 'nightmare');
let accessToken = await app.models.AccessToken.findById(loginResponse.token);
let ctx = {req: {accessToken: accessToken}};
let response = await app.models.Account.logout(ctx);
let afterToken = await app.models.AccessToken.findById(loginResponse.token);
expect(response).toBeTruthy();
expect(afterToken).toBeNull();
});
it('should throw a 401 error when token is invalid', async() => {
let error;
let ctx = {req: {accessToken: {id: 'invalidToken'}}};
try {
response = await app.models.Account.logout(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(401);
});
it('should throw an error when no token is passed', async() => {
let error;
let ctx = {req: {accessToken: null}};
try {
response = await app.models.Account.logout(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,17 @@
module.exports = Self => {
Self.remoteMethod('validateToken', {
description: 'Validates the current logged user token',
returns: {
type: 'Boolean',
root: true
},
http: {
path: `/validateToken`,
verb: 'GET'
}
});
Self.validateToken = async function() {
return true;
};
};

View File

@ -0,0 +1,96 @@
const request = require('request-promise-native');
module.exports = Self => {
Self.remoteMethodCtx('sendMessage', {
description: 'Send a RocketChat message',
accessType: 'WRITE',
accepts: [{
arg: 'to',
type: 'String',
required: true,
description: 'user (@) or channel (#) to send the message'
}, {
arg: 'message',
type: 'String',
required: true,
description: 'The message'
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/sendMessage`,
verb: 'POST'
}
});
Self.sendMessage = async(ctx, to, message) => {
const models = Self.app.models;
const accessToken = ctx.req.accessToken;
const sender = await models.Account.findById(accessToken.userId);
const recipient = to.replace('@', '');
if (sender.name != recipient)
return sendMessage(to, `@${sender.name}: ${message}`);
};
async function sendMessage(name, message) {
const models = Self.app.models;
const chatConfig = await models.ChatConfig.findOne();
if (!Self.token)
Self.token = await login();
const uri = `${chatConfig.uri}/chat.postMessage`;
return send(uri, {
'channel': name,
'text': message
}).catch(async error => {
if (error.statusCode === 401 && !Self.loginAttempted) {
Self.token = await login();
Self.loginAttempted = true;
return sendMessage(name, message);
}
throw new Error(error.message);
});
}
/**
* Returns a rocketchat token
* @return {Object} userId and authToken
*/
async function login() {
const models = Self.app.models;
const chatConfig = await models.ChatConfig.findOne();
const uri = `${chatConfig.uri}/login`;
return send(uri, {
user: chatConfig.user,
password: chatConfig.password
}).then(res => res.data);
}
function send(uri, body) {
if (process.env.NODE_ENV !== 'production') {
return new Promise(resolve => {
return resolve({statusCode: 200, message: 'Fake notification sent'});
});
}
const options = {
method: 'POST',
uri: uri,
body: body,
headers: {'content-type': 'application/json'},
json: true
};
if (Self.token) {
options.headers['X-Auth-Token'] = Self.token.authToken;
options.headers['X-User-Id'] = Self.token.userId;
}
return request(options);
}
};

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('chat sendMessage()', () => {
it('should return a "Fake notification sent" as response', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let response = await app.models.Chat.sendMessage(ctx, '@salesPerson', 'I changed something');
expect(response.statusCode).toEqual(200);
expect(response.message).toEqual('Fake notification sent');
});
it('should not return a response', async() => {
let ctx = {req: {accessToken: {userId: 18}}};
let response = await app.models.Chat.sendMessage(ctx, '@salesPerson', 'I changed something');
expect(response).toBeUndefined();
});
});

View File

@ -0,0 +1,62 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('downloadFile', {
description: 'Download a document',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: `/:id/downloadFile`,
verb: 'GET'
}
});
Self.downloadFile = async function(ctx, id) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const dms = await Self.findById(id);
const hasReadRole = await models.DmsType.hasReadRole(ctx, dms.dmsTypeFk);
if (!hasReadRole)
throw new UserError(`You don't have enough privileges`);
const pathHash = storageConnector.getPathHash(dms.id);
try {
await models.Container.getFile(pathHash, dms.file);
} catch (e) {
if (e.code != 'ENOENT')
throw e;
const error = new UserError(`File doesn't exists`);
error.statusCode = 404;
throw error;
}
const stream = models.Container.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`];
};
};

View File

@ -0,0 +1,34 @@
const UserError = require('vn-loopback/util/user-error');
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 trashDmsType = await models.DmsType.findOne({where: {code: 'trash'}});
const dms = await models.Dms.findById(id);
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, dms.dmsTypeFk);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
return dms.updateAttribute('dmsTypeFk', trashDmsType.id);
};
};

View File

@ -0,0 +1,26 @@
const app = require('vn-loopback/server/server');
describe('dms downloadFile()', () => {
let dmsId = 1;
it('should return a response for an employee with text content-type', async() => {
let workerId = 107;
let ctx = {req: {accessToken: {userId: workerId}}};
const result = await app.models.Dms.downloadFile(ctx, dmsId);
expect(result[1]).toEqual('text/plain');
});
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.downloadFile(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,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,22 @@
const app = require('vn-loopback/server/server');
describe('dms updateFile()', () => {
it(`should return an error for a user without enough privileges`, async() => {
let clientId = 101;
let companyId = 442;
let warehouseId = 1;
let dmsTypeId = 14;
let dmsId = 1;
let ctx = {req: {accessToken: {userId: clientId}}, args: {dmsTypeId: dmsTypeId}};
let error;
await app.models.Dms.updateFile(ctx, dmsId, warehouseId, companyId, dmsTypeId).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,150 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
module.exports = Self => {
Self.remoteMethodCtx('updateFile', {
description: 'updates a file properties or file',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
},
{
arg: 'warehouseId',
type: 'Number',
description: 'The warehouse id'
}, {
arg: 'companyId',
type: 'Number',
description: 'The company id'
}, {
arg: 'dmsTypeId',
type: 'Number',
description: 'The dms type id'
}, {
arg: 'reference',
type: 'String'
}, {
arg: 'description',
type: 'String'
}, {
arg: 'hasFileAttached',
type: 'Boolean',
description: 'True if has an attached file'
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/updateFile`,
verb: 'POST'
}
});
Self.updateFile = async(ctx, id, warehouseId, companyId,
dmsTypeId, reference, description, hasFileAttached, options) => {
const models = Self.app.models;
let tx;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, dmsTypeId);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
const dms = await Self.findById(id, null, myOptions);
await dms.updateAttributes({
dmsTypeFk: dmsTypeId,
companyFk: companyId,
warehouseFk: warehouseId,
reference: reference,
description: description
}, myOptions);
if (hasFileAttached)
await uploadNewFile(ctx, dms, myOptions);
if (tx) await tx.commit();
return dms;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
async function uploadNewFile(ctx, dms, myOptions) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const fileOptions = {};
const tempContainer = await getContainer('temp');
const makeUpload = await models.Container.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];
if (file) {
const oldExtension = storageConnector.getFileExtension(dms.file);
const newExtension = storageConnector.getFileExtension(file.name);
const fileName = `${dms.id}.${newExtension}`;
try {
if (oldExtension != newExtension) {
const pathHash = storageConnector.getPathHash(dms.id);
await models.Container.removeFile(pathHash, dms.file);
}
} catch (err) {}
const updatedDms = await dms.updateAttributes({
contentType: file.type,
file: fileName
}, myOptions);
const pathHash = storageConnector.getPathHash(updatedDms.id);
const container = await getContainer(pathHash);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`;
const destinationPath = `${container.client.root}/${pathHash}/${updatedDms.file}`;
fs.rename(originPath, destinationPath);
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

@ -0,0 +1,147 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
module.exports = Self => {
Self.remoteMethodCtx('uploadFile', {
description: 'Uploads a file and inserts into dms model',
accessType: 'WRITE',
accepts: [
{
arg: 'warehouseId',
type: 'Number',
description: 'The warehouse id',
required: true
}, {
arg: 'companyId',
type: 'Number',
description: 'The company id',
required: true
}, {
arg: 'dmsTypeId',
type: 'Number',
description: 'The dms type id',
required: true
}, {
arg: 'reference',
type: 'String',
required: true
}, {
arg: 'description',
type: 'String',
required: true
}, {
arg: 'hasFile',
type: 'Boolean',
description: 'True if has an attached file',
required: true
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/uploadFile`,
verb: 'POST'
}
});
Self.uploadFile = async(ctx, options) => {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models;
const fileOptions = {};
const args = ctx.args;
let tx;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
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 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);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`;
const destinationPath = `${container.client.root}/${pathHash}/${newDms.file}`;
await fs.rename(originPath, destinationPath);
addedDms.push(newDms);
}
if (tx) await tx.commit();
return addedDms;
} catch (e) {
if (tx) await tx.rollback();
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;
const newDms = await Self.create({
workerFk: myWorker.id,
dmsTypeFk: args.dmsTypeId,
companyFk: args.companyId,
warehouseFk: args.warehouseId,
reference: args.reference,
description: args.description,
contentType: file.type,
hasFile: args.hasFile
}, myOptions);
let fileName = file.name;
const extension = storageConnector.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

@ -0,0 +1,48 @@
module.exports = Self => {
Self.remoteMethodCtx('send', {
description: 'Send message to user',
accessType: 'WRITE',
accepts: [{
arg: 'data',
type: 'object',
required: true,
description: 'recipientFk, message',
http: {source: 'body'}
}, {
arg: 'context',
type: 'object',
http: function(ctx) {
return ctx;
}
}],
returns: {
type: 'boolean',
root: true
},
http: {
path: `/:recipient/send`,
verb: 'post'
}
});
Self.send = async(ctx, data, options) => {
const accessToken = ctx.options && ctx.options.accessToken || ctx.req && ctx.req.accessToken;
const userId = accessToken.userId;
const models = Self.app.models;
const sender = await models.Account.findById(userId, null, options);
const recipient = await models.Account.findById(data.recipientFk, null, options);
await Self.create({
sender: sender.name,
recipient: recipient.name,
message: data.message
}, options);
return await models.MessageInbox.create({
sender: sender.name,
recipient: recipient.name,
finalRecipient: recipient.name,
message: data.message
}, options);
};
};

View File

@ -0,0 +1,14 @@
const app = require('vn-loopback/server/server');
describe('message send()', () => {
it('should return a response containing the same message in params', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let params = {
recipientFk: 1,
message: 'I changed something'
};
let response = await app.models.Message.send(ctx, params, {transaction: 'You'});
expect(response.message).toEqual(params.message);
});
});

View File

@ -0,0 +1,32 @@
module.exports = function(Self) {
Self.remoteMethodCtx('getConfig', {
description: 'returns the information from UserConfig model for the active user',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
}, {
arg: 'tableCode',
type: 'String',
description: `Code of the table you ask its configuration`
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/getConfig`,
verb: 'get'
}
});
Self.getConfig = async ctx => {
let userView = await Self.app.models.UserConfigView.findOne({
where: {tableCode: ctx.args.tableCode, userFk: ctx.req.accessToken.userId}
});
return userView;
};
};

View File

@ -0,0 +1,34 @@
module.exports = function(Self) {
Self.remoteMethodCtx('save', {
description: 'returns the information from UserConfig model for the active user',
accepts: [{
arg: 'config',
type: 'Object',
required: true,
description: `Code of the table you ask its configuration`,
http: {source: 'body'}
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/save`,
verb: 'post'
}
});
Self.save = async(ctx, config) => {
let userView = await Self.app.models.UserConfigView.findOne({
where: {tableCode: config.tableCode, userFk: ctx.req.accessToken.userId}
});
if (userView)
return userView.updateAttributes(config);
config.userFk = ctx.req.accessToken.userId;
return await Self.app.models.UserConfigView.create(config);
};
};

View File

@ -0,0 +1,31 @@
module.exports = function(Self) {
Self.remoteMethodCtx('getUserConfig', {
description: 'returns the information from UserConfig model for the active user',
accepts: [],
returns: {
type: 'object',
root: true
},
http: {
path: `/getUserConfig`,
verb: 'get'
}
});
Self.getUserConfig = async ctx => {
let userConfig = await Self.app.models.UserConfig.findOne({
where: {userFk: ctx.req.accessToken.userId}
});
if (!userConfig) {
let newConfig = {
warehouseFk: 1,
companyFk: 442,
userFk: ctx.req.accessToken.userId
};
userConfig = await Self.app.models.UserConfig.create(newConfig);
}
return userConfig;
};
};

View File

@ -0,0 +1,28 @@
module.exports = function(Self) {
Self.remoteMethodCtx('setUserConfig', {
description: 'Change worker of tickets state',
accepts: [{
arg: 'params',
type: 'object',
required: true,
description: 'warehouseFk, companyFk',
http: {source: 'body'}
}],
returns: {
arg: 'response',
type: 'object'
},
http: {
path: `/setUserConfig`,
verb: 'post'
}
});
Self.setUserConfig = async(ctx, params) => {
let token = ctx.req.accessToken;
let currentUserId = token && token.userId;
params.userFk = currentUserId;
return await Self.app.models.UserConfig.upsertWithWhere({userFk: currentUserId}, params);
};
};

View File

@ -0,0 +1,10 @@
const app = require('vn-loopback/server/server');
describe('userConfig getUserConfig()', () => {
it(`should return the configuration data of a given user`, async() => {
await app.models.UserConfig.getUserConfig({req: {accessToken: {userId: 9}}})
.then(response => {
expect(response.warehouseFk).toEqual(1);
});
});
});

74
back/model-config.json Normal file
View File

@ -0,0 +1,74 @@
{
"Account": {
"dataSource": "vn"
},
"Bank": {
"dataSource": "vn"
},
"Country": {
"dataSource": "vn"
},
"Company": {
"dataSource": "vn"
},
"Container": {
"dataSource": "storage"
},
"Chat": {
"dataSource": "vn"
},
"ChatConfig": {
"dataSource": "vn"
},
"Delivery": {
"dataSource": "vn"
},
"Message": {
"dataSource": "vn"
},
"MessageInbox": {
"dataSource": "vn"
},
"Province": {
"dataSource": "vn"
},
"UserConfig": {
"dataSource": "vn"
},
"Warehouse": {
"dataSource": "vn"
},
"Sip": {
"dataSource": "vn"
},
"UserConfigView": {
"dataSource": "vn"
},
"EmailUser": {
"dataSource": "vn"
},
"Dms": {
"dataSource": "vn"
},
"DmsType": {
"dataSource": "vn"
},
"Town": {
"dataSource": "vn"
},
"Postcode": {
"dataSource": "vn"
},
"UserPhoneType": {
"dataSource": "vn"
},
"UserPhone": {
"dataSource": "vn"
},
"UserLog": {
"dataSource": "vn"
}
}

90
back/models/account.js Normal file
View File

@ -0,0 +1,90 @@
const md5 = require('md5');
module.exports = Self => {
require('../methods/account/login')(Self);
require('../methods/account/logout')(Self);
require('../methods/account/acl')(Self);
require('../methods/account/validate-token')(Self);
// Validations
Self.validatesUniquenessOf('name', {
message: `A client with that Web User name already exists`
});
Self.observe('before save', (ctx, next) => {
if (ctx.currentInstance && ctx.currentInstance.id && ctx.data && ctx.data.password)
ctx.data.password = md5(ctx.data.password);
next();
});
Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
}
],
returns: {
type: 'Object',
root: true
},
http: {
verb: 'GET',
path: '/getCurrentUserData'
}
});
Self.getCurrentUserData = async function(ctx) {
let userId = ctx.req.accessToken.userId;
let account = await Self.findById(userId, {
fields: ['id', 'name', 'nickname']
});
let worker = await Self.app.models.Worker.findOne({
fields: ['id'],
where: {userFk: userId}
});
return Object.assign(account, {workerId: worker.id});
};
/**
* Checks if user has a role.
*
* @param {Integer} userId The user id
* @param {String} name The role name
* @param {Object} options Options
* @return {Boolean} %true if user has the role, %false otherwise
*/
Self.hasRole = async function(userId, name, options) {
let roles = await Self.getRoles(userId, options);
return roles.some(role => role == name);
};
/**
* Get all user roles.
*
* @param {Integer} userId The user id
* @param {Object} options Options
* @return {Object} User role list
*/
Self.getRoles = async(userId, options) => {
let result = await Self.rawSql(
`SELECT r.name
FROM account.user u
JOIN account.roleRole rr ON rr.role = u.role
JOIN account.role r ON r.id = rr.inheritsFrom
WHERE u.id = ?`, [userId], options);
let roles = [];
for (role of result)
roles.push(role.name);
return roles;
};
};

84
back/models/account.json Normal file
View File

@ -0,0 +1,84 @@
{
"name": "Account",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.user"
}
},
"properties": {
"id": {
"type": "number",
"required": true
},
"name": {
"type": "string",
"required": true
},
"roleFk": {
"type": "number",
"mysql": {
"columnName": "role"
}
},
"nickname": {
"type": "string"
},
"password": {
"type": "string",
"required": true
},
"active": {
"type": "boolean"
},
"email": {
"type": "string"
},
"created": {
"type": "date"
},
"updated": {
"type": "date"
}
},
"relations": {
"role": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "roleFk"
},
"emailUser": {
"type": "hasOne",
"model": "EmailUser",
"foreignKey": "userFk"
},
"worker": {
"type": "hasOne",
"model": "Worker",
"foreignKey": "userFk"
}
},
"acls": [
{
"property": "login",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "logout",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"property": "validateToken",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
}
]
}

40
back/models/bank.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "Bank",
"base": "VnModel",
"options": {
"mysql": {
"table": "bank"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"bank": {
"type": "string",
"required": true
},
"account": {
"type": "string",
"required": true
},
"cash": {
"type": "string",
"required": true
},
"entityFk": {
"type": "string",
"required": true
},
"isActive": {
"type": "string",
"required": true
},
"currencyFk": {
"type": "string",
"required": true
}
}
}

View File

@ -0,0 +1,32 @@
{
"name": "ChatConfig",
"description": "Chat API config",
"base": "VnModel",
"options": {
"mysql": {
"table": "chatConfig"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"description": "Identifier"
},
"uri": {
"type": "String"
},
"user": {
"type": "String"
},
"password": {
"type": "String"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

3
back/models/chat.js Normal file
View File

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

12
back/models/chat.json Normal file
View File

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

29
back/models/company.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "Company",
"description": "Companies",
"base": "VnModel",
"options": {
"mysql": {
"table": "company"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"description": "Identifier"
},
"code": {
"type": "String"
},
"expired": {
"type": "date"
}
},
"scope": {
"where" :{
"expired": null
}
}
}

View File

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

39
back/models/country.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "Country",
"description": "Worldwide countries",
"base": "VnModel",
"options": {
"mysql": {
"table": "country"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"country": {
"type": "string",
"required": true
},
"code": {
"type": "string"
}
},
"relations": {
"currency": {
"type": "belongsTo",
"model": "Currency",
"foreignKey": "currencyFk"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

6
back/models/dms.js Normal file
View File

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

61
back/models/dms.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "Dms",
"description": "Documental Managment system",
"base": "VnModel",
"options": {
"mysql": {
"table": "dms"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"file": {
"type": "string"
},
"contentType": {
"type": "string"
},
"reference": {
"type": "string"
},
"description": {
"type": "string"
},
"hardCopyNumber": {
"type": "Number"
},
"hasFile": {
"type": "boolean"
},
"created": {
"type": "Date"
}
},
"relations": {
"dmsType": {
"type": "belongsTo",
"model": "DmsType",
"foreignKey": "dmsTypeFk"
},
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"warehouse": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseFk"
},
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "companyFk"
}
}
}

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

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

47
back/models/dmsType.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "DmsType",
"description": "Documental Managment system types",
"base": "VnModel",
"options": {
"mysql": {
"table": "dmsType"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"name": {
"type": "string",
"required": true
},
"path": {
"type": "string",
"required": true
},
"code": {
"type": "string",
"required": true
}
},
"relations": {
"readRole": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "readRoleFk"
},
"writeRole": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "writeRoleFk"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1,27 @@
{
"name": "EmailUser",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.emailUser"
}
},
"properties": {
"userFk": {
"id": true,
"type": "Number",
"required": true
},
"email": {
"type": "string",
"required": true
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

View File

@ -0,0 +1,43 @@
{
"name": "MessageInbox",
"base": "VnModel",
"options": {
"mysql": {
"table": "messageInbox"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"sender": {
"type": "String",
"required": true
},
"recipient": {
"type": "String",
"required": true
},
"finalRecipient": {
"type": "String",
"required": true
},
"message": {
"type": "String"
}
},
"relations": {
"remitter": {
"type": "belongsTo",
"model": "User",
"foreignKey": "sender"
},
"receptor": {
"type": "belongsTo",
"model": "User",
"foreignKey": "recipient"
}
}
}

3
back/models/message.js Normal file
View File

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

39
back/models/message.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "Message",
"base": "VnModel",
"options": {
"mysql": {
"table": "message"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"sender": {
"type": "String",
"required": true
},
"recipient": {
"type": "String",
"required": true
},
"message": {
"type": "String"
}
},
"relations": {
"remitter": {
"type": "belongsTo",
"model": "User",
"foreignKey": "sender"
},
"receptor": {
"type": "belongsTo",
"model": "User",
"foreignKey": "recipient"
}
}
}

9
back/models/postcode.js Normal file
View File

@ -0,0 +1,9 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`This postcode already exists`);
return err;
});
};

50
back/models/postcode.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "Postcode",
"base": "VnModel",
"options": {
"mysql": {
"table": "postCode"
}
},
"properties": {
"code": {
"id": true,
"type": "String"
}
},
"relations": {
"town": {
"type": "belongsTo",
"model": "Town",
"foreignKey": "townFk"
},
"geo": {
"type": "belongsTo",
"model": "ZoneGeo",
"foreignKey": "geoFk"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}],
"scopes": {
"location": {
"include": {
"relation": "town",
"scope": {
"include": {
"relation": "province",
"scope": {
"include": {
"relation": "country"
}
}
}
}
}
}
}
}

5
back/models/sip.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = Self => {
Self.validatesUniquenessOf('extension', {
message: `The extension must be unique`
});
};

31
back/models/sip.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "Sip",
"base": "VnModel",
"options": {
"mysql": {
"table": "pbx.sip"
}
},
"properties": {
"userFk": {
"type": "Number",
"id": true,
"description": "The user id",
"mysql": {
"columnName": "user_id"
}
},
"extension": {
"type": "String",
"required": true
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "user_id"
}
}
}

View File

@ -0,0 +1,15 @@
const app = require('vn-loopback/server/server');
describe('loopback model Account', () => {
it('should return true if the user has the given role', async() => {
let result = await app.models.Account.hasRole(1, 'employee');
expect(result).toBeTruthy();
});
it('should return false if the user doesnt have the given role', async() => {
let result = await app.models.Account.hasRole(1, 'administrator');
expect(result).toBeFalsy();
});
});

View File

@ -0,0 +1,9 @@
const app = require('vn-loopback/server/server');
describe('loopback model Company', () => {
it('should check that the company FTH doesnt exists', async() => {
let result = await app.models.Company.findOne({where: {code: 'FTH'}});
expect(result).toBeFalsy();
});
});

57
back/models/town.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "Town",
"base": "VnModel",
"options": {
"mysql": {
"table": "town"
}
},
"properties": {
"id": {
"id": true,
"type": "Number"
},
"name": {
"type": "String"
}
},
"relations": {
"province": {
"type": "belongsTo",
"model": "Province",
"foreignKey": "provinceFk"
},
"postcodes": {
"type": "hasMany",
"model": "Postcode",
"foreignKey": "townFk"
},
"geo": {
"type": "belongsTo",
"model": "ZoneGeo",
"foreignKey": "geoFk"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}],
"scopes": {
"location": {
"include": [{
"relation": "postcodes"
},
{
"relation": "province",
"scope": {
"include": {
"relation": "country"
}
}
}],
"fields": ["id", "name", "provinceFk"]
}
}
}

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/user-config-view/getConfig')(Self);
require('../methods/user-config-view/save')(Self);
};

View File

@ -0,0 +1,33 @@
{
"name": "UserConfigView",
"base": "VnModel",
"options": {
"mysql": {
"table": "salix.userConfigView"
}
},
"properties": {
"id": {
"id": true,
"type": "Number"
},
"userFk": {
"type": "String",
"required": true
},
"tableCode": {
"type": "String",
"required": true
},
"configuration": {
"type": "Object"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/user-config/setUserConfig')(Self);
require('../methods/user-config/getUserConfig')(Self);
};

View File

@ -0,0 +1,45 @@
{
"name": "UserConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "userConfig"
}
},
"properties": {
"userFk": {
"id": true,
"type": "Number",
"required": true
},
"warehouseFk": {
"type": "Number"
},
"companyFk": {
"type": "Number"
},
"created": {
"type": "Date"
},
"updated": {
"type": "Date"
}
},
"relations": {
"warehouse": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseFk"
},
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "companyFk"
},
"account": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

58
back/models/user-log.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "UserLog",
"base": "VnModel",
"options": {
"mysql": {
"table": "userLog"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"forceId": false
},
"originFk": {
"type": "Number",
"required": true
},
"userFk": {
"type": "Number"
},
"action": {
"type": "String",
"required": true
},
"changedModel": {
"type": "String"
},
"oldInstance": {
"type": "Object"
},
"newInstance": {
"type": "Object"
},
"creationDate": {
"type": "Date"
},
"changedModelId": {
"type": "Number"
},
"changedModelValue": {
"type": "String"
},
"description": {
"type": "String"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -0,0 +1,18 @@
{
"name": "UserPhoneType",
"base": "VnModel",
"options": {
"mysql": {
"table": "userPhoneType"
}
},
"properties": {
"code": {
"id": true,
"type": "String"
},
"description": {
"type": "String"
}
}
}

View File

@ -0,0 +1,9 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`This phone already exists`);
return err;
});
};

View File

@ -0,0 +1,39 @@
{
"name": "UserPhone",
"base": "Loggable",
"log": {
"model":"UserLog",
"relation": "user"
},
"options": {
"mysql": {
"table": "userPhone"
}
},
"properties": {
"id": {
"id": true,
"type": "Number"
},
"phone": {
"type": "Number",
"required": true
},
"typeFk": {
"type": "String",
"required": true
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
},
"type": {
"type": "belongsTo",
"model": "UserPhoneType",
"foreignKey": "typeFk"
}
}
}

19
back/models/user.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "user",
"base": "User",
"options": {
"mysql": {
"table": "salix.user"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"forceId": false
},
"username":{
"type": "string"
}
}
}

View File

@ -0,0 +1,38 @@
{
"name": "Warehouse",
"description": "Warehouses from where orders are sent",
"base": "VnModel",
"options": {
"mysql": {
"table": "warehouse"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"forceId": false
},
"name": {
"type": "String"
},
"isInventory": {
"type": "Number"
},
"isManaged":{
"type": "boolean"
},
"hasStowaway":{
"type": "boolean"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
],
"scope" : {"where": {"isForTicket": {"neq": 0}}}
}

6
back/process.yml Normal file
View File

@ -0,0 +1,6 @@
apps:
- script: ./loopback/server/server.js
name: salix-back
instances: 1
max_restarts: 3
restart_delay: 15000

39
back/tests.js Normal file
View File

@ -0,0 +1,39 @@
require('require-yaml');
process.on('warning', warning => {
console.log(warning.name);
console.log(warning.message);
console.log(warning.stack);
});
let verbose = false;
if (process.argv[2] === '--v')
verbose = true;
let Jasmine = require('jasmine');
let jasmine = new Jasmine();
let SpecReporter = require('jasmine-spec-reporter').SpecReporter;
let serviceSpecs = [
`${__dirname}/**/*[sS]pec.js`,
`${__dirname}/../loopback/**/*[sS]pec.js`,
`${__dirname}/../modules/*/back/**/*.[sS]pec.js`
];
jasmine.loadConfig({
spec_dir: '.',
spec_files: serviceSpecs,
helpers: []
});
jasmine.addReporter(new SpecReporter({
spec: {
// displayStacktrace: 'summary',
displaySuccessful: verbose,
displayFailedSpec: true,
displaySpecDuration: true
}
}));
jasmine.execute();

View File

@ -1 +0,0 @@
export * from './src/auth';

View File

@ -1,2 +0,0 @@
import './module';
import './login/login';

View File

@ -1,29 +0,0 @@
<div>
<div class="box-wrapper">
<div class="box">
<img src="./logo.svg"/>
<form name="form" ng-submit="$ctrl.submit()">
<vn-textfield
label="User"
model="$ctrl.user"
name="user"
vn-id="userField"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
model="$ctrl.password"
name="password"
type="password">
</vn-textfield>
<div class="footer">
<vn-submit label="Enter"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>
</form>
</div>
</div>
<vn-snackbar vn-id="snackbar"></vn-snackbar>
</div>

View File

@ -1,84 +0,0 @@
import ngModule from '../module';
import './style.scss';
/**
* A simple login form.
*/
export default class Controller {
constructor($element, $scope, $window, $http) {
this.$element = $element;
this.$ = $scope;
this.$window = $window;
this.$http = $http;
}
submit() {
if (!this.user) {
this.focusUser();
this.showError('Please insert your user and password');
return;
}
this.loading = true;
let params = {
user: this.user,
password: this.password,
location: this.$window.location.href
};
this.$http.post('/auth/login', params).then(
json => this.onLoginOk(json),
json => this.onLoginErr(json)
);
}
onLoginOk(json) {
this.loading = false;
let data = json.data;
let params = {
token: data.token,
continue: data.continue
};
this.$window.location = `${data.loginUrl}?${this.encodeUri(params)}`;
}
encodeUri(object) {
let uri = '';
for (var key in object)
if (object[key]) {
if (uri.length > 0)
uri += '&';
uri += encodeURIComponent(key) + '=' + encodeURIComponent(object[key]);
}
return uri;
}
onLoginErr(json) {
this.loading = false;
this.password = '';
let message;
switch (json.status) {
case 401:
message = 'Invalid credentials';
break;
case -1:
message = 'Can\'t contact with server';
break;
default:
message = 'Something went wrong';
}
this.showError(message);
this.focusUser();
}
focusUser() {
this.$.userField.select();
this.$.userField.focus();
}
showError(message) {
this.$.snackbar.showError({message: message});
}
}
Controller.$inject = ['$element', '$scope', '$window', '$http'];
ngModule.component('vnLogin', {
template: require('./login.html'),
controller: Controller
});

View File

@ -1,119 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:ns="&amp;#38;ns_sfw;"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="400"
height="168.56424"
viewBox="0 0 400 168.56424"
enable-background="new 0 0 560 960"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="logo.svg"><defs
id="defs43" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview41"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="3.09"
inkscape:cx="200"
inkscape:cy="84.28212"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" /><metadata
id="metadata3"><ns:sfw><ns:slices /><ns:sliceSourceBounds
height="212.103"
width="503.32"
y="-235.507"
x="28.34"
bottomLeftOrigin="true" /></ns:sfw><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><g
id="g5"
transform="matrix(0.79472445,0,0,0.79472445,-22.522491,-18.600526)"><g
id="g7"><path
d="M 51.517,90.859 28.34,23.407 l 18.318,0 9.479,34.665 0.776,2.839 c 1.158,4.151 2.041,7.632 2.65,10.44 0.336,-1.372 0.747,-2.992 1.234,-4.854 0.487,-1.862 1.174,-4.306 2.057,-7.328 l 9.942,-35.763 18.22,0 -23.361,67.453 -16.138,0 z"
id="path9"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#3e3d3d;fill-rule:evenodd" /><path
d="m 523.891,90.859 -16.591,0 c 0.036,-0.791 0.106,-1.614 0.209,-2.466 l 0.354,-2.653 c -2.932,2.285 -5.847,3.974 -8.737,5.071 -2.892,1.096 -5.879,1.644 -8.969,1.644 -4.8,0 -8.23,-1.32 -10.289,-3.957 -2.061,-2.639 -2.463,-6.202 -1.207,-10.688 1.154,-4.121 3.184,-7.47 6.091,-10.048 2.904,-2.577 6.718,-4.385 11.438,-5.423 2.602,-0.548 5.877,-1.144 9.821,-1.788 5.886,-0.915 9.079,-2.258 9.572,-4.027 l 0.337,-1.197 c 0.406,-1.449 0.171,-2.551 -0.706,-3.304 -0.873,-0.754 -2.383,-1.129 -4.521,-1.129 -2.325,0 -4.304,0.476 -5.93,1.43 -1.63,0.953 -2.868,2.353 -3.722,4.201 l -15.015,0 c 2.371,-5.631 5.874,-9.82 10.507,-12.573 4.636,-2.751 10.511,-4.128 17.628,-4.128 4.428,0 8.014,0.535 10.754,1.606 2.739,1.069 4.66,2.69 5.763,4.86 0.753,1.559 1.074,3.417 0.958,5.573 -0.112,2.157 -0.838,5.617 -2.173,10.386 l -5.283,18.874 c -0.633,2.254 -0.926,4.03 -0.884,5.327 0.043,1.295 0.416,2.141 1.118,2.537 l -0.523,1.872 z M 512.532,67.424 c -1.479,0.794 -3.889,1.528 -7.229,2.201 -1.618,0.304 -2.856,0.564 -3.71,0.777 -2.115,0.551 -3.698,1.255 -4.748,2.107 -1.052,0.854 -1.777,2 -2.179,3.435 -0.497,1.771 -0.346,3.199 0.452,4.285 0.801,1.084 2.104,1.626 3.911,1.626 2.787,0 5.284,-0.803 7.496,-2.406 2.208,-1.603 3.677,-3.703 4.402,-6.298 l 1.605,-5.727 z"
id="path11"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#a3d131;fill-rule:evenodd" /><path
d="m 441.489,90.859 13.951,-49.816 15.682,0 -2.441,8.716 c 2.699,-3.419 5.567,-5.915 8.604,-7.489 3.039,-1.569 6.566,-2.386 10.587,-2.448 l -4.518,16.13 c -0.677,-0.089 -1.354,-0.161 -2.029,-0.206 -0.673,-0.046 -1.315,-0.068 -1.927,-0.068 -2.505,0 -4.682,0.374 -6.525,1.121 -1.843,0.749 -3.471,1.901 -4.886,3.46 -0.902,1.038 -1.758,2.527 -2.558,4.466 -0.803,1.939 -1.808,5.076 -3.026,9.414 l -4.681,16.72 -16.233,0 z"
id="path13"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#a3d131;fill-rule:evenodd" /><path
d="m 447.466,41.043 -13.947,49.816 -15.961,0 1.923,-6.863 c -2.729,2.77 -5.509,4.812 -8.336,6.121 -2.823,1.309 -5.84,1.962 -9.048,1.962 -5.497,0 -9.239,-1.41 -11.23,-4.23 -1.99,-2.818 -2.208,-7.005 -0.655,-12.554 l 9.591,-34.251 16.325,0 -7.806,27.876 c -1.145,4.097 -1.424,6.917 -0.83,8.461 0.586,1.542 2.146,2.315 4.68,2.315 2.839,0 5.169,-0.94 6.993,-2.818 1.82,-1.881 3.329,-4.945 4.517,-9.194 l 7.459,-26.64 16.325,0 z"
id="path15"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#a3d131;fill-rule:evenodd" /><path
d="m 361.923,50.894 2.757,-9.85 6.663,0 3.942,-14.073 16.322,0 -3.938,14.073 8.351,0 -2.759,9.85 -8.352,0 -6.042,21.585 c -0.924,3.3 -1.107,5.483 -0.551,6.553 0.559,1.068 2.014,1.603 4.369,1.603 l 1.223,-0.023 0.869,-0.068 -2.914,10.408 c -1.805,0.329 -3.551,0.586 -5.239,0.765 -1.686,0.18 -3.294,0.271 -4.819,0.271 -5.658,0 -9.141,-1.382 -10.447,-4.145 -1.304,-2.763 -0.694,-8.648 1.824,-17.655 l 5.402,-19.293 -6.661,0 z"
id="path17"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#a3d131;fill-rule:evenodd" /><path
d="m 350.752,90.859 -16.594,0 c 0.037,-0.791 0.106,-1.614 0.21,-2.466 l 0.353,-2.653 c -2.934,2.285 -5.846,3.974 -8.737,5.071 -2.891,1.096 -5.881,1.644 -8.966,1.644 -4.805,0 -8.234,-1.32 -10.292,-3.957 -2.057,-2.639 -2.462,-6.202 -1.204,-10.688 1.152,-4.121 3.184,-7.47 6.088,-10.048 2.909,-2.577 6.72,-4.385 11.442,-5.423 2.599,-0.548 5.872,-1.144 9.818,-1.788 5.885,-0.915 9.077,-2.258 9.573,-4.027 l 0.338,-1.197 c 0.403,-1.449 0.171,-2.551 -0.706,-3.304 -0.873,-0.754 -2.383,-1.129 -4.525,-1.129 -2.324,0 -4.3,0.476 -5.93,1.43 -1.626,0.953 -2.867,2.353 -3.722,4.201 l -15.01,0 c 2.371,-5.631 5.873,-9.82 10.509,-12.573 4.634,-2.751 10.507,-4.128 17.626,-4.128 4.428,0 8.01,0.535 10.749,1.606 2.74,1.069 4.665,2.69 5.765,4.86 0.755,1.559 1.077,3.417 0.961,5.573 -0.117,2.157 -0.837,5.617 -2.178,10.386 l -5.281,18.874 c -0.633,2.254 -0.928,4.03 -0.883,5.327 0.045,1.295 0.417,2.141 1.119,2.537 l -0.523,1.872 z M 339.394,67.424 c -1.48,0.794 -3.893,1.528 -7.233,2.201 -1.616,0.304 -2.853,0.564 -3.71,0.777 -2.115,0.551 -3.696,1.255 -4.746,2.107 -1.052,0.854 -1.777,2 -2.18,3.435 -0.498,1.771 -0.345,3.199 0.456,4.285 0.796,1.084 2.102,1.626 3.908,1.626 2.786,0 5.285,-0.803 7.493,-2.406 2.212,-1.603 3.68,-3.703 4.407,-6.298 l 1.605,-5.727 z"
id="path19"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#a3d131;fill-rule:evenodd" /><path
d="m 243.068,23.405 -18.893,67.455 -16.32,0 1.835,-6.548 c -2.596,2.712 -5.274,4.716 -8.033,6.011 -2.763,1.297 -5.747,1.946 -8.957,1.946 -6.232,0 -10.613,-2.406 -13.151,-7.215 -2.532,-4.812 -2.708,-11.11 -0.53,-18.896 2.199,-7.851 5.936,-14.246 11.217,-19.196 5.283,-4.947 10.963,-7.421 17.043,-7.421 3.268,0 5.996,0.659 8.181,1.975 2.181,1.316 3.737,3.259 4.668,5.83 l 6.706,-23.94 16.234,0 z m -50.367,42.038 c -1.25,4.457 -1.374,7.868 -0.373,10.234 1.003,2.366 3.062,3.552 6.172,3.552 3.112,0 5.798,-1.169 8.054,-3.505 2.259,-2.335 4.017,-5.764 5.282,-10.281 1.172,-4.181 1.238,-7.416 0.198,-9.706 -1.036,-2.292 -3.097,-3.437 -6.178,-3.437 -2.901,0 -5.534,1.177 -7.901,3.526 -2.367,2.352 -4.117,5.557 -5.254,9.617"
id="path21"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#3e3d3d;fill-rule:evenodd" /><path
d="m 131.769,90.859 13.949,-49.816 15.682,0 -2.441,8.716 c 2.701,-3.419 5.569,-5.915 8.604,-7.489 3.039,-1.569 13.384,-2.386 17.405,-2.448 l -4.517,16.13 c -0.675,-0.089 -1.351,-0.161 -2.03,-0.206 -0.671,-0.046 -1.315,-0.068 -1.929,-0.068 -2.503,0 -11.494,0.374 -13.339,1.121 -1.845,0.749 -3.471,1.901 -4.886,3.46 -0.902,1.038 -1.756,2.527 -2.556,4.466 -0.803,1.939 -1.812,5.076 -3.029,9.414 l -4.682,16.72 -16.231,0 z"
id="path23"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#3e3d3d;fill-rule:evenodd" /><path
d="m 114.704,75.192 15.667,0 c -2.994,5.465 -7.055,9.691 -12.186,12.683 -5.126,2.994 -10.866,4.487 -17.218,4.487 -7.727,0 -13.177,-2.345 -16.355,-7.033 -3.176,-4.689 -3.63,-11.081 -1.364,-19.175 2.309,-8.247 6.396,-14.773 12.263,-19.585 5.868,-4.809 12.634,-7.215 20.3,-7.215 7.909,0 13.439,2.446 16.586,7.336 3.151,4.891 3.52,11.644 1.106,20.264 l -0.528,1.813 -0.373,1.079 -33.689,0 c -0.978,3.502 -0.97,6.176 0.027,8.022 0.994,1.843 2.961,2.765 5.895,2.765 2.169,0 4.086,-0.457 5.748,-1.372 1.66,-0.914 3.034,-2.27 4.121,-4.069 m -13.672,-14.543 18.577,-0.043 c 0.834,-3.195 0.687,-5.692 -0.446,-7.489 -1.133,-1.794 -3.116,-2.693 -5.961,-2.693 -2.689,0 -5.083,0.883 -7.183,2.648 -2.1,1.765 -3.759,4.292 -4.987,7.577"
id="path25"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#3e3d3d;fill-rule:evenodd" /><path
d="m 279.482,91.578 -14.248,-41.467 -14.362,41.467 -16.14,0 21.281,-67.454 18.224,0 9.862,34.664 0.778,2.84 c 1.156,4.151 2.041,7.633 2.65,10.443 l 1.234,-4.857 c 0.487,-1.862 1.172,-4.304 2.057,-7.327 l 9.942,-35.763 18.222,0 -23.364,67.454 -16.136,0 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#a3d131" /></g><g
id="g29"><path
d="m 122.886,124.71 c -4.603,-5.205 -9.749,-8.869 -15.438,-10.989 -5.693,-2.117 -12.352,-3.179 -19.98,-3.179 -13.804,0 -23.794,2.605 -29.97,7.81 -6.175,5.208 -9.263,12.169 -9.263,20.888 0,4.359 0.755,8.023 2.271,10.989 1.513,2.969 3.874,5.48 7.083,7.538 3.207,2.061 7.294,3.786 12.26,5.177 4.964,1.393 10.898,2.814 17.8,4.268 7.021,1.453 13.378,3.028 19.072,4.723 5.69,1.697 10.535,3.846 14.531,6.448 3.996,2.605 7.083,5.844 9.263,9.718 2.18,3.876 3.27,8.658 3.27,14.349 0,5.449 -1.09,10.234 -3.27,14.35 -2.18,4.117 -5.268,7.568 -9.263,10.353 -3.996,2.787 -8.81,4.876 -14.44,6.267 -5.631,1.391 -11.897,2.089 -18.799,2.089 -10.293,0 -19.557,-1.604 -27.79,-4.813 -8.236,-3.207 -15.802,-8.143 -22.705,-14.803 l 3.633,-4.541 c 6.054,6.176 12.956,10.838 20.707,13.985 7.748,3.15 16.529,4.723 26.337,4.723 12.107,0 21.643,-2.208 28.607,-6.63 6.961,-4.418 10.444,-11.17 10.444,-20.252 0,-4.601 -0.849,-8.506 -2.543,-11.715 -1.697,-3.207 -4.268,-5.932 -7.719,-8.174 -3.451,-2.239 -7.782,-4.178 -12.987,-5.813 -5.208,-1.635 -11.383,-3.179 -18.527,-4.632 -7.024,-1.453 -13.259,-2.966 -18.708,-4.541 -5.449,-1.572 -10.021,-3.57 -13.713,-5.993 -3.695,-2.421 -6.479,-5.387 -8.355,-8.9 -1.879,-3.511 -2.815,-7.93 -2.815,-13.259 0,-5.69 1.09,-10.745 3.27,-15.167 2.18,-4.419 5.236,-8.111 9.172,-11.08 3.934,-2.966 8.688,-5.236 14.258,-6.812 5.568,-1.572 11.806,-2.361 18.708,-2.361 8.355,0 15.649,1.212 21.887,3.633 6.235,2.424 11.957,6.297 17.165,11.625 l -3.453,4.721 z"
id="path31"
inkscape:connector-curvature="0"
style="fill:#f7931e" /><path
d="m 142.321,234.599 55.58,-128.96 5.267,0 55.581,128.96 -6.902,0 -19.072,-44.5 -64.662,0 -19.072,44.5 -6.72,0 z m 58.304,-120.968 -30.696,71.019 60.847,0 -30.151,-71.019 z"
id="path33"
inkscape:connector-curvature="0"
style="fill:#f7931e" /><path
d="m 280.18,234.599 0,-128.96 6.175,0 0,123.148 79.192,0 0,5.813 -85.367,0 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#f7931e" /><path
d="m 387.523,234.599 0,-128.779 6.176,0 0,128.779 -6.176,0 z"
id="path37"
inkscape:connector-curvature="0"
style="fill:#f7931e" /><path
d="m 420.58,105.639 47.588,60.666 47.77,-60.666 7.266,0 -51.402,65.388 49.949,63.572 -7.266,0 -46.316,-58.85 -46.135,58.85 -7.447,0 49.949,-63.572 -51.402,-65.388 7.446,0 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#f7931e" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,50 +0,0 @@
vn-login > div {
position: absolute;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
color: #333;
font-size: 1.1em;
font-weight: normal;
background-color: #3c393b;
.box-wrapper {
position: relative;
max-width: 19em;
margin: auto;
height: inherit;
}
.box {
box-sizing: border-box;
position: absolute;
top: 50%;
width: 100%;
margin-top: -13.5em;
padding: 3em;
background-color: white;
box-shadow: 0 0 1em 0 rgba(1,1,1,.6);
border-radius: .5em;
}
img {
width: 100%;
padding-bottom: 1em;
}
.footer {
margin-top: 1em;
text-align: center;
position: relative;
}
.spinner-wrapper {
position: absolute;
width: 0;
top: .3em;
right: 4em;
overflow: visible;
}
}

View File

@ -1,14 +0,0 @@
import {ng} from 'vendor';
import 'core';
let ngModule = ng.module('vnAuth', ['vnCore']);
export default ngModule;
config.$inject = ['$translatePartialLoaderProvider', '$httpProvider'];
export function config($translatePartialLoaderProvider, $httpProvider) {
$translatePartialLoaderProvider.addPart('auth');
$httpProvider.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];
}
ngModule.config(config);

View File

@ -1 +0,0 @@
export * from './src/client';

View File

@ -1,225 +0,0 @@
{
"module": "client",
"name": "Clients",
"icon": "person",
"validations" : true,
"routes": [
{
"url": "/clients?q",
"state": "clients",
"component": "vn-client-index",
"acl": ["employee"]
},
{
"url": "/create",
"state": "create",
"component": "vn-client-create"
},
{
"url": "/clients/:id",
"state": "clientCard",
"abstract": true,
"component": "vn-client-card"
},
{
"url": "/basic-data",
"state": "clientCard.basicData",
"component": "vn-client-basic-data",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Basic data",
"icon": "settings"
}
},
{
"url": "/fiscal-data",
"state": "clientCard.fiscalData",
"component": "vn-client-fiscal-data",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Fiscal data",
"icon": "account_balance"
}
},
{
"url": "/billing-data",
"state": "clientCard.billingData",
"component": "vn-client-billing-data",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Pay method",
"icon": "icon-payment"
}
},
{
"url": "/addresses",
"state": "clientCard.addresses",
"component": "ui-view",
"abstract": true
},
{
"url": "/list",
"state": "clientCard.addresses.list",
"component": "vn-client-addresses",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Addresses",
"icon": "local_shipping"
}
},
{
"url": "/create",
"state": "clientCard.addresses.create",
"component": "vn-address-create"
},
{
"url": "/:addressId/edit",
"state": "clientCard.addresses.edit",
"component": "vn-address-edit"
},
{
"url": "/web-access",
"state": "clientCard.webAccess",
"component": "vn-client-web-access",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Web access",
"icon": "cloud"
}
},
{
"url": "/notes",
"state": "clientCard.notes",
"component": "ui-view",
"abstract": true
},
{
"url": "/list",
"state": "clientCard.notes.list",
"component": "vn-client-notes",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Notes",
"icon": "insert_drive_file"
}
},
{
"url": "/create",
"state": "clientCard.notes.create",
"component": "vn-note-create"
},
{
"url": "/credit",
"abstract": true,
"state": "clientCard.credit",
"component": "ui-view"
},
{
"url": "/list",
"state": "clientCard.credit.list",
"component": "vn-client-credit-list",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Credit",
"icon": "credit_card"
}
}, {
"url": "/create",
"state": "clientCard.credit.create",
"component": "vn-client-credit-create",
"params": {
"client": "$ctrl.client"
}
},
{
"url": "/greuge",
"abstract": true,
"state": "clientCard.greuge",
"component": "ui-view"
},
{
"url": "/list",
"state": "clientCard.greuge.list",
"component": "vn-client-greuge-list",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Greuge",
"icon": "work"
}
},
{
"url": "/create",
"state": "clientCard.greuge.create",
"component": "vn-client-greuge-create",
"params": {
"client": "$ctrl.client"
}
},
{
"url": "/mandate",
"state": "clientCard.mandate",
"component": "vn-client-mandate",
"menu": {
"description": "Mandate",
"icon": "pan_tool"
}
},
{
"url": "/invoices",
"state": "clientCard.invoices",
"component": "vn-client-invoices",
"menu": {
"description": "Invoices",
"icon": "icon-invoices"
}
},
{
"url": "/recovery",
"abstract": true,
"state": "clientCard.recovery",
"component": "ui-view"
},
{
"url": "/list",
"state": "clientCard.recovery.list",
"component": "vn-client-recovery-list",
"params": {
"client": "$ctrl.client"
},
"menu": {
"description": "Recovery",
"icon": "icon-recovery"
}
}, {
"url": "/create",
"state": "clientCard.recovery.create",
"component": "vn-client-recovery-create",
"params": {
"client": "$ctrl.client"
}
}, {
"url": "/summary",
"state": "clientCard.summary",
"component": "vn-client-summary",
"params": {
"client": "$ctrl.client"
}
}
]
}

View File

@ -1,45 +0,0 @@
<vn-watcher
vn-id="watcher"
url="/client/api/Addresses"
id-field="id"
data="$ctrl.address"
save="post"
form="form">
</vn-watcher>
<form name="form" ng-submit="watcher.submitGo('clientCard.addresses.list')" margin-medium>
<vn-card pad-large>
<vn-title>Address</vn-title>
<vn-horizontal>
<vn-check vn-one label="Default" field="$ctrl.address.isDefaultAddress"></vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one label="Consignee" field="$ctrl.address.nickname" vn-focus></vn-textfield>
<vn-textfield vn-one label="Street address" field="$ctrl.address.street"></vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one label="Postcode" field="$ctrl.address.postalCode"></vn-textfield>
<vn-textfield vn-one label="Town/City" field="$ctrl.address.city"></vn-textfield>
<vn-autocomplete vn-one
field="$ctrl.address.provinceFk"
url="/client/api/Provinces"
show-field="name"
value-field="id"
label="Province">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
field="$ctrl.address.agencyModeFk"
url="/client/api/AgencyModes"
show-field="name"
value-field="id"
label="Agency">
</vn-autocomplete>
<vn-textfield vn-one label="Phone" field="$ctrl.address.phone"></vn-textfield>
<vn-textfield vn-one label="Mobile" field="$ctrl.address.mobile"></vn-textfield>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -1,16 +0,0 @@
import ngModule from '../module';
export default class Controller {
constructor($state) {
this.address = {
clientFk: parseInt($state.params.id),
isActive: true
};
}
}
Controller.$inject = ['$state'];
ngModule.component('vnAddressCreate', {
template: require('./address-create.html'),
controller: Controller
});

View File

@ -1,25 +0,0 @@
import './address-create.js';
describe('Client', () => {
describe('Component vnAddressCreate', () => {
let controller;
let $componentController;
let $state;
beforeEach(() => {
angular.mock.module('client');
});
beforeEach(angular.mock.inject((_$componentController_, _$state_) => {
$componentController = _$componentController_;
$state = _$state_;
$state.params.id = '1234';
controller = $componentController('vnAddressCreate', {$state});
}));
it('should define and set address property', () => {
expect(controller.address.clientFk).toBe(1234);
expect(controller.address.isActive).toBe(true);
});
});
});

View File

@ -1,100 +0,0 @@
<mg-ajax
path="/client/api/Addresses/{{edit.params.addressId}}"
actions="$ctrl.address = edit.model; $ctrl._setIconAdd();"
options="mgEdit">
</mg-ajax>
<vn-watcher
vn-id="watcher"
url="/client/api/Addresses"
id-field="id"
data="$ctrl.address"
form="form">
</vn-watcher>
<form name="form" ng-submit="$ctrl.submit()" margin-medium>
<vn-card pad-large>
<vn-title>Address</vn-title>
<vn-horizontal>
<vn-check vn-one label="Enabled" field="$ctrl.address.isActive"></vn-check>
<vn-check
vn-one label="Is equalizated"
field="$ctrl.address.isEqualizated"
vn-acl="administrative, salesAssistant">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one label="Consignee" field="$ctrl.address.nickname" vn-focus></vn-textfield>
<vn-textfield vn-one label="Street" field="$ctrl.address.street"></vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one label="Postcode" field="$ctrl.address.postalCode"></vn-textfield>
<vn-textfield vn-one label="City" field="$ctrl.address.city"></vn-textfield>
<vn-autocomplete vn-one
initial-data="$ctrl.address.province"
field="$ctrl.address.provinceFk"
url="/client/api/Provinces"
show-field="name"
value-field="id"
label="Province">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
initial-data="$ctrl.address.agencyMode"
field="$ctrl.address.agencyModeFk"
url="/client/api/AgencyModes"
show-field="name"
value-field="id"
label="Agency">
</vn-autocomplete>
<vn-textfield vn-one label="Phone" field="$ctrl.address.phone"></vn-textfield>
<vn-textfield vn-one label="Mobile" field="$ctrl.address.mobile"></vn-textfield>
</vn-horizontal>
<vn-one margin-medium-top>
<vn-title>Notes</vn-title>
<mg-ajax path="/client/api/ObservationTypes" options="mgIndex as observationsTypes"></mg-ajax>
<vn-horizontal ng-repeat="observation in $ctrl.observations track by $index">
<vn-autocomplete
vn-one
initial-data="observation.observationType"
field="observation.observationTypeFk"
data="observationsTypes.model"
show-field="description"
label="Observation type">
<tpl-item>{{$parent.$parent.item.description}}</tpl-item>
</vn-autocomplete>
<vn-textfield
vn-two
margin-large-right
label="Description"
model="observation.description"
rule="addressObservation.description">
</vn-textfield>
<vn-auto pad-medium-top>
<vn-icon
pointer
medium-grey
vn-tooltip="Remove note"
tooltip-position = "left"
icon="remove_circle_outline"
ng-click="$ctrl.removeObservation($index)">
</vn-icon>
</vn-one>
</vn-horizontal>
</vn-one>
<vn-one>
<vn-icon
pointer
margin-medium-left
vn-tooltip="Add note"
tooltip-position = "right"
orange
icon="add_circle"
ng-if="observationsTypes.model.length > $ctrl.observations.length"
ng-click="$ctrl.addObservation()">
</vn-icon>
</vn-one>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -1,134 +0,0 @@
import ngModule from '../module';
export default class Controller {
constructor($state, $scope, $http, $q, $translate, vnApp) {
this.$state = $state;
this.$scope = $scope;
this.$http = $http;
this.$q = $q;
this.$translate = $translate;
this.vnApp = vnApp;
this.address = {
id: parseInt($state.params.addressId)
};
this.observations = [];
this.observationsOld = {};
this.observationsRemoved = [];
}
_setDirtyForm() {
if (this.$scope.form) {
this.$scope.form.$setDirty();
}
}
_unsetDirtyForm() {
if (this.$scope.form) {
this.$scope.form.$setPristine();
}
}
addObservation() {
this.observations.push({observationTypeFk: null, addressFk: this.address.id, description: null});
}
removeObservation(index) {
let item = this.observations[index];
if (item) {
this.observations.splice(index, 1);
if (item.id) {
this.observationsRemoved.push(item.id);
this._setDirtyForm();
}
}
if (this.observations.length === 0 && Object.keys(this.observationsOld).length === 0) {
this._unsetDirtyForm();
}
}
_submitObservations(objectObservations) {
return this.$http.post(`/client/api/AddressObservations/crudAddressObservations`, objectObservations);
}
_observationsEquals(ob1, ob2) {
return ob1.id === ob2.id && ob1.observationTypeFk === ob2.observationTypeFk && ob1.description === ob2.description;
}
submit() {
if (this.$scope.form.$invalid) {
return false;
}
let canWatcherSubmit = this.$scope.watcher.dataChanged();
let canObservationsSubmit;
let repeatedTypes = false;
let types = [];
let observationsObj = {
delete: this.observationsRemoved,
create: [],
update: []
};
for (let i = 0; i < this.observations.length; i++) {
let observation = this.observations[i];
let isNewObservation = observation.id === undefined;
if (observation.observationTypeFk && types.indexOf(observation.observationTypeFk) !== -1) {
repeatedTypes = true;
break;
}
if (observation.observationTypeFk)
types.push(observation.observationTypeFk);
if (isNewObservation && observation.observationTypeFk && observation.description) {
observationsObj.create.push(observation);
} else if (!isNewObservation && !this._observationsEquals(this.observationsOld[observation.id], observation)) {
observationsObj.update.push(observation);
}
}
canObservationsSubmit = observationsObj.update.length > 0 || observationsObj.create.length > 0 || observationsObj.delete.length > 0;
if (repeatedTypes) {
this.vnApp.showMessage(
this.$translate.instant('The observation type must be unique')
);
} else if (canWatcherSubmit && !canObservationsSubmit) {
this.$scope.watcher.submit().then(() => {
this.$state.go('clientCard.addresses.list', {id: this.$state.params.id});
});
} else if (!canWatcherSubmit && canObservationsSubmit) {
this._submitObservations(observationsObj).then(() => {
this.$state.go('clientCard.addresses.list', {id: this.$state.params.id});
});
} else if (canWatcherSubmit && canObservationsSubmit) {
this.$q.all([this.$scope.watcher.submit(), this._submitObservations(observationsObj)]).then(() => {
this.$state.go('clientCard.addresses.list', {id: this.$state.params.id});
});
} else {
this.vnApp.showMessage(
this.$translate.instant('No changes to save')
);
}
this._unsetDirtyForm();
}
$onInit() {
let filter = {
where: {addressFk: this.address.id},
include: {relation: 'observationType'}
};
this.$http.get(`/client/api/AddressObservations?filter=${JSON.stringify(filter)}`).then(res => {
this.observations = res.data;
res.data.forEach(item => {
this.observationsOld[item.id] = Object.assign({}, item);
});
});
}
}
Controller.$inject = ['$state', '$scope', '$http', '$q', '$translate', 'vnApp'];
ngModule.component('vnAddressEdit', {
template: require('./address-edit.html'),
controller: Controller
});

View File

@ -1,55 +0,0 @@
import './address-edit.js';
describe('Client', () => {
describe('Component vnAddressEdit', () => {
let $componentController;
let $state;
let controller;
let $httpBackend;
beforeEach(() => {
angular.mock.module('client');
});
beforeEach(angular.mock.inject((_$componentController_, _$state_, _$httpBackend_) => {
$componentController = _$componentController_;
$state = _$state_;
$httpBackend = _$httpBackend_;
$state.params.addressId = '1';
controller = $componentController('vnAddressEdit', {$state: $state});
}));
it('should define and set address property', () => {
expect(controller.address.id).toEqual(1);
});
describe('_observationsEquals', () => {
it('should return true if two observations are equals independent of control attributes', () => {
let ob1 = {id: 1, observationTypeFk: 1, description: 'Spiderman rocks', showAddIcon: true};
let ob2 = {id: 1, observationTypeFk: 1, description: 'Spiderman rocks', showAddIcon: false};
let equals = controller._observationsEquals(ob2, ob1);
expect(equals).toBeTruthy();
});
it('should return false if two observations are not equals independent of control attributes', () => {
let ob1 = {id: 1, observationTypeFk: 1, description: 'Spiderman rocks', showAddIcon: true};
let ob2 = {id: 1, observationTypeFk: 1, description: 'Spiderman sucks', showAddIcon: true};
let equals = controller._observationsEquals(ob2, ob1);
expect(equals).toBeFalsy();
});
});
describe('$onInit()', () => {
it('should perform a GET query to receive the address observations', () => {
let filter = {where: {addressFk: 1}, include: {relation: 'observationType'}};
let res = ['some notes'];
$httpBackend.when('GET', `/client/api/AddressObservations?filter=${JSON.stringify(filter)}`).respond(res);
$httpBackend.expectGET(`/client/api/AddressObservations?filter=${JSON.stringify(filter)}`);
controller.$onInit();
$httpBackend.flush();
});
});
});
});

View File

@ -1,7 +0,0 @@
Enabled: Activo
Is equalizated: Recargo de equivalencia
Observation type: Tipo de observación
Description: Descripción
The observation type must be unique: El tipo de observación ha de ser único
Add note: Añadir nota
Remove note: Quitar nota

View File

@ -1,47 +0,0 @@
<mg-ajax path="/client/api/Clients/{{index.params.id}}/listAddresses" options="mgIndex"></mg-ajax>
<vn-vertical pad-medium>
<vn-card pad-large>
<vn-title vn-one>Addresses</vn-title>
<vn-horizontal ng-repeat="address in index.model.items track by address.id" class="pad-medium-top" style="align-items: center;">
<vn-one border-radius class="pad-small border-solid"
ng-class="{'bg-dark-item': address.isDefaultAddress,'bg-opacity-item': !address.isActive && !address.isDefaultAddress}">
<vn-horizontal style="align-items: center;">
<vn-none pad-medium-h style="color:#FFA410;">
<i class="material-icons" ng-if="address.isDefaultAddress">star</i>
<i class="material-icons"
vn-tooltip="Active first to set as default"
tooltip-position="left"
ng-if="!address.isActive">star_border</i>
<i class="material-icons pointer"
ng-if="address.isActive && !address.isDefaultAddress"
vn-tooltip="Set as default"
tooltip-position="left"
ng-click="$ctrl.setDefault(address)">star_border</i>
</vn-none>
<vn-one border-solid-right>
<div><b>{{::address.nickname}}</b></div>
<div>{{::address.street}}</div>
<div>{{::address.city}}, {{::address.province}}</div>
<div>{{::address.phone}}, {{::address.mobile}}</div>
</vn-one>
<vn-vertical vn-one pad-medium-h>
<vn-one ng-repeat="observation in address.observations track by $index" ng-class="{'pad-small-top': $index}">
<b margin-medium-right>{{::observation.observationType.description}}:</b>
<span>{{::observation.description}}</span>
</vn-one>
</vn-vertical>
<a vn-auto ui-sref="clientCard.addresses.edit({addressId: {{::address.id}}})">
<vn-icon-button icon="edit"></vn-icon-button>
</a>
</vn-horizontal>
</vn-one>
</vn-horizontal>
</vn-card>
<vn-paging index="index" total="index.model.total"></vn-paging>
<vn-float-button
fixed-bottom-right
ui-sref="clientCard.addresses.create"
icon="add"
label="Add">
</vn-float-button>
</vn-vertical>

View File

@ -1,22 +0,0 @@
import ngModule from '../module';
class ClientAddresses {
constructor($http, $scope) {
this.$http = $http;
this.$scope = $scope;
}
setDefault(address) {
if (address.isActive) {
let params = {isDefaultAddress: true};
this.$http.patch(`/client/api/Addresses/${address.id}`, params).then(
() => this.$scope.index.accept()
);
}
}
}
ClientAddresses.$inject = ['$http', '$scope'];
ngModule.component('vnClientAddresses', {
template: require('./addresses.html'),
controller: ClientAddresses
});

View File

@ -1,46 +0,0 @@
<mg-ajax path="/client/api/Clients/{{patch.params.id}}" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.client"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="watcher.submit()" margin-medium>
<vn-card pad-large>
<vn-title>Basic data</vn-title>
<vn-horizontal>
<vn-textfield vn-one label="Comercial Name" field="$ctrl.client.name" vn-focus></vn-textfield>
<vn-textfield vn-one label="Contact" field="$ctrl.client.contact"></vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one label="Phone" field="$ctrl.client.phone"></vn-textfield>
<vn-textfield vn-one label="Mobile" field="$ctrl.client.mobile"></vn-textfield>
<vn-textfield vn-one
label="Email"
field="$ctrl.client.email"
info="You can save multiple emails">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
initial-data="$ctrl.client.salesPerson"
field="$ctrl.client.salesPersonFk"
url="/client/api/Clients/activeSalesPerson"
show-field="name"
value-field="id"
select-fields="name"
label="Salesperson"
where="{or: [{firstName: {regexp: 'search'}}, {name: {regexp: 'search'}}]}">
</vn-autocomplete>
<vn-autocomplete vn-one
initial-data="$ctrl.client.contactChannel"
field="$ctrl.client.contactChannelFk"
url="/client/api/ContactChannels"
label="Channel">
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -1,8 +0,0 @@
import ngModule from '../module';
ngModule.component('vnClientBasicData', {
template: require('./basic-data.html'),
bindings: {
client: '<'
}
});

View File

@ -1,14 +0,0 @@
Comercial Name: Nombre comercial
Tax number: NIF/CIF
Social name: Razón social
Phone: Teléfono
Mobile: Móvil
Fax: Fax
Email: Correo electrónico
Salesperson: Comercial
Channel: Canal
You can save multiple emails: >-
Puede guardar varios correos electrónicos encadenándolos mediante comas
sin espacios, ejemplo: user@dominio.com, user2@dominio.com siendo el primer
correo electrónico el principal
Contact: Contacto

View File

@ -1,64 +0,0 @@
<mg-ajax path="/client/api/Clients/{{patch.params.id}}" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.client"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="$ctrl.submit()" pad-medium>
<vn-card pad-large>
<vn-title>Pay method</vn-title>
<vn-horizontal>
<vn-autocomplete vn-two
vn-acl="administrative, salesAssistant"
field="$ctrl.client.payMethodFk"
url="/client/api/PayMethods"
select-fields="ibanRequired"
initial-data="$ctrl.client.payMethod"
label="Pay method">
</vn-autocomplete>
<vn-textfield
vn-two label="IBAN"
field="$ctrl.client.iban"
vn-acl="administrative, salesAssistant">
</vn-textfield>
<vn-textfield
vn-one label="Due day"
field="$ctrl.client.dueDay"
vn-acl="administrative, salesAssistant">
</vn-textfield>
</vn-horizontal>
<vn-horizontal margin-medium-bottom>
<vn-one>
<vn-check
label="Received core VNH"
field="$ctrl.client.hasCoreVnh"
vn-acl="administrative, salesAssistant">
</vn-check>
</vn-one>
<vn-one>
<vn-check
label="Received core VNL"
field="$ctrl.client.hasCoreVnl"
vn-acl="administrative, salesAssistant">
</vn-check>
</vn-one>
<vn-one>
<vn-check
label="Received B2B VNL"
field="$ctrl.client.hasSepaVnl"
vn-acl="administrative, salesAssistant">
</vn-check>
</vn-one>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save" vn-acl="administrative, salesAssistant"></vn-submit>
</vn-button-bar>
</form>
<vn-confirm
vn-id="send-mail"
on-response="$ctrl.returnDialog(response)"
question="Changed terms"
message="Notify customer?">
</vn-confirm>

View File

@ -1,57 +0,0 @@
import ngModule from '../module';
export default class Controller {
constructor($scope, $http, vnApp, $translate) {
this.$ = $scope;
this.$http = $http;
this.vnApp = vnApp;
this.translate = $translate;
this.billData = {};
this.copyData();
}
$onChanges() {
this.copyData();
}
copyData() {
if (this.client) {
this.billData.payMethodFk = this.client.payMethodFk;
this.billData.iban = this.client.iban;
this.billData.dueDay = this.client.dueDay;
}
}
submit() {
return this.$.watcher.submit().then(
() => this.checkPaymentChanges());
}
checkPaymentChanges() {
let equals = true;
Object.keys(this.billData).forEach(
val => {
if (this.billData[val] !== this.client[val]) {
this.billData[val] = this.client[val];
equals = false;
}
}
);
if (!equals) {
this.$.sendMail.show();
}
}
returnDialog(response) {
if (response === 'ACCEPT') {
this.$http.post(`/mailer/notification/payment-update/${this.client.id}`).then(
() => this.vnApp.showMessage(this.translate.instant('Notification sent!'))
);
}
}
}
Controller.$inject = ['$scope', '$http', 'vnApp', '$translate'];
ngModule.component('vnClientBillingData', {
template: require('./billing-data.html'),
controller: Controller,
bindings: {
client: '<'
}
});

View File

@ -1,80 +0,0 @@
import './billing-data.js';
describe('Client', () => {
describe('Component vnClientBillingData', () => {
let $componentController;
let $httpBackend;
let $scope;
let controller;
beforeEach(() => {
angular.mock.module('client');
});
beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$httpBackend_) => {
$componentController = _$componentController_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', /\/locale\/\w+\/[a-z]{2}\.json/).respond({});
$scope = $rootScope.$new();
let submit = jasmine.createSpy('submit').and.returnValue(Promise.resolve());
$scope.watcher = {submit};
let show = jasmine.createSpy('show');
$scope.sendMail = {show};
controller = $componentController('vnClientBillingData', {$scope: $scope});
}));
describe('copyData()', () => {
it(`should define billData using client's data`, () => {
controller.client = {
dueDay: 0,
iban: null,
payMethodFk: 1
};
controller.billData = {};
controller.copyData(controller.client);
expect(controller.billData).toEqual(controller.client);
});
});
describe('submit()', () => {
it(`should call submit() on the watcher then receive a callback`, done => {
spyOn(controller, 'checkPaymentChanges');
controller.submit()
.then(() => {
expect(controller.$.watcher.submit).toHaveBeenCalledWith();
expect(controller.checkPaymentChanges).toHaveBeenCalledWith();
done();
});
});
});
describe('checkPaymentChanges()', () => {
it(`should not call sendMail.show() if there are no changes on billing data`, () => {
controller.billData = {marvelHero: 'Silver Surfer'};
controller.client = {marvelHero: 'Silver Surfer'};
controller.checkPaymentChanges();
expect(controller.$.sendMail.show).not.toHaveBeenCalled();
});
it(`should call sendMail.show() if there are changes on billing data object`, () => {
controller.billData = {marvelHero: 'Silver Surfer'};
controller.client = {marvelHero: 'Spider-Man'};
controller.checkPaymentChanges();
expect(controller.$.sendMail.show).toHaveBeenCalledWith();
});
});
describe('returnDialog()', () => {
it('should request to send notification email', () => {
controller.client = {id: '123'};
$httpBackend.when('POST', `/mailer/notification/payment-update/${controller.client.id}`).respond('done');
$httpBackend.expectPOST(`/mailer/notification/payment-update/${controller.client.id}`);
controller.returnDialog('ACCEPT');
$httpBackend.flush();
});
});
});
});

View File

@ -1,15 +0,0 @@
Changed terms: Payment terms have changed
Notify customer?: Do you want to notify customer?
No: No
Yes, notify: Yes, notify
Notification sent!: Notification sent!
Notification error: Error while sending notification
Yes, propagate: Yes, propagate
Equivalent tax spreaded: Equivalent tax spreaded
Invoice by address: Invoice by address
Equalization tax: Equalization tax
Due day: Due day
Received core VNH: VNH core received
Received core VNL: VNL core received
Received B2B VNL: VNL B2B received
Save: Save

View File

@ -1,15 +0,0 @@
Changed terms: Has modificado las condiciones de pago
Notify customer?: ¿Deseas notificar al cliente de dichos cambios?
No: No
Yes, notify: Sí, notificar
Notification sent!: ¡Notificación enviada!
Notification error: Error al enviar notificación
Yes, propagate: Si, propagar
Equivalent tax spreaded: Recargo de equivalencia propagado
Invoice by address: Facturar por consignatario
Equalization tax: Recargo de equivalencia
Due day: Vencimiento
Received core VNH: Recibido core VNH
Received core VNL: Recibido core VNL
Received B2B VNL: Recibido B2B VNL
Save: Guardar

View File

@ -1,16 +0,0 @@
<vn-main-block>
<mg-ajax
path="/client/api/Clients/{{edit.params.id}}/card"
actions="$ctrl.client = edit.model"
options="mgEdit">
</mg-ajax>
<vn-horizontal>
<vn-auto class="left-block">
<vn-client-descriptor client="$ctrl.client"></vn-client-descriptor>
<vn-left-menu></vn-left-menu>
</vn-auto>
<vn-one>
<vn-vertical ui-view></vn-vertical>
</vn-one>
</vn-horizontal>
</vn-main-block>

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