From cd1c51e297e3862e565cc5bec91f59469e3539bb Mon Sep 17 00:00:00 2001 From: Camel Aissani Date: Tue, 18 May 2021 20:52:12 +0200 Subject: [PATCH] added eslint prettier and fixed files --- .eslintrc | 23 - .eslintrc.json | 26 + .husky/.gitignore | 1 + .husky/pre-commit | 4 + .prettierignore | 2 + .prettierrc.json | 5 + .travis.yml | 4 +- README.md | 21 +- backend/businesslogic/FR/computeRent/index.js | 127 +- .../FR/computeRent/tasks/1_base.js | 97 +- .../FR/computeRent/tasks/2_debts.js | 26 +- .../FR/computeRent/tasks/3_discounts.js | 42 +- .../FR/computeRent/tasks/4_vat.js | 80 +- .../FR/computeRent/tasks/5_balance.js | 19 +- .../FR/computeRent/tasks/6_payment.js | 20 +- .../FR/computeRent/tasks/7_total.js | 54 +- backend/businesslogic/index.js | 2 +- backend/managers/accountingmanager.js | 10 +- backend/managers/contract.js | 320 ++-- backend/managers/documentmanager.js | 94 +- backend/managers/emailmanager.js | 150 +- backend/managers/frontdata.js | 1116 +++++++------ backend/managers/leasemanager.js | 257 ++- backend/managers/loginmanager.js | 637 ++++---- backend/managers/notificationmanager.js | 292 ++-- backend/managers/occupantmanager.js | 571 +++---- backend/managers/ownermanager.js | 136 +- backend/managers/propertymanager.js | 318 ++-- backend/managers/realmmanager.js | 280 ++-- backend/managers/rentmanager.js | 616 ++++---- backend/managers/templatemanager.js | 84 + backend/models/account.js | 72 +- backend/models/db.js | 454 +++--- backend/models/document.js | 12 +- backend/models/lease.js | 49 +- backend/models/model.js | 152 +- backend/models/notification.js | 14 +- backend/models/objectfilter.js | 119 +- backend/models/occupant.js | 97 +- backend/models/property.js | 71 +- backend/models/realm.js | 194 +-- backend/models/rent.js | 26 +- backend/pages/accounting/index.js | 12 +- backend/pages/accounting/model/index.js | 9 +- backend/pages/dashboard/index.js | 10 +- backend/pages/dashboard/model/index.js | 8 +- backend/pages/ejshelpers.js | 118 +- backend/pages/index.js | 47 +- backend/pages/occupant/index.js | 10 +- backend/pages/occupant/model/index.js | 8 +- backend/pages/owner/index.js | 10 +- backend/pages/owner/model/index.js | 8 +- backend/pages/print/index.js | 14 +- backend/pages/print/model/index.js | 45 +- backend/pages/profile/index.js | 10 +- backend/pages/profile/model/index.js | 8 +- backend/pages/property/index.js | 10 +- backend/pages/property/model/index.js | 8 +- backend/pages/realm/index.js | 10 +- backend/pages/realm/model/index.js | 8 +- backend/pages/rent/index.js | 12 +- backend/pages/rent/model/index.js | 8 +- backend/pages/signin/index.js | 14 +- backend/pages/signin/model/index.js | 8 +- backend/pages/signup/index.js | 14 +- backend/pages/signup/model/index.js | 8 +- backend/pages/website/index.js | 12 +- backend/pages/website/model/index.js | 8 +- backend/routes/api.js | 98 +- backend/routes/apiv2.js | 224 +-- backend/routes/auth.js | 92 +- backend/routes/index.js | 78 +- backend/routes/page.js | 64 +- backend/utils/crypto.js | 31 +- config/index.js | 82 +- config/website.json | 90 +- documentation/ABOUT.md | 37 +- documentation/README.md | 70 +- frontend/js/accounting/middleware.js | 182 ++- frontend/js/application.js | 82 +- frontend/js/baseview_middleware.js | 12 +- frontend/js/connection_middleware.js | 94 +- frontend/js/dashboard/middleware.js | 275 ++-- frontend/js/form.js | 476 +++--- frontend/js/formvalidators.js | 189 ++- frontend/js/index.js | 54 +- frontend/js/language.js | 169 +- frontend/js/lib/anilayout.js | 104 +- frontend/js/lib/anilist.js | 860 +++++----- frontend/js/lib/helper.js | 569 +++---- frontend/js/lib/objectfilter.js | 47 +- frontend/js/login/loginform.js | 54 +- frontend/js/login/middleware.js | 94 +- frontend/js/menu.js | 100 +- frontend/js/menu_middleware.js | 28 +- frontend/js/occupant/contractdocumentsform.js | 191 +-- frontend/js/occupant/middleware.js | 518 +++--- frontend/js/occupant/occupantform.js | 989 ++++++------ frontend/js/owner/middleware.js | 118 +- frontend/js/owner/ownerform.js | 298 ++-- frontend/js/print.js | 43 +- frontend/js/property/middleware.js | 147 +- frontend/js/property/propertyform.js | 212 +-- frontend/js/rent/middleware.js | 631 ++++---- frontend/js/rent/paymentform.js | 514 +++--- frontend/js/selectrealm/middleware.js | 42 +- frontend/js/selectrealm/realmform.js | 533 +++---- frontend/js/signup/middleware.js | 83 +- frontend/js/signup/signupform.js | 46 +- frontend/js/view_middleware.js | 18 +- frontend/js/viewcontroller.js | 541 ++++--- frontend/js/website/middleware.js | 11 +- frontend/less/accounting.less | 64 +- frontend/less/bootswatch.less | 10 +- frontend/less/card.less | 10 +- frontend/less/datepicker.less | 15 +- frontend/less/form.less | 159 +- frontend/less/index.less | 133 +- frontend/less/layout.less | 179 ++- frontend/less/list.less | 296 ++-- frontend/less/menu.less | 84 +- frontend/less/print.less | 740 ++++----- frontend/less/style.less | 54 +- frontend/less/table.less | 17 +- frontend/less/tiles.less | 282 ++-- frontend/less/variables.less | 805 +++++----- frontend/less/website.less | 18 +- frontend/locales/en.json | 554 +++---- frontend/locales/fr.json | 554 +++---- frontend/locales/pt.json | 540 +++---- package-lock.json | 1405 +++++++++++++---- package.json | 9 +- scripts/build.js | 367 ++--- scripts/migration.js | 567 ++++--- scripts/mongodump.js | 4 +- scripts/mongorestore.js | 56 +- server.js | 236 +-- test/api.js | 715 +++++---- test/businesslogic_FR/computeRent.js | 192 ++- test/managers/contract.js | 1077 ++++++++----- test/notificationmanager.js | 4 +- test/occupantmanager.js | 1 - test/pages.js | 362 +++-- test/requester.js | 33 +- 144 files changed, 14189 insertions(+), 11674 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.json create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 backend/managers/templatemanager.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c50d6fc..0000000 --- a/.eslintrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module" - }, - "rules": { - "for-direction": "off", - "getter-return": "off", - "indent": ["error", 4], - "quotes": ["error", "single"], - "linebreak-style": ["error", "unix"], - "semi": ["error", "always"], - "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }] - }, - "env": { - "node": true, - "browser": true, - "jquery": true, - "es6": true - }, - "extends": "eslint:recommended" -} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..63cc65c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": { + "semi": "error", + "quotes": ["error", "single", { "avoidEscape": true }], + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true + } + ] + }, + "env": { + "node": true, + "browser": true, + "jquery": true, + "es2021": true + }, + "extends": ["eslint:recommended"] +} diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..30a229a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +bkp +dist \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..73925d6 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "semi": true, + "tabWidth": 2, + "singleQuote": true +} diff --git a/.travis.yml b/.travis.yml index 9f26044..ccd1ab6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ before_install: # - sudo apt-get install -y nasm node_js: - - "8.12.0" + - '8.12.0' before_script: - npm install coveralls @@ -15,4 +15,4 @@ script: - npm run coverage after_script: - - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js \ No newline at end of file + - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/README.md b/README.md index a8e7bca..c97dabe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Loca -==== +# Loca [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/microrealestate) ![Docker](https://github.com/camelaissani/loca/workflows/Docker/badge.svg) @@ -12,12 +11,13 @@ Loca This nodejs project is a tentative of web application that offers a toolkit for owners of buildings, flats, offices, meeting rooms, car parks, letter boxes... The idea is to make easy the management of properties and occupants by proposing many services: - - Gather all information of your properties and occupants in one place - - Create rent contract from templates available in the system - - Follow the rent payments month by month - - Template letters for recovery of not paid rents -![Loca](http://www.nuageprive.fr/images/loca-sample.png "Open source real estate management") +- Gather all information of your properties and occupants in one place +- Create rent contract from templates available in the system +- Follow the rent payments month by month +- Template letters for recovery of not paid rents + +![Loca](http://www.nuageprive.fr/images/loca-sample.png 'Open source real estate management') [Check out the live demo.](http://demo.nuageprive.fr/) @@ -25,8 +25,7 @@ The idea is to make easy the management of properties and occupants by proposing Follow instructions from [here](https://github.com/microrealestate/microrealestate#getting-started) -Technical Stack ---------------- +## Technical Stack Back-end: @@ -38,10 +37,8 @@ JQuery, Bootstrap, Handlebars, and [frontexpress](https://github.com/camelaissan Build system based on RollupJS +## Why I created this application? -Why I created this application? -------------------------------- Simply to help my best friend and I to manage properties that we rent. Above all, to have a good reason to play with node and javascript :-) - diff --git a/backend/businesslogic/FR/computeRent/index.js b/backend/businesslogic/FR/computeRent/index.js index f9d98fb..edf64b5 100755 --- a/backend/businesslogic/FR/computeRent/index.js +++ b/backend/businesslogic/FR/computeRent/index.js @@ -1,68 +1,67 @@ const fs = require('fs'); const path = require('path'); - -module.exports = function(contract, rentDate, previousRent, settlements) { - const rent = { - term: 0, - month: 0, - year: 0, - preTaxAmounts: [ - // { - // description: '', - // amount: '' - // } - ], - charges: [ - // { - // description: '', - // amount: '' - // } - ], - discounts: [ - // { - // origin: '', // 'contract', 'settlement' - // description: '', - // amount: '' - // } - ], - debts: [ - // { - // description: '', - // amount: '' - // } - ], - vats: [ - // { - // origin: '', // 'contract', 'settlement' - // description: '', - // rate: 0, - // amount: 0 - // } - ], - payments: [ - // { - // date: '', - // amount: 0, - // type: '', - // reference: '' - // } - ], - description: '', - total: { - balance: 0, - preTaxAmount: 0, - charges: 0, - discount: 0, - vat: 0, - grandTotal: 0, - payment: 0 - } - }; - const tasks_dir = path.join(__dirname, 'tasks'); - const taskFiles = fs.readdirSync(tasks_dir); - return taskFiles.reduce((rent, taskFile) => { - const task = require(path.join(tasks_dir, taskFile)); - return task(contract, rentDate, previousRent, settlements, rent); - }, rent); +module.exports = function (contract, rentDate, previousRent, settlements) { + const rent = { + term: 0, + month: 0, + year: 0, + preTaxAmounts: [ + // { + // description: '', + // amount: '' + // } + ], + charges: [ + // { + // description: '', + // amount: '' + // } + ], + discounts: [ + // { + // origin: '', // 'contract', 'settlement' + // description: '', + // amount: '' + // } + ], + debts: [ + // { + // description: '', + // amount: '' + // } + ], + vats: [ + // { + // origin: '', // 'contract', 'settlement' + // description: '', + // rate: 0, + // amount: 0 + // } + ], + payments: [ + // { + // date: '', + // amount: 0, + // type: '', + // reference: '' + // } + ], + description: '', + total: { + balance: 0, + preTaxAmount: 0, + charges: 0, + discount: 0, + vat: 0, + grandTotal: 0, + payment: 0, + }, + }; + const tasks_dir = path.join(__dirname, 'tasks'); + const taskFiles = fs.readdirSync(tasks_dir); + return taskFiles.reduce((rent, taskFile) => { + const task = require(path.join(tasks_dir, taskFile)); + return task(contract, rentDate, previousRent, settlements, rent); + }, rent); }; diff --git a/backend/businesslogic/FR/computeRent/tasks/1_base.js b/backend/businesslogic/FR/computeRent/tasks/1_base.js index 2d7d2bb..7cd4006 100644 --- a/backend/businesslogic/FR/computeRent/tasks/1_base.js +++ b/backend/businesslogic/FR/computeRent/tasks/1_base.js @@ -1,46 +1,69 @@ const moment = require('moment'); -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - const currentMoment = moment(rentDate, 'DD/MM/YYYY HH:mm'); - rent.term = Number(currentMoment.format('YYYYMMDDHH')); - if (contract.frequency === 'months') { - rent.term = Number(moment(currentMoment).startOf('month').format('YYYYMMDDHH')); - } - if (contract.frequency === 'days') { - rent.term = Number(moment(currentMoment).startOf('day').format('YYYYMMDDHH')); - } - if (contract.frequency === 'hours') { - rent.term = Number(moment(currentMoment).startOf('hour').format('YYYYMMDDHH')); - } - rent.month = currentMoment.month() + 1; // 0 based - rent.year = currentMoment.year(); +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + const currentMoment = moment(rentDate, 'DD/MM/YYYY HH:mm'); + rent.term = Number(currentMoment.format('YYYYMMDDHH')); + if (contract.frequency === 'months') { + rent.term = Number( + moment(currentMoment).startOf('month').format('YYYYMMDDHH') + ); + } + if (contract.frequency === 'days') { + rent.term = Number( + moment(currentMoment).startOf('day').format('YYYYMMDDHH') + ); + } + if (contract.frequency === 'hours') { + rent.term = Number( + moment(currentMoment).startOf('hour').format('YYYYMMDDHH') + ); + } + rent.month = currentMoment.month() + 1; // 0 based + rent.year = currentMoment.year(); - contract.properties.filter((property) => { - const entryMoment = moment(property.entryDate, 'DD/MM/YYYY').startOf('day'); - const exitMoment = moment(property.exitDate, 'DD/MM/YYYY').endOf('day'); + contract.properties + .filter((property) => { + const entryMoment = moment(property.entryDate, 'DD/MM/YYYY').startOf( + 'day' + ); + const exitMoment = moment(property.exitDate, 'DD/MM/YYYY').endOf('day'); - return currentMoment.isBetween(entryMoment, exitMoment, contract.frequency, '[]'); - }).forEach(function (property) { - if (property.property) { - const name = property.property.name || ''; - const preTaxAmount = property.rent || 0; - const expenses = property.expenses || []; + return currentMoment.isBetween( + entryMoment, + exitMoment, + contract.frequency, + '[]' + ); + }) + .forEach(function (property) { + if (property.property) { + const name = property.property.name || ''; + const preTaxAmount = property.rent || 0; + const expenses = property.expenses || []; - rent.preTaxAmounts.push({ - description: name, - amount: preTaxAmount - }); + rent.preTaxAmounts.push({ + description: name, + amount: preTaxAmount, + }); - if (expenses.length) { - rent.charges.push(...expenses.map(({title, amount}) => ({ - description: title, - amount - }))); - } + if (expenses.length) { + rent.charges.push( + ...expenses.map(({ title, amount }) => ({ + description: title, + amount, + })) + ); } + } }); - if (settlements) { - rent.description = settlements.description || ''; - } - return rent; + if (settlements) { + rent.description = settlements.description || ''; + } + return rent; }; diff --git a/backend/businesslogic/FR/computeRent/tasks/2_debts.js b/backend/businesslogic/FR/computeRent/tasks/2_debts.js index 307f12b..cab0e45 100644 --- a/backend/businesslogic/FR/computeRent/tasks/2_debts.js +++ b/backend/businesslogic/FR/computeRent/tasks/2_debts.js @@ -1,11 +1,17 @@ -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - if (settlements && settlements.debts) { - settlements.debts.forEach(debt => { - rent.debts.push({ - description: debt.description, - amount: debt.amount - }); - }); - } - return rent; +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + if (settlements && settlements.debts) { + settlements.debts.forEach((debt) => { + rent.debts.push({ + description: debt.description, + amount: debt.amount, + }); + }); + } + return rent; }; diff --git a/backend/businesslogic/FR/computeRent/tasks/3_discounts.js b/backend/businesslogic/FR/computeRent/tasks/3_discounts.js index 6d3758e..39a6cd5 100644 --- a/backend/businesslogic/FR/computeRent/tasks/3_discounts.js +++ b/backend/businesslogic/FR/computeRent/tasks/3_discounts.js @@ -1,20 +1,26 @@ -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - if (contract.discount) { - rent.discounts.push({ - origin: 'contract', - description: 'Remise exceptionnelle', - amount: contract.discount - }); - } +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + if (contract.discount) { + rent.discounts.push({ + origin: 'contract', + description: 'Remise exceptionnelle', + amount: contract.discount, + }); + } - if (settlements && settlements.discounts) { - settlements.discounts.forEach(discount => { - rent.discounts.push({ - origin: 'settlement', - description: discount.description, - amount: discount.amount - }); - }); - } - return rent; + if (settlements && settlements.discounts) { + settlements.discounts.forEach((discount) => { + rent.discounts.push({ + origin: 'settlement', + description: discount.description, + amount: discount.amount, + }); + }); + } + return rent; }; diff --git a/backend/businesslogic/FR/computeRent/tasks/4_vat.js b/backend/businesslogic/FR/computeRent/tasks/4_vat.js index 97fcacb..7003a60 100644 --- a/backend/businesslogic/FR/computeRent/tasks/4_vat.js +++ b/backend/businesslogic/FR/computeRent/tasks/4_vat.js @@ -1,43 +1,49 @@ -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - if (contract.vatRate) { - const rate = contract.vatRate || 0; +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + if (contract.vatRate) { + const rate = contract.vatRate || 0; - rent.preTaxAmounts.forEach((preTaxAmount) => { - rent.vats.push({ - origin: 'contract', - description: `${preTaxAmount.description} T.V.A. (${rate*100}%)`, - amount: preTaxAmount.amount*rate, - rate - }); - }); + rent.preTaxAmounts.forEach((preTaxAmount) => { + rent.vats.push({ + origin: 'contract', + description: `${preTaxAmount.description} T.V.A. (${rate * 100}%)`, + amount: preTaxAmount.amount * rate, + rate, + }); + }); - rent.charges.forEach((charges) => { - rent.vats.push({ - origin: 'contract', - description: `${charges.description} T.V.A. (${rate*100}%)`, - amount: charges.amount*rate, - rate - }); - }); + rent.charges.forEach((charges) => { + rent.vats.push({ + origin: 'contract', + description: `${charges.description} T.V.A. (${rate * 100}%)`, + amount: charges.amount * rate, + rate, + }); + }); - rent.debts.forEach((debt) => { - rent.vats.push({ - origin: 'debts', - description: `${debt.description} T.V.A. (${rate*100}%)`, - amount: debt.amount*rate, - rate - }); - }); + rent.debts.forEach((debt) => { + rent.vats.push({ + origin: 'debts', + description: `${debt.description} T.V.A. (${rate * 100}%)`, + amount: debt.amount * rate, + rate, + }); + }); - rent.discounts.forEach((discount) => { - rent.vats.push({ - origin: discount.origin, - description: `${discount.description} T.V.A. (${rate*100}%)`, - amount: discount.amount*rate*(-1), - rate - }); - }); - } + rent.discounts.forEach((discount) => { + rent.vats.push({ + origin: discount.origin, + description: `${discount.description} T.V.A. (${rate * 100}%)`, + amount: discount.amount * rate * -1, + rate, + }); + }); + } - return rent; + return rent; }; diff --git a/backend/businesslogic/FR/computeRent/tasks/5_balance.js b/backend/businesslogic/FR/computeRent/tasks/5_balance.js index 88deb9b..448f100 100755 --- a/backend/businesslogic/FR/computeRent/tasks/5_balance.js +++ b/backend/businesslogic/FR/computeRent/tasks/5_balance.js @@ -1,7 +1,14 @@ -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - rent.balance = 0; - if (previousRent) { - rent.total.balance = previousRent.total.grandTotal - previousRent.total.payment; - } - return rent; +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + rent.balance = 0; + if (previousRent) { + rent.total.balance = + previousRent.total.grandTotal - previousRent.total.payment; + } + return rent; }; diff --git a/backend/businesslogic/FR/computeRent/tasks/6_payment.js b/backend/businesslogic/FR/computeRent/tasks/6_payment.js index ec889c0..0396cf7 100755 --- a/backend/businesslogic/FR/computeRent/tasks/6_payment.js +++ b/backend/businesslogic/FR/computeRent/tasks/6_payment.js @@ -1,8 +1,14 @@ -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - if (settlements && settlements.payments) { - settlements.payments.forEach(payment => { - rent.payments.push(payment); - }); - } - return rent; +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + if (settlements && settlements.payments) { + settlements.payments.forEach((payment) => { + rent.payments.push(payment); + }); + } + return rent; }; diff --git a/backend/businesslogic/FR/computeRent/tasks/7_total.js b/backend/businesslogic/FR/computeRent/tasks/7_total.js index bc2a873..3694c71 100755 --- a/backend/businesslogic/FR/computeRent/tasks/7_total.js +++ b/backend/businesslogic/FR/computeRent/tasks/7_total.js @@ -1,20 +1,44 @@ const math = require('mathjs'); -module.exports = function(contract, rentDate, previousRent, settlements, rent) { - const preTaxAmount = rent.preTaxAmounts.reduce((total, preTaxAmount) => total + preTaxAmount.amount, 0); - const charges = rent.charges.reduce((total, charges) => total + charges.amount, 0); - const debts = rent.debts.reduce((total, debt) => total + debt.amount, 0); - const discount = rent.discounts.reduce((total, discount) => total + discount.amount, 0); - const vat = math.round(rent.vats.reduce((total, vat) => total + vat.amount, 0), 2); - const payment = rent.payments.reduce((total, payment) => total + payment.amount, 0); +module.exports = function ( + contract, + rentDate, + previousRent, + settlements, + rent +) { + const preTaxAmount = rent.preTaxAmounts.reduce( + (total, preTaxAmount) => total + preTaxAmount.amount, + 0 + ); + const charges = rent.charges.reduce( + (total, charges) => total + charges.amount, + 0 + ); + const debts = rent.debts.reduce((total, debt) => total + debt.amount, 0); + const discount = rent.discounts.reduce( + (total, discount) => total + discount.amount, + 0 + ); + const vat = math.round( + rent.vats.reduce((total, vat) => total + vat.amount, 0), + 2 + ); + const payment = rent.payments.reduce( + (total, payment) => total + payment.amount, + 0 + ); - rent.total.preTaxAmount = preTaxAmount; - rent.total.charges = charges; - rent.total.debts = debts; - rent.total.discount = discount; - rent.total.vat = vat; - rent.total.grandTotal = math.round(preTaxAmount + charges + debts - discount + vat + rent.total.balance, 2); - rent.total.payment = payment; + rent.total.preTaxAmount = preTaxAmount; + rent.total.charges = charges; + rent.total.debts = debts; + rent.total.discount = discount; + rent.total.vat = vat; + rent.total.grandTotal = math.round( + preTaxAmount + charges + debts - discount + vat + rent.total.balance, + 2 + ); + rent.total.payment = payment; - return rent; + return rent; }; diff --git a/backend/businesslogic/index.js b/backend/businesslogic/index.js index cd7b326..d63327f 100644 --- a/backend/businesslogic/index.js +++ b/backend/businesslogic/index.js @@ -1,5 +1,5 @@ const config = require('../../config'); module.exports = { - computeRent: require(`./${config.businesslogic}/computeRent`) + computeRent: require(`./${config.businesslogic}/computeRent`), }; diff --git a/backend/managers/accountingmanager.js b/backend/managers/accountingmanager.js index be462cc..d8be794 100644 --- a/backend/managers/accountingmanager.js +++ b/backend/managers/accountingmanager.js @@ -5,13 +5,13 @@ const occupantModel = require('../models/occupant'); // Exported functions //////////////////////////////////////////////////////////////////////////////// function all(req, res) { - const year = req.params.year; + const year = req.params.year; - occupantModel.findAll(req.realm, (errors, occupants) => { - res.json(FD.toAccountingData(year, occupants)); - }); + occupantModel.findAll(req.realm, (errors, occupants) => { + res.json(FD.toAccountingData(year, occupants)); + }); } module.exports = { - all + all, }; diff --git a/backend/managers/contract.js b/backend/managers/contract.js index 4a1c62a..5889834 100644 --- a/backend/managers/contract.js +++ b/backend/managers/contract.js @@ -1,151 +1,221 @@ const moment = require('moment'); const BL = require('../businesslogic'); -const create = contract => { - const supportedFrequencies = ['hours', 'days', 'weeks', 'months', 'years']; - - if (!contract.frequency || supportedFrequencies.indexOf(contract.frequency) === -1) { - throw Error(`unsupported frequency, should be one of these ${supportedFrequencies.join(', ')}`); - } - - if (!contract.properties || contract.properties.length === 0) { - throw Error('properties not defined or empty'); +const create = (contract) => { + const supportedFrequencies = ['hours', 'days', 'weeks', 'months', 'years']; + + if ( + !contract.frequency || + supportedFrequencies.indexOf(contract.frequency) === -1 + ) { + throw Error( + `unsupported frequency, should be one of these ${supportedFrequencies.join( + ', ' + )}` + ); + } + + if (!contract.properties || contract.properties.length === 0) { + throw Error('properties not defined or empty'); + } + + const momentBegin = moment(contract.begin, 'DD/MM/YYYY HH:mm'); + const momentEnd = moment(contract.end, 'DD/MM/YYYY HH:mm'); + let momentTermination; + if (contract.termination) { + momentTermination = moment(contract.termination, 'DD/MM/YYYY HH:mm'); + if (!momentTermination.isBetween(momentBegin, momentEnd, 'minutes', '[]')) { + throw Error('termination date is out of the contract time frame'); } - - const momentBegin = moment(contract.begin, 'DD/MM/YYYY HH:mm'); - const momentEnd = moment(contract.end, 'DD/MM/YYYY HH:mm'); - let momentTermination; - if (contract.termination) { - momentTermination = moment(contract.termination, 'DD/MM/YYYY HH:mm'); - if (!momentTermination.isBetween(momentBegin, momentEnd, 'minutes', '[]')) { - throw Error('termination date is out of the contract time frame'); - } - } - - if (momentEnd.isSameOrBefore(momentBegin)) { - throw Error('contract duration is not correct, check begin/end contract date'); - } - - const terms = Math.round(momentEnd.diff(momentBegin, contract.frequency, true)); - contract = { - ...contract, - terms, - rents: [] - }; - - const current = moment(momentBegin); - let previousRent; - while(current.isSameOrBefore(momentTermination || momentEnd)) { - const rent = BL.computeRent(contract, current.format('DD/MM/YYYY HH:mm'), previousRent); - contract.rents.push(rent); - previousRent = rent; - current.add(1, contract.frequency); - } - return contract; + } + + if (momentEnd.isSameOrBefore(momentBegin)) { + throw Error( + 'contract duration is not correct, check begin/end contract date' + ); + } + + const terms = Math.round( + momentEnd.diff(momentBegin, contract.frequency, true) + ); + contract = { + ...contract, + terms, + rents: [], + }; + + const current = moment(momentBegin); + let previousRent; + while (current.isSameOrBefore(momentTermination || momentEnd)) { + const rent = BL.computeRent( + contract, + current.format('DD/MM/YYYY HH:mm'), + previousRent + ); + contract.rents.push(rent); + previousRent = rent; + current.add(1, contract.frequency); + } + return contract; }; const update = (inputContract, modification) => { - const originalContract = JSON.parse(JSON.stringify(inputContract)); - const modifiedContract = { - ...originalContract, - ...modification - }; - - const momentBegin = moment(modifiedContract.begin, 'DD/MM/YYYY HH:mm'); - const momentEnd = moment(modifiedContract.end, 'DD/MM/YYYY HH:mm'); - let momentTermination; - if (modifiedContract.termination) { - momentTermination = moment(modifiedContract.termination, 'DD/MM/YYYY HH:mm'); - } - - // Check possible lost payments - _checkLostPayments(momentBegin, momentTermination || momentEnd, inputContract); - - const updatedContract = create(modifiedContract); - - if (inputContract.rents) { - inputContract.rents - .filter(rent => _isPayment(rent) || rent.discounts.some(discount => discount.origin === 'settlement')) - .forEach(paidRent => { - payTerm(updatedContract, moment(String(paidRent.term), 'YYYYMMDDHH').format('DD/MM/YYYY HH:mm'), { - payments: paidRent.payments, - discounts: paidRent.discounts.filter(discount => discount.origin === 'settlement') - }); - }); - } - - return updatedContract; + const originalContract = JSON.parse(JSON.stringify(inputContract)); + const modifiedContract = { + ...originalContract, + ...modification, + }; + + const momentBegin = moment(modifiedContract.begin, 'DD/MM/YYYY HH:mm'); + const momentEnd = moment(modifiedContract.end, 'DD/MM/YYYY HH:mm'); + let momentTermination; + if (modifiedContract.termination) { + momentTermination = moment( + modifiedContract.termination, + 'DD/MM/YYYY HH:mm' + ); + } + + // Check possible lost payments + _checkLostPayments( + momentBegin, + momentTermination || momentEnd, + inputContract + ); + + const updatedContract = create(modifiedContract); + + if (inputContract.rents) { + inputContract.rents + .filter( + (rent) => + _isPayment(rent) || + rent.discounts.some((discount) => discount.origin === 'settlement') + ) + .forEach((paidRent) => { + payTerm( + updatedContract, + moment(String(paidRent.term), 'YYYYMMDDHH').format( + 'DD/MM/YYYY HH:mm' + ), + { + payments: paidRent.payments, + discounts: paidRent.discounts.filter( + (discount) => discount.origin === 'settlement' + ), + } + ); + }); + } + + return updatedContract; }; -const renew = contract => { - const momentEnd = moment(contract.end, 'DD/MM/YYYY HH:mm'); - const momentNewEnd = moment(momentEnd).add(contract.terms, contract.frequency); - - return { - ...update(contract, {end: momentNewEnd.format('DD/MM/YYYY HH:mm')}), - terms: contract.terms - }; +const renew = (contract) => { + const momentEnd = moment(contract.end, 'DD/MM/YYYY HH:mm'); + const momentNewEnd = moment(momentEnd).add( + contract.terms, + contract.frequency + ); + + return { + ...update(contract, { end: momentNewEnd.format('DD/MM/YYYY HH:mm') }), + terms: contract.terms, + }; }; const terminate = (inputContract, termination) => { - return update(inputContract, {termination}); + return update(inputContract, { termination }); }; const payTerm = (contract, term, settlements) => { - if (!contract.rents || !contract.rents.length) { - throw Error('cannot pay term, the rents were not generated'); + if (!contract.rents || !contract.rents.length) { + throw Error('cannot pay term, the rents were not generated'); + } + const current = moment(term, 'DD/MM/YYYY HH:mm'); + const momentBegin = moment(contract.begin, 'DD/MM/YYYY HH:mm'); + const momentEnd = moment( + contract.termination || contract.end, + 'DD/MM/YYYY HH:mm' + ); + + if (!current.isBetween(momentBegin, momentEnd, contract.frequency, '[]')) { + throw Error('payment term is out of the contract time frame'); + } + + const previousTerm = moment(current).subtract('1', contract.frequency); + const previousRentIndex = contract.rents.findIndex( + (rent) => rent.term === Number(previousTerm.format('YYYYMMDDHH')) + ); + + let previousRent = + previousRentIndex > -1 ? contract.rents[previousRentIndex] : null; + contract.rents.forEach((rent, index) => { + if (index > previousRentIndex) { + if (index > previousRentIndex + 1) { + const { debts, discounts, payments } = rent; + settlements = { + debts, + discounts: discounts.filter((d) => d.origin === 'settlement'), + payments, + }; + } + contract.rents[index] = BL.computeRent( + contract, + current.format('DD/MM/YYYY HH:mm'), + previousRent, + settlements + ); + previousRent = contract.rents[index]; + current.add(1, contract.frequency); } - const current = moment(term, 'DD/MM/YYYY HH:mm'); - const momentBegin = moment(contract.begin, 'DD/MM/YYYY HH:mm'); - const momentEnd = moment(contract.termination || contract.end, 'DD/MM/YYYY HH:mm'); + }); - if (!current.isBetween(momentBegin, momentEnd, contract.frequency, '[]')) { - throw Error('payment term is out of the contract time frame'); - } - - const previousTerm = moment(current).subtract('1', contract.frequency); - const previousRentIndex = contract.rents.findIndex(rent => rent.term === Number(previousTerm.format('YYYYMMDDHH'))); - - let previousRent = previousRentIndex >-1 ? contract.rents[previousRentIndex] : null; - contract.rents.forEach((rent, index) => { - if (index > previousRentIndex) { - if (index > previousRentIndex + 1) { - const { debts, discounts, payments } = rent; - settlements = { debts, discounts: discounts.filter(d => d.origin === 'settlement'), payments }; - } - contract.rents[index] = BL.computeRent(contract, current.format('DD/MM/YYYY HH:mm'), previousRent, settlements); - previousRent = contract.rents[index]; - current.add(1, contract.frequency); - } - }); - - return contract; + return contract; }; -const _isPayment = rent => { - return !!(rent.payments.length > 0 && rent.payments.some(payment => payment.amount && payment.amount > 0)); +const _isPayment = (rent) => { + return !!( + rent.payments.length > 0 && + rent.payments.some((payment) => payment.amount && payment.amount > 0) + ); }; const _checkLostPayments = (momentBegin, momentEnd, contract) => { - if (!contract.rents || !contract.rents.length) { - return; - } - - const lostPayments = contract.rents - .filter(rent => - !moment(rent.term, 'YYYYMMDDHH').isBetween(momentBegin, momentEnd, contract.frequency, '[]') && - _isPayment(rent)) - .map(rent => String(rent.term) + ' ' + rent.payments.map(payment => payment.amount).join(' + ')); - - if (lostPayments.length > 0) { - throw Error(`Some payments will be lost because they are out of the contract time frame:\n${lostPayments.join('\n')}`); - } + if (!contract.rents || !contract.rents.length) { + return; + } + + const lostPayments = contract.rents + .filter( + (rent) => + !moment(rent.term, 'YYYYMMDDHH').isBetween( + momentBegin, + momentEnd, + contract.frequency, + '[]' + ) && _isPayment(rent) + ) + .map( + (rent) => + String(rent.term) + + ' ' + + rent.payments.map((payment) => payment.amount).join(' + ') + ); + + if (lostPayments.length > 0) { + throw Error( + `Some payments will be lost because they are out of the contract time frame:\n${lostPayments.join( + '\n' + )}` + ); + } }; module.exports = { - create, - update, - terminate, - renew, - payTerm + create, + update, + terminate, + renew, + payTerm, }; diff --git a/backend/managers/documentmanager.js b/backend/managers/documentmanager.js index 66bf2dd..dbde122 100644 --- a/backend/managers/documentmanager.js +++ b/backend/managers/documentmanager.js @@ -11,63 +11,69 @@ const axios = require('axios'); // Exported functions //////////////////////////////////////////////////////////////////////////////// const get = async (req, res) => { - const { language } = req; - const { document, id, term } = req.params; - let url = `${config.PDFGENERATOR_URL}/${document}/${id}`; - if (term) { - url = `${url}/${term}`; - } + const { language } = req; + const { document, id, term } = req.params; + let url = `${config.PDFGENERATOR_URL}/${document}/${id}`; + if (term) { + url = `${url}/${term}`; + } - const response = await axios.get(url, { - responseType: 'stream', - headers: { - 'Accept-Language': language - } - }); + const response = await axios.get(url, { + responseType: 'stream', + headers: { + 'Accept-Language': language, + }, + }); - response.data.pipe(res); + response.data.pipe(res); }; - const update = (req, res) => { - const realm = req.realm; - const occupant = documentModel.schema.filter(req.body); + const realm = req.realm; + const occupant = documentModel.schema.filter(req.body); - if (!occupant.documents) { - occupant.documents = []; - } + if (!occupant.documents) { + occupant.documents = []; + } - occupantModel.findOne(realm, occupant._id, (errors, dbOccupant) => { - if (errors) { - res.json({ - errors: errors - }); - return; - } + occupantModel.findOne(realm, occupant._id, (errors, dbOccupant) => { + if (errors) { + res.json({ + errors: errors, + }); + return; + } - dbOccupant.documents = []; + dbOccupant.documents = []; - occupant.documents.forEach((document) => { - const momentExpirationDate = moment(document.expirationDate, 'DD/MM/YYYY').endOf('day'); - if (document.name && document.name.trim() !== '' && momentExpirationDate.isValid()) { - document.expirationDate = momentExpirationDate.toDate(); - dbOccupant.documents.push(document); - } - }); + occupant.documents.forEach((document) => { + const momentExpirationDate = moment( + document.expirationDate, + 'DD/MM/YYYY' + ).endOf('day'); + if ( + document.name && + document.name.trim() !== '' && + momentExpirationDate.isValid() + ) { + document.expirationDate = momentExpirationDate.toDate(); + dbOccupant.documents.push(document); + } + }); - occupantModel.update(realm, dbOccupant, (errors) => { - if (errors) { - res.json({ - errors: errors - }); - return; - } - res.json(FD.toOccupantData(dbOccupant)); + occupantModel.update(realm, dbOccupant, (errors) => { + if (errors) { + res.json({ + errors: errors, }); + return; + } + res.json(FD.toOccupantData(dbOccupant)); }); + }); }; module.exports = { - get, - update + get, + update, }; diff --git a/backend/managers/emailmanager.js b/backend/managers/emailmanager.js index f1e1817..4a04942 100644 --- a/backend/managers/emailmanager.js +++ b/backend/managers/emailmanager.js @@ -6,74 +6,96 @@ const config = require('../../config'); const occupantModel = require('../models/occupant'); const _sendEmail = async (locale, message) => { - const postData = { - templateName: message.document, - recordId: message.tenantId, - params: { - term: message.term - } - }; + const postData = { + templateName: message.document, + recordId: message.tenantId, + params: { + term: message.term, + }, + }; - try { - const response = await axios.post(config.EMAILER_URL, postData, { - headers: { - 'Accept-Language': locale - } - }); + try { + const response = await axios.post(config.EMAILER_URL, postData, { + headers: { + 'Accept-Language': locale, + }, + }); - logger.info(`POST ${config.EMAILER_URL} ${response.status}`); - logger.debug(`data sent: ${JSON.stringify(postData)}`); - logger.debug(`response: ${JSON.stringify(response.data)}`); + logger.info(`POST ${config.EMAILER_URL} ${response.status}`); + logger.debug(`data sent: ${JSON.stringify(postData)}`); + logger.debug(`response: ${JSON.stringify(response.data)}`); - return response.data.map(({ templateName, recordId, params, email, status, error }) => ({ - document: templateName, - tenantId: recordId, - term: params.term, - email, - status - })); - } catch (error) { - logger.error(`POST ${config.EMAILER_URL} failed`); - logger.error(`data sent: ${JSON.stringify(postData)}`); - logger.error((error.response && error.response.data && error.response.data.message) || error.message); - throw error; - } + return response.data.map( + ({ templateName, recordId, params, email, status /*, error*/ }) => ({ + document: templateName, + tenantId: recordId, + term: params.term, + email, + status, + }) + ); + } catch (error) { + logger.error(`POST ${config.EMAILER_URL} failed`); + logger.error(`data sent: ${JSON.stringify(postData)}`); + logger.error( + (error.response && error.response.data && error.response.data.message) || + error.message + ); + throw error; + } }; module.exports = { - send: async (req, res) => { - try { - const realm = req.realm; - const { document, tenantIds, terms, year, month } = req.body; - const findTenant = promisify(occupantModel.findOne).bind(occupantModel); - const messages = []; - await Promise.all(tenantIds.map(async (tenantId, index) => { - const tenant = await findTenant(realm, tenantId); - messages.push({ - name: tenant.name, - tenantId, - document, - term: Number(terms && terms[index] || moment(`${year}/${month}/01`, 'YYYY/MM/DD').format('YYYYMMDDHH')) - }); - })); - const statusList = await Promise.all(messages.map(async message => { - try { - return await _sendEmail(req.language, message); - } catch (error) { - return [{ - ...message, - error: (error.response && error.response.data) || { status: 500, message: 'Something went wrong'} - }]; - } - })); - const results = statusList.reduce((acc, statuses, index) => { - acc.push(...statuses.map(status => ({ name: messages[index].name, ...status }))); - return acc; - }, []); - res.json(results); - } catch (err) { - logger.error(err); - res.status(500).send(err); - } + send: async (req, res) => { + try { + const realm = req.realm; + const { document, tenantIds, terms, year, month } = req.body; + const findTenant = promisify(occupantModel.findOne).bind(occupantModel); + const messages = []; + await Promise.all( + tenantIds.map(async (tenantId, index) => { + const tenant = await findTenant(realm, tenantId); + messages.push({ + name: tenant.name, + tenantId, + document, + term: Number( + (terms && terms[index]) || + moment(`${year}/${month}/01`, 'YYYY/MM/DD').format('YYYYMMDDHH') + ), + }); + }) + ); + const statusList = await Promise.all( + messages.map(async (message) => { + try { + return await _sendEmail(req.language, message); + } catch (error) { + return [ + { + ...message, + error: (error.response && error.response.data) || { + status: 500, + message: 'Something went wrong', + }, + }, + ]; + } + }) + ); + const results = statusList.reduce((acc, statuses, index) => { + acc.push( + ...statuses.map((status) => ({ + name: messages[index].name, + ...status, + })) + ); + return acc; + }, []); + res.json(results); + } catch (err) { + logger.error(err); + res.status(500).send(err); } -}; \ No newline at end of file + }, +}; diff --git a/backend/managers/frontdata.js b/backend/managers/frontdata.js index 5c96809..9cd0c01 100644 --- a/backend/managers/frontdata.js +++ b/backend/managers/frontdata.js @@ -3,542 +3,654 @@ const math = require('mathjs'); const config = require('../../config'); function toRentData(inputRent, inputOccupant, emailStatus) { - const rent = JSON.parse(JSON.stringify(inputRent)); - const rentMoment = moment(String(rent.term), 'YYYYMMDDHH'); - - let rentToReturn = { - month: rent.month, - year: rent.year, - term: rent.term, - balance: rent.total.balance, - newBalance: rent.total.payment - rent.total.grandTotal, - hasMultiplePayments: !!(rent.payments && rent.payments.length > 1), - payment: rent.total.payment || 0, - payments: rent.payments, - discount: rent.total.discount, - totalAmount: rent.total.grandTotal, - totalWithoutBalanceAmount: rent.total.grandTotal - rent.total.balance, - totalToPay: rent.total.grandTotal, - description: rent.description, - countMonthNotPaid: 0, - paymentStatus: [] - }; - - Object.assign( - rentToReturn, - rent.discounts - .filter(discount => discount.origin === 'settlement') - .reduce((acc, discount) => { - return { - promo: acc.promo + discount.amount, - notepromo: `${acc.notepromo}${discount.description}\n` - }; - }, {promo:0, notepromo:''}) - ); - - Object.assign( - rentToReturn, - rent.debts - .reduce((acc, debt) => { - return { - extracharge: acc.extracharge + debt.amount, - noteextracharge: `${acc.noteextracharge}${debt.description}\n` - }; - }, {extracharge:0, noteextracharge:''}) - ); - - // Get the first vat rate found in vats. - const vatRate = (rent.vats && rent.vats.length) ? rent.vats.filter(vat => vat.origin === 'contract')[0].rate : 0; - if (vatRate) { - if (rentToReturn.promo > 0) { - rentToReturn.promo = Math.round(rentToReturn.promo * ( 1 + vatRate ) * 100) / 100; - } - - if (rentToReturn.extracharge > 0) { - rentToReturn.extracharge = Math.round(rentToReturn.extracharge * ( 1 + vatRate ) * 100) / 100; - } + const rent = JSON.parse(JSON.stringify(inputRent)); + const rentMoment = moment(String(rent.term), 'YYYYMMDDHH'); + + let rentToReturn = { + month: rent.month, + year: rent.year, + term: rent.term, + balance: rent.total.balance, + newBalance: rent.total.payment - rent.total.grandTotal, + hasMultiplePayments: !!(rent.payments && rent.payments.length > 1), + payment: rent.total.payment || 0, + payments: rent.payments, + discount: rent.total.discount, + totalAmount: rent.total.grandTotal, + totalWithoutBalanceAmount: rent.total.grandTotal - rent.total.balance, + totalToPay: rent.total.grandTotal, + description: rent.description, + countMonthNotPaid: 0, + paymentStatus: [], + }; + + Object.assign( + rentToReturn, + rent.discounts + .filter((discount) => discount.origin === 'settlement') + .reduce( + (acc, discount) => { + return { + promo: acc.promo + discount.amount, + notepromo: `${acc.notepromo}${discount.description}\n`, + }; + }, + { promo: 0, notepromo: '' } + ) + ); + + Object.assign( + rentToReturn, + rent.debts.reduce( + (acc, debt) => { + return { + extracharge: acc.extracharge + debt.amount, + noteextracharge: `${acc.noteextracharge}${debt.description}\n`, + }; + }, + { extracharge: 0, noteextracharge: '' } + ) + ); + + // Get the first vat rate found in vats. + const vatRate = + rent.vats && rent.vats.length + ? rent.vats.filter((vat) => vat.origin === 'contract')[0].rate + : 0; + if (vatRate) { + if (rentToReturn.promo > 0) { + rentToReturn.promo = + Math.round(rentToReturn.promo * (1 + vatRate) * 100) / 100; } - Object.assign( - rentToReturn, - rent.discounts - .filter(discount => discount.origin === 'contract') - .reduce((acc, discount) => { - return { - totalWithoutVatAmount: acc.totalWithoutVatAmount - discount.amount - }; - }, {totalWithoutVatAmount: rent.total.preTaxAmount + rent.total.charges}) - ); - - Object.assign( - rentToReturn, - rent.vats - .filter(vat => vat.origin === 'contract') - .reduce((acc, vat) => { - return { - vatAmount: acc.vatAmount + vat.amount - }; - }, {vatAmount: 0}) - ); - - // payment status - rentToReturn.status = ''; - if (rentMoment.isSameOrBefore(moment(), 'month')) { - if (rentToReturn.totalAmount <= 0 || rentToReturn.newBalance >= 0) { - rentToReturn.status = 'paid'; - } else if (rentToReturn.payment > 0) { - rentToReturn.status = 'partiallypaid'; - } else { - rentToReturn.status = 'notpaid'; - } + if (rentToReturn.extracharge > 0) { + rentToReturn.extracharge = + Math.round(rentToReturn.extracharge * (1 + vatRate) * 100) / 100; + } + } + + Object.assign( + rentToReturn, + rent.discounts + .filter((discount) => discount.origin === 'contract') + .reduce( + (acc, discount) => { + return { + totalWithoutVatAmount: acc.totalWithoutVatAmount - discount.amount, + }; + }, + { totalWithoutVatAmount: rent.total.preTaxAmount + rent.total.charges } + ) + ); + + Object.assign( + rentToReturn, + rent.vats + .filter((vat) => vat.origin === 'contract') + .reduce( + (acc, vat) => { + return { + vatAmount: acc.vatAmount + vat.amount, + }; + }, + { vatAmount: 0 } + ) + ); + + // payment status + rentToReturn.status = ''; + if (rentMoment.isSameOrBefore(moment(), 'month')) { + if (rentToReturn.totalAmount <= 0 || rentToReturn.newBalance >= 0) { + rentToReturn.status = 'paid'; + } else if (rentToReturn.payment > 0) { + rentToReturn.status = 'partiallypaid'; + } else { + rentToReturn.status = 'notpaid'; + } + } + + if (inputOccupant) { + // email status + if (emailStatus) { + const computedEmailStatus = { + status: { + rentcall: !!(emailStatus.rentcall && emailStatus.rentcall.length), + rentcall_reminder: !!( + emailStatus.rentcall_reminder && + emailStatus.rentcall_reminder.length + ), + rentcall_last_reminder: !!( + emailStatus.rentcall_last_reminder && + emailStatus.rentcall_last_reminder.length + ), + invoice: !!(emailStatus.invoice && emailStatus.invoice.length), + }, + last: { + rentcall: + (emailStatus.rentcall && + emailStatus.rentcall.length && + emailStatus.rentcall[0]) || + undefined, + rentcall_reminder: + (emailStatus.rentcall_reminder && + emailStatus.rentcall_reminder.length && + emailStatus.rentcall_reminder[0]) || + undefined, + rentcall_last_reminder: + (emailStatus.rentcall_last_reminder && + emailStatus.rentcall_last_reminder.length && + emailStatus.rentcall_last_reminder[0]) || + undefined, + invoice: + (emailStatus.invoice && + emailStatus.invoice.length && + emailStatus.invoice[0]) || + undefined, + }, + count: { + rentcall: (emailStatus.rentcall && emailStatus.rentcall.length) || 0, + rentcall_reminder: + (emailStatus.rentcall_reminder && + emailStatus.rentcall_reminder.length) || + 0, + rentcall_last_reminder: + (emailStatus.rentcall_last_reminder && + emailStatus.rentcall_last_reminder.length) || + 0, + get allRentcall() { + return ( + this.rentcall + + this.rentcall_reminder + + this.rentcall_last_reminder + ); + }, + invoice: (emailStatus.invoice && emailStatus.invoice.length) || 0, + }, + ...emailStatus, + }; + + // if (emailStatus.rentcall_reminder && emailStatus.rentcall_reminder.length) { + // const lastRentCallReminder = emailStatus.rentcall_reminder[0]; + // if (computedEmailStatus.last.rentcall) { + // if (moment(computedEmailStatus.last.rentcall.sentDate).isBefore(moment(lastRentCallReminder.sentDate))){ + // computedEmailStatus.last.rentcall = lastRentCallReminder; + // } + // } else { + // computedEmailStatus.last.rentcall = lastRentCallReminder; + // } + // } + + Object.assign(rentToReturn, { emailStatus: computedEmailStatus }); } + const occupant = toOccupantData(inputOccupant); - if (inputOccupant) { - // email status - if (emailStatus) { - const computedEmailStatus = { - status: { - rentcall: !!(emailStatus.rentcall && emailStatus.rentcall.length), - rentcall_reminder: !!(emailStatus.rentcall_reminder && emailStatus.rentcall_reminder.length), - rentcall_last_reminder: !!(emailStatus.rentcall_last_reminder && emailStatus.rentcall_last_reminder.length), - invoice: !!(emailStatus.invoice && emailStatus.invoice.length) - }, - last: { - rentcall: (emailStatus.rentcall && emailStatus.rentcall.length && emailStatus.rentcall[0]) || undefined, - rentcall_reminder: (emailStatus.rentcall_reminder && emailStatus.rentcall_reminder.length && emailStatus.rentcall_reminder[0]) || undefined, - rentcall_last_reminder: (emailStatus.rentcall_last_reminder && emailStatus.rentcall_last_reminder.length && emailStatus.rentcall_last_reminder[0]) || undefined, - invoice: (emailStatus.invoice && emailStatus.invoice.length && emailStatus.invoice[0]) || undefined - }, - count: { - rentcall: (emailStatus.rentcall && emailStatus.rentcall.length) || 0, - rentcall_reminder: (emailStatus.rentcall_reminder && emailStatus.rentcall_reminder.length) || 0, - rentcall_last_reminder: (emailStatus.rentcall_last_reminder && emailStatus.rentcall_last_reminder.length) || 0, - get allRentcall() { - return this.rentcall + this.rentcall_reminder + this.rentcall_last_reminder; - }, - invoice: (emailStatus.invoice && emailStatus.invoice.length) || 0 - }, - ...emailStatus - }; + Object.assign(rentToReturn, { + _id: occupant._id, + occupant: occupant, + vatRatio: occupant.vatRatio, + uid: `${occupant._id}|${rent.month}|${rent.year}`, + }); - // if (emailStatus.rentcall_reminder && emailStatus.rentcall_reminder.length) { - // const lastRentCallReminder = emailStatus.rentcall_reminder[0]; - // if (computedEmailStatus.last.rentcall) { - // if (moment(computedEmailStatus.last.rentcall.sentDate).isBefore(moment(lastRentCallReminder.sentDate))){ - // computedEmailStatus.last.rentcall = lastRentCallReminder; - // } - // } else { - // computedEmailStatus.last.rentcall = lastRentCallReminder; - // } - // } - - Object.assign(rentToReturn, {emailStatus: computedEmailStatus}); + // count number of month rent not paid + let endCounting = false; + inputOccupant.rents + .reverse() + .filter((currentRent) => { + if ( + moment(String(currentRent.term), 'YYYYMMDDHH').isSameOrBefore( + moment(), + 'month' + ) + ) { + if (endCounting) { + return false; + } + + const { grandTotal, payment } = currentRent.total; + const newBalance = payment - grandTotal; + + if (grandTotal <= 0 || newBalance >= 0) { + endCounting = true; + return false; + } + + if (payment > 0) { + endCounting = true; + } + + return true; } + return false; + }) + .reverse() + .forEach((currentRent) => { + const payment = currentRent.total.payment; + const term = moment(String(currentRent.term), 'YYYYMMDDHH'); + rentToReturn.paymentStatus.push({ + month: term.month() + 1, + status: payment > 0 ? 'partiallypaid' : 'notpaid', + }); + rentToReturn.countMonthNotPaid++; + }); + } - const occupant = toOccupantData(inputOccupant); - - Object.assign( - rentToReturn, - { - _id: occupant._id, - occupant: occupant, - vatRatio: occupant.vatRatio, - uid: `${occupant._id}|${rent.month}|${rent.year}` - } - ); - - // count number of month rent not paid - let endCounting = false; - inputOccupant.rents - .reverse() - .filter(currentRent => { - if (moment(String(currentRent.term), 'YYYYMMDDHH').isSameOrBefore(moment(), 'month')) { - if (endCounting) { - return false; - } - - const { grandTotal, payment } = currentRent.total; - const newBalance = payment - grandTotal; - - if (grandTotal <= 0 || newBalance >= 0) { - endCounting = true; - return false; - } - - if (payment > 0) { - endCounting = true; - } - - return true; - } - return false; - }) - .reverse() - .forEach(currentRent => { - const payment = currentRent.total.payment; - const term = moment(String(currentRent.term), 'YYYYMMDDHH'); - rentToReturn.paymentStatus.push({ - month: term.month() + 1, - status: payment > 0 ? 'partiallypaid' : 'notpaid' - }); - rentToReturn.countMonthNotPaid++; - }); - } - - return rentToReturn; + return rentToReturn; } function toPrintData(realm, doc, fromMonth, month, year, occupants) { - let months = Array.from(Array(13).keys()).slice(1); - if (month) { - month = month > 12 ? 12 : month; - months = [Number(month)]; - } - if (fromMonth) { - fromMonth = fromMonth > 12 ? 12 : fromMonth; - months = Array.from(Array(13).keys()).slice(fromMonth); // [fromMonth,..,12] - } - - const terms = months.map(month => Number(moment(`01/${month}/${year} 00:00`, 'DD/MM/YYYY HH:mm').format('YYYYMMDDHH'))); - const momentToday = moment(); - - const data = { - today: momentToday.format('LL'), - year: momentToday.format('YYYY'), - months: Array.from(Array(13).keys()).slice(1), // [1,2..,12] - occupants: occupants.map(occupant => { - const data = toOccupantData(occupant); - data.rents = occupant.rents - .filter(rent => terms.indexOf(rent.term) !== -1 ) - .map(rent => { - const momentTerm = moment(rent.term, 'YYYYMMDDHH'); - const dueDate = moment(momentTerm).add(10, 'days'); - const dueDay = dueDate.isoWeekday(); - let today = momentToday; - - if ( dueDay === 6 ) { - dueDate.subtract(1, 'days'); - } else if ( dueDay === 7 ) { - dueDate.add(1, 'days'); - } - - if (dueDate.isSameOrBefore(momentToday)) { - today = moment(momentTerm); - const day = today.isoWeekday(); - if ( day === 6 ) { - today.subtract(1, 'days'); - } else if ( day === 7 ) { - today.add(1, 'days'); - } - } - - rent.today = today.format('LL'); - rent.dueDate = dueDate.format('LL'); - rent.callDate = moment(`20/${rent.month}/${year}`, 'DD/MM/YYYY').subtract(1, 'months').format('LL'), - rent.invoiceDate = moment(`20/${rent.month}/${year}`, 'DD/MM/YYYY').format('LL'), - rent.period = moment(`01/${rent.month}/${year}`, 'DD/MM/YYYY').format('MMMM YYYY'), - rent.billingReference = `${momentTerm.format('MM_YY_')}${occupant.reference}`, - rent.total.payment = rent.total.payment || 0; - rent.total.subTotal = rent.total.preTaxAmount + rent.total.debts + rent.total.charges - rent.total.discount; - rent.total.newBalance = rent.total.grandTotal - rent.total.payment; - return rent; - }); - return data; - }), - config, - document: doc, - realm: { - companyInfo: { - name: '?', - legalStructure: '?', - capital: '?', - ein: '?', - dos: '?', - vatNumber: '?', - legalRepresentative: '?' - }, - addresses: [{ - street1: '?', - street2: '?', - zipCode: '?', - city: '?', - state: '?', - country: '?' - }], - bankInfo: { - name: '?', - iban: '?' - }, - contacts: [{ - name: '?', - email: '?', - phone1: '?', - phone2: '?' - }], - ...realm - } - }; + let months = Array.from(Array(13).keys()).slice(1); + if (month) { + month = month > 12 ? 12 : month; + months = [Number(month)]; + } + if (fromMonth) { + fromMonth = fromMonth > 12 ? 12 : fromMonth; + months = Array.from(Array(13).keys()).slice(fromMonth); // [fromMonth,..,12] + } + + const terms = months.map((month) => + Number( + moment(`01/${month}/${year} 00:00`, 'DD/MM/YYYY HH:mm').format( + 'YYYYMMDDHH' + ) + ) + ); + const momentToday = moment(); + + const data = { + today: momentToday.format('LL'), + year: momentToday.format('YYYY'), + months: Array.from(Array(13).keys()).slice(1), // [1,2..,12] + occupants: occupants.map((occupant) => { + const data = toOccupantData(occupant); + data.rents = occupant.rents + .filter((rent) => terms.indexOf(rent.term) !== -1) + .map((rent) => { + const momentTerm = moment(rent.term, 'YYYYMMDDHH'); + const dueDate = moment(momentTerm).add(10, 'days'); + const dueDay = dueDate.isoWeekday(); + let today = momentToday; + + if (dueDay === 6) { + dueDate.subtract(1, 'days'); + } else if (dueDay === 7) { + dueDate.add(1, 'days'); + } + + if (dueDate.isSameOrBefore(momentToday)) { + today = moment(momentTerm); + const day = today.isoWeekday(); + if (day === 6) { + today.subtract(1, 'days'); + } else if (day === 7) { + today.add(1, 'days'); + } + } + + rent.today = today.format('LL'); + rent.dueDate = dueDate.format('LL'); + (rent.callDate = moment(`20/${rent.month}/${year}`, 'DD/MM/YYYY') + .subtract(1, 'months') + .format('LL')), + (rent.invoiceDate = moment( + `20/${rent.month}/${year}`, + 'DD/MM/YYYY' + ).format('LL')), + (rent.period = moment( + `01/${rent.month}/${year}`, + 'DD/MM/YYYY' + ).format('MMMM YYYY')), + (rent.billingReference = `${momentTerm.format('MM_YY_')}${ + occupant.reference + }`), + (rent.total.payment = rent.total.payment || 0); + rent.total.subTotal = + rent.total.preTaxAmount + + rent.total.debts + + rent.total.charges - + rent.total.discount; + rent.total.newBalance = rent.total.grandTotal - rent.total.payment; + return rent; + }); + return data; + }), + config, + document: doc, + realm: { + companyInfo: { + name: '?', + legalStructure: '?', + capital: '?', + ein: '?', + dos: '?', + vatNumber: '?', + legalRepresentative: '?', + }, + addresses: [ + { + street1: '?', + street2: '?', + zipCode: '?', + city: '?', + state: '?', + country: '?', + }, + ], + bankInfo: { + name: '?', + iban: '?', + }, + contacts: [ + { + name: '?', + email: '?', + phone1: '?', + phone2: '?', + }, + ], + ...realm, + }, + }; - if (months) { - data.months = months; - } + if (months) { + data.months = months; + } - return data; + return data; } function toOccupantData(inputOccupant) { - const occupant = JSON.parse(JSON.stringify(inputOccupant)); - - // set default values for occupant - Object.assign( - occupant, - { - frequency: occupant.frequency || 'months', - street1: occupant.street1 || '', - street2: occupant.street2 || '', - zipCode: occupant.zipCode || '', - city: occupant.city || '', - legalForm: occupant.legalForm || '', - siret: occupant.siret || '', - contract: occupant.contract || '', - reference: occupant.reference || '', - guaranty: occupant.guaranty ? Number(occupant.guaranty) : 0, - vatRatio: occupant.vatRatio ? Number(occupant.vatRatio) : 0, - discount: occupant.discount ? Number(occupant.discount) : 0, - rental: 0, - expenses: 0, - total: 0 + const occupant = JSON.parse(JSON.stringify(inputOccupant)); + + // set default values for occupant + Object.assign(occupant, { + frequency: occupant.frequency || 'months', + street1: occupant.street1 || '', + street2: occupant.street2 || '', + zipCode: occupant.zipCode || '', + city: occupant.city || '', + legalForm: occupant.legalForm || '', + siret: occupant.siret || '', + contract: occupant.contract || '', + reference: occupant.reference || '', + guaranty: occupant.guaranty ? Number(occupant.guaranty) : 0, + vatRatio: occupant.vatRatio ? Number(occupant.vatRatio) : 0, + discount: occupant.discount ? Number(occupant.discount) : 0, + rental: 0, + expenses: 0, + total: 0, + }); + + occupant.contactEmails = + occupant.contacts && occupant.contacts.length + ? occupant.contacts.reduce((acc, { email }) => { + if (email) { + return [...acc, email.toLowerCase()]; + } + return acc; + }, []) + : []; + + occupant.hasContactEmails = occupant.contactEmails.length > 0; + + // Compute if contract is completed + occupant.status = 'inprogress'; + occupant.terminated = false; + const currentDate = moment(); + const endMoment = moment( + occupant.terminationDate || occupant.endDate, + 'DD/MM/YYYY' + ); + if (endMoment.isBefore(currentDate, 'day')) { + occupant.terminated = true; + occupant.status = 'stopped'; + } + if (occupant.properties) { + occupant.office = { + surface: 0, + m2Price: 0, + m2Expense: 0, + price: 0, + expense: 0, + }; + occupant.parking = { + price: 0, + expense: 0, + }; + occupant.properties.forEach((item) => { + var property = item.property; + if (property.type === 'parking') { + occupant.parking.price += property.price; + if (property.expense) { + occupant.parking.expense += property.expense; } - ); - - occupant.contactEmails = (occupant.contacts && occupant.contacts.length) ? occupant.contacts.reduce((acc, {email}) => { - if (email) { - return [ - ...acc, - email.toLowerCase() - ]; + } else { + occupant.office.surface += property.surface; + occupant.office.price += property.price; + if (property.expense) { + occupant.office.expense += property.expense; } - return acc; - }, []) : []; - - occupant.hasContactEmails = occupant.contactEmails.length > 0; - - // Compute if contract is completed - occupant.status = 'inprogress'; - occupant.terminated = false; - const currentDate = moment(); - const endMoment = moment(occupant.terminationDate || occupant.endDate, 'DD/MM/YYYY'); - if (endMoment.isBefore(currentDate, 'day')) { - occupant.terminated = true; - occupant.status = 'stopped'; + } + occupant.rental += property.price || 0; + occupant.expenses += property.expense || 0; + }); + occupant.preTaxTotal = + occupant.rental + occupant.expenses - occupant.discount; + occupant.total = occupant.preTaxTotal; + if (occupant.vatRatio) { + occupant.vat = occupant.preTaxTotal * occupant.vatRatio; + occupant.total = occupant.preTaxTotal + occupant.vat; } - if (occupant.properties) { - occupant.office = { - surface: 0, - m2Price: 0, - m2Expense: 0, - price: 0, - expense: 0 - }; - occupant.parking = { - price: 0, - expense: 0 - }; - occupant.properties.forEach((item) => { - var property = item.property; - if (property.type === 'parking') { - occupant.parking.price += property.price; - if (property.expense) { - occupant.parking.expense += property.expense; - } - } else { - occupant.office.surface += property.surface; - occupant.office.price += property.price; - if (property.expense) { - occupant.office.expense += property.expense; - } - } - occupant.rental += property.price || 0; - occupant.expenses += property.expense || 0; - }); - occupant.preTaxTotal = occupant.rental + occupant.expenses - occupant.discount; - occupant.total = occupant.preTaxTotal; - if (occupant.vatRatio) { - occupant.vat = occupant.preTaxTotal * occupant.vatRatio; - occupant.total = occupant.preTaxTotal + occupant.vat; - } - if (occupant.office) { - occupant.office.m2Price = occupant.office.price / occupant.office.surface; - occupant.office.m2Expense = occupant.office.expense / occupant.office.surface; - } + if (occupant.office) { + occupant.office.m2Price = occupant.office.price / occupant.office.surface; + occupant.office.m2Expense = + occupant.office.expense / occupant.office.surface; } - - occupant.hasPayments = occupant.rents ? occupant.rents.some( - rent => (rent.payments && rent.payments.some(payment => payment.amount > 0)) || - rent.discounts.some(discount => discount.origin === 'settlement') - ) : false; - delete occupant.rents; - return occupant; + } + + occupant.hasPayments = occupant.rents + ? occupant.rents.some( + (rent) => + (rent.payments && + rent.payments.some((payment) => payment.amount > 0)) || + rent.discounts.some((discount) => discount.origin === 'settlement') + ) + : false; + delete occupant.rents; + return occupant; } function toAccountingData(year, inputOccupants) { - const beginOfYear = moment(year, 'YYYY').startOf('year'); - const endOfYear = moment(beginOfYear).endOf('year'); - const termsOfYear = Array.from(Array(13).keys()).slice(1).map(month => { - // 2017000000 + 120000 + 100 - // = 2017120100 // YYYYMMDDHH - return (Number(year) * 1000000) + (Number(month) * 10000) + 100; + const beginOfYear = moment(year, 'YYYY').startOf('year'); + const endOfYear = moment(beginOfYear).endOf('year'); + const termsOfYear = Array.from(Array(13).keys()) + .slice(1) + .map((month) => { + // 2017000000 + 120000 + 100 + // = 2017120100 // YYYYMMDDHH + return Number(year) * 1000000 + Number(month) * 10000 + 100; }); - const occupants = JSON.parse(JSON.stringify(inputOccupants)) - .filter(occupant => { + const occupants = JSON.parse(JSON.stringify(inputOccupants)).filter( + (occupant) => { + const beginMoment = moment(occupant.beginDate, 'DD/MM/YYYY'); + const endMoment = moment( + occupant.terminationDate || occupant.endDate, + 'DD/MM/YYYY' + ); + return ( + beginMoment.isBetween(beginOfYear, endOfYear, 'day', '[]') || + endMoment.isBetween(beginOfYear, endOfYear, 'day', '[]') || + (beginMoment.isSameOrBefore(beginOfYear) && + endMoment.isSameOrAfter(endOfYear)) + ); + } + ); + + return { + payments: { + occupants: occupants.map((occupant) => { + return { + year, + occupantId: occupant._id, + name: occupant.name, + reference: occupant.reference, + properties: occupant.properties.map((p) => { + return { name: p.property.name, type: p.property.type }; + }), + beginDate: occupant.beginDate, + endDate: occupant.terminationDate || occupant.endDate, + deposit: occupant.guaranty, + rents: termsOfYear.map((term) => { + let currentRent = occupant.rents.find((rent) => rent.term === term); + if (currentRent) { + currentRent = toRentData(currentRent); + currentRent.occupantId = occupant._id; + } + return currentRent || { inactive: true }; + }), + }; + }), + }, + entriesExists: { + entries: { + occupants: occupants + .filter((occupant) => { const beginMoment = moment(occupant.beginDate, 'DD/MM/YYYY'); - const endMoment = moment(occupant.terminationDate || occupant.endDate, 'DD/MM/YYYY'); - return beginMoment.isBetween(beginOfYear, endOfYear, 'day', '[]') || - endMoment.isBetween(beginOfYear, endOfYear, 'day', '[]') || - (beginMoment.isSameOrBefore(beginOfYear) && endMoment.isSameOrAfter(endOfYear)); - }); - + return beginMoment.isBetween(beginOfYear, endOfYear, 'day', '[]'); + }) + .map((occupant) => { + return { + name: occupant.name, + reference: occupant.reference, + properties: occupant.properties.map((p) => { + return { name: p.property.name, type: p.property.type }; + }), + beginDate: occupant.beginDate, + deposit: occupant.guaranty, + }; + }), + }, + exits: { + occupants: occupants + .filter((occupant) => { + const endMoment = moment( + occupant.terminationDate || occupant.endDate, + 'DD/MM/YYYY' + ); + return endMoment.isBetween(beginOfYear, endOfYear, 'day', '[]'); + }) + .map((occupant) => { + const totalAmount = occupant.rents + .filter((rent) => { + return ( + rent.term >= Number(beginOfYear.format('YYYYMMDDHH')) && + rent.term <= Number(endOfYear.format('YYYYMMDDHH')) + ); + }) + .reduce((acc, rent) => { + let balance = rent.total.grandTotal - rent.total.payment; + return balance !== 0 ? balance * -1 : balance; + }, 0); - return { - payments: { - occupants: occupants.map((occupant) => { - return { - year, - occupantId: occupant._id, - name: occupant.name, - reference: occupant.reference, - properties: occupant.properties.map((p) => { return {name: p.property.name, type: p.property.type};}), - beginDate: occupant.beginDate, - endDate: occupant.terminationDate || occupant.endDate, - deposit: occupant.guaranty, - rents: termsOfYear.map(term => { - let currentRent = occupant.rents.find(rent => rent.term === term); - if (currentRent) { - currentRent = toRentData(currentRent); - currentRent.occupantId = occupant._id; - } - return currentRent || {inactive: true}; - }) - }; - }) - }, - entriesExists: { - entries: { - occupants: occupants - .filter(occupant => { - const beginMoment = moment(occupant.beginDate, 'DD/MM/YYYY'); - return beginMoment.isBetween(beginOfYear, endOfYear, 'day', '[]'); - }) - .map(occupant => { - return { - name: occupant.name, - reference: occupant.reference, - properties: occupant.properties.map((p) => { return {name: p.property.name, type: p.property.type};}), - beginDate: occupant.beginDate, - deposit: occupant.guaranty - }; - }) - }, - exits: { - occupants: occupants - .filter((occupant) => { - const endMoment = moment(occupant.terminationDate || occupant.endDate, 'DD/MM/YYYY'); - return endMoment.isBetween(beginOfYear, endOfYear, 'day', '[]'); - }) - .map((occupant) => { - const totalAmount = occupant.rents - .filter(rent => { - return rent.term >= Number(beginOfYear.format('YYYYMMDDHH')) && - rent.term <= Number(endOfYear.format('YYYYMMDDHH')); - }) - .reduce((acc, rent) => { - let balance = rent.total.grandTotal - rent.total.payment; - return balance!==0?balance*-1:balance; - }, 0); - - return { - name: occupant.name, - reference: occupant.reference, - properties: occupant.properties.map((p) => { return {name: p.property.name, type: p.property.type};}), - leaseBroken: occupant.terminationDate && occupant.terminationDate!==occupant.endDate, - endDate: occupant.terminationDate || occupant.endDate, - deposit: occupant.guaranty, - depositRefund: occupant.guarantyPayback, - totalAmount: totalAmount, - toPay: Number(occupant.guarantyPayback?0:occupant.guaranty) + Number(totalAmount) - }; - }) - } - } - }; + return { + name: occupant.name, + reference: occupant.reference, + properties: occupant.properties.map((p) => { + return { name: p.property.name, type: p.property.type }; + }), + leaseBroken: + occupant.terminationDate && + occupant.terminationDate !== occupant.endDate, + endDate: occupant.terminationDate || occupant.endDate, + deposit: occupant.guaranty, + depositRefund: occupant.guarantyPayback, + totalAmount: totalAmount, + toPay: + Number(occupant.guarantyPayback ? 0 : occupant.guaranty) + + Number(totalAmount), + }; + }), + }, + }, + }; } function toProperty(inputProperty, inputOccupant, inputOccupants) { - const currentDate = moment(); - let property = { - _id: inputProperty._id, - type: inputProperty.type, - name: inputProperty.name, - description: inputProperty.description, - surface: inputProperty.surface, - phone: inputProperty.phone, - digicode: inputProperty.digicode, - address: inputProperty.address, - - price: inputProperty.price, - - beginDate: '', - endDate: '', - lastBusyDay: '', - occupantLabel: '', - available: true, - status: 'vacant', - - // TODO moved in Occupant.properties model - expense: inputProperty.expense || 0, - priceWithExpenses: math.round(inputProperty.price + inputProperty.expense, 2), - m2Expense: inputProperty.surface ? math.round((inputProperty.expense / inputProperty.surface), 2) : null, - m2Price: inputProperty.surface ? math.round((inputProperty.price / inputProperty.surface), 2) : null, - - // TODO to remove, replaced by address - location: inputProperty.location + const currentDate = moment(); + let property = { + _id: inputProperty._id, + type: inputProperty.type, + name: inputProperty.name, + description: inputProperty.description, + surface: inputProperty.surface, + phone: inputProperty.phone, + digicode: inputProperty.digicode, + address: inputProperty.address, + + price: inputProperty.price, + + beginDate: '', + endDate: '', + lastBusyDay: '', + occupantLabel: '', + available: true, + status: 'vacant', + + // TODO moved in Occupant.properties model + expense: inputProperty.expense || 0, + priceWithExpenses: math.round( + inputProperty.price + inputProperty.expense, + 2 + ), + m2Expense: inputProperty.surface + ? math.round(inputProperty.expense / inputProperty.surface, 2) + : null, + m2Price: inputProperty.surface + ? math.round(inputProperty.price / inputProperty.surface, 2) + : null, + + // TODO to remove, replaced by address + location: inputProperty.location, + }; + if (inputOccupant) { + property = { + ...property, + beginDate: inputOccupant.entryDate, + endDate: inputOccupant.exitDate, + lastBusyDay: inputOccupant.terminationDate || inputOccupant.endDate, + occupantLabel: inputOccupant.name, }; - if (inputOccupant) { - property = { - ...property, - beginDate: inputOccupant.entryDate, - endDate: inputOccupant.exitDate, - lastBusyDay: inputOccupant.terminationDate || inputOccupant.endDate, - occupantLabel: inputOccupant.name - }; - if (property.lastBusyDay) { - property.available = moment(property.lastBusyDay, 'DD/MM/YYYY').isBefore(currentDate, 'day'); - if (!property.available) { - property.status = 'occupied'; - } - } - } - property.occupancyHistory = []; - if (inputOccupants && inputOccupants.length) { - property.occupancyHistory = inputOccupants.map(occupant => { - return { - id: occupant._id, - name: occupant.name, - beginDate: occupant.beginDate, - endDate: occupant.terminationDate || occupant.endDate - }; - }); + if (property.lastBusyDay) { + property.available = moment(property.lastBusyDay, 'DD/MM/YYYY').isBefore( + currentDate, + 'day' + ); + if (!property.available) { + property.status = 'occupied'; + } } + } + property.occupancyHistory = []; + if (inputOccupants && inputOccupants.length) { + property.occupancyHistory = inputOccupants.map((occupant) => { + return { + id: occupant._id, + name: occupant.name, + beginDate: occupant.beginDate, + endDate: occupant.terminationDate || occupant.endDate, + }; + }); + } - return property; + return property; } module.exports = { - toOccupantData, - toProperty, - toRentData, - toPrintData, - toAccountingData + toOccupantData, + toProperty, + toRentData, + toPrintData, + toAccountingData, }; diff --git a/backend/managers/leasemanager.js b/backend/managers/leasemanager.js index 1f14ead..e878d08 100644 --- a/backend/managers/leasemanager.js +++ b/backend/managers/leasemanager.js @@ -5,163 +5,162 @@ const occupantModel = require('../models/occupant'); * @returns a Set of leaseId (_id) */ async function _leaseUsedByTenant(realm) { - return await new Promise((resolve, reject) => { - occupantModel.findAll(realm, (errors, occupants) => { - if (errors && errors.length > 0) { - return reject(errors); - } - - resolve(occupants.reduce((acc, { leaseId }) => { - acc.add(leaseId); - return acc; - }, new Set())); - }); + return await new Promise((resolve, reject) => { + occupantModel.findAll(realm, (errors, occupants) => { + if (errors && errors.length > 0) { + return reject(errors); + } + + resolve( + occupants.reduce((acc, { leaseId }) => { + acc.add(leaseId); + return acc; + }, new Set()) + ); }); + }); } function _rejectMissingFields(lease, res) { - if (!lease.name || !lease.timeRange) { - res.status(422).json({ - errors: ['"name", "timeRange" fields are required'] - }); - return true; - } - return false; + if (!lease.name || !lease.timeRange) { + res.status(422).json({ + errors: ['"name", "timeRange" fields are required'], + }); + return true; + } + return false; } //////////////////////////////////////////////////////////////////////////////// // Exported functions //////////////////////////////////////////////////////////////////////////////// function add(req, res) { - const realm = req.realm; - const lease = leaseModel.schema.filter(req.body); - - if (_rejectMissingFields(lease, res)) { - return; + const realm = req.realm; + const lease = leaseModel.schema.filter(req.body); + + if (_rejectMissingFields(lease, res)) { + return; + } + delete lease._id; + + leaseModel.add(realm, lease, async (errors, dbLease) => { + if (errors) { + return res.status(500).json({ + errors: errors, + }); } - delete lease._id; - - leaseModel.add(realm, lease, async (errors, dbLease) => { - if (errors) { - return res.status(500).json({ - errors: errors - }); - } - const setOfUsedLeases = await _leaseUsedByTenant(realm); - dbLease.usedByTenants = setOfUsedLeases.has(dbLease._id); - res.json(dbLease); - }); + const setOfUsedLeases = await _leaseUsedByTenant(realm); + dbLease.usedByTenants = setOfUsedLeases.has(dbLease._id); + res.json(dbLease); + }); } async function update(req, res) { - const realm = req.realm; - let lease = leaseModel.schema.filter(req.body); - - if (_rejectMissingFields(lease, res)) { - return; - } - - const setOfUsedLeases = await _leaseUsedByTenant(realm); - if (setOfUsedLeases.has(lease._id)) { - // if lease already used by tenants, only allow to update name, description, active fields - const dbLease = await new Promise((resolve, reject) => { - leaseModel.findOne(realm, lease._id, async (errors, dbLease) => { - if (errors && errors.length > 0) { - console.error(errors); - return resolve(); - } - resolve(dbLease); - }); - }); - if (!dbLease) { - return res.sendStatus(404); - } - lease = { - ...dbLease, - name: lease.name || dbLease.name, - description: lease.description || dbLease.description, - active: lease.active !== undefined ? lease.active : dbLease.active - }; - } - - leaseModel.update(realm, lease, (errors, dbLease) => { - if (errors) { - return res.status(500).json({ - errors: errors - }); + const realm = req.realm; + let lease = leaseModel.schema.filter(req.body); + + if (_rejectMissingFields(lease, res)) { + return; + } + + const setOfUsedLeases = await _leaseUsedByTenant(realm); + if (setOfUsedLeases.has(lease._id)) { + // if lease already used by tenants, only allow to update name, description, active fields + const dbLease = await new Promise((resolve /*, reject*/) => { + leaseModel.findOne(realm, lease._id, async (errors, dbLease) => { + if (errors && errors.length > 0) { + console.error(errors); + return resolve(); } - dbLease.usedByTenants = setOfUsedLeases.has(lease._id); - res.json(dbLease); + resolve(dbLease); + }); }); + if (!dbLease) { + return res.sendStatus(404); + } + lease = { + ...dbLease, + name: lease.name || dbLease.name, + description: lease.description || dbLease.description, + active: lease.active !== undefined ? lease.active : dbLease.active, + }; + } + + leaseModel.update(realm, lease, (errors, dbLease) => { + if (errors) { + return res.status(500).json({ + errors: errors, + }); + } + dbLease.usedByTenants = setOfUsedLeases.has(lease._id); + res.json(dbLease); + }); } async function remove(req, res) { - const realm = req.realm; - const leaseIds = req.params.ids.split(','); + const realm = req.realm; + const leaseIds = req.params.ids.split(','); - const setOfUsedLeases = await _leaseUsedByTenant(realm); - if (leaseIds.some(lease => setOfUsedLeases.has(lease._id))) { - return res.status(422).json({ - errors: ['One lease is used by tenants. It cannot be removed'] - }); - } + const setOfUsedLeases = await _leaseUsedByTenant(realm); + if (leaseIds.some((lease) => setOfUsedLeases.has(lease._id))) { + return res.status(422).json({ + errors: ['One lease is used by tenants. It cannot be removed'], + }); + } - leaseModel.remove( - realm, - leaseIds, - errors => { - if (errors) { - return res.status(500).json({ - errors - }); - } - res.sendStatus(200); - } - ); + leaseModel.remove(realm, leaseIds, (errors) => { + if (errors) { + return res.status(500).json({ + errors, + }); + } + res.sendStatus(200); + }); } function all(req, res) { - const realm = req.realm; - leaseModel.findAll(realm, async (errors, dbLeases) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - const setOfUsedLeases = await _leaseUsedByTenant(realm); - const allLeases = dbLeases.map(dbLease => ({ - ...dbLease, - usedByTenants: setOfUsedLeases.has(dbLease._id) - })); - - const systemLeases = allLeases.filter(lease => lease.system).sort((l1, l2) => l1.name.localeCompare(l2.name)); - const otherLeases = allLeases.filter(lease => !lease.system).sort((l1, l2) => l1.name.localeCompare(l2.name)); - res.json([ - ...systemLeases, - ...otherLeases - ]); - }); + const realm = req.realm; + leaseModel.findAll(realm, async (errors, dbLeases) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, + }); + } + const setOfUsedLeases = await _leaseUsedByTenant(realm); + const allLeases = dbLeases.map((dbLease) => ({ + ...dbLease, + usedByTenants: setOfUsedLeases.has(dbLease._id), + })); + + const systemLeases = allLeases + .filter((lease) => lease.system) + .sort((l1, l2) => l1.name.localeCompare(l2.name)); + const otherLeases = allLeases + .filter((lease) => !lease.system) + .sort((l1, l2) => l1.name.localeCompare(l2.name)); + res.json([...systemLeases, ...otherLeases]); + }); } function one(req, res) { - const realm = req.realm; - const leaseId = req.params.id; - leaseModel.findOne(realm, leaseId, async (errors, dbLease) => { - if (errors && errors.length > 0) { - return res.status(404).json({ - errors: errors - }); - } - const setOfUsedLeases = await _leaseUsedByTenant(realm); - dbLease.usedByTenants = setOfUsedLeases.has(dbLease._id); - res.json(dbLease); - }); + const realm = req.realm; + const leaseId = req.params.id; + leaseModel.findOne(realm, leaseId, async (errors, dbLease) => { + if (errors && errors.length > 0) { + return res.status(404).json({ + errors: errors, + }); + } + const setOfUsedLeases = await _leaseUsedByTenant(realm); + dbLease.usedByTenants = setOfUsedLeases.has(dbLease._id); + res.json(dbLease); + }); } module.exports = { - add, - update, - remove, - one, - all + add, + update, + remove, + one, + all, }; diff --git a/backend/managers/loginmanager.js b/backend/managers/loginmanager.js index b3b79e6..600bb4a 100644 --- a/backend/managers/loginmanager.js +++ b/backend/managers/loginmanager.js @@ -9,359 +9,380 @@ const accountModel = require('../models/account'); const realmModel = require('../models/realm'); const ResponseTypes = { - SUCCESS: 'success', - DB_ERROR: 'db-error', - ENCRYPT_ERROR: 'encrypt-error', - MISSING_FIELD: 'missing-field', - USER_NOT_FOUND: 'login-user-not-found', - INVALID_PASSWORD: 'login-invalid-password', - INVALID_REALM: 'login-invalid-realm', - REALM_NOT_FOUND: 'login-realm-not-found', - REALM_TAKEN: 'signup-realm-taken', - EMAIL_TAKEN: 'signup-email-taken' + SUCCESS: 'success', + DB_ERROR: 'db-error', + ENCRYPT_ERROR: 'encrypt-error', + MISSING_FIELD: 'missing-field', + USER_NOT_FOUND: 'login-user-not-found', + INVALID_PASSWORD: 'login-invalid-password', + INVALID_REALM: 'login-invalid-realm', + REALM_NOT_FOUND: 'login-realm-not-found', + REALM_TAKEN: 'signup-realm-taken', + EMAIL_TAKEN: 'signup-email-taken', }; function _createRealm(name, email, callback) { - const newRealm = { - name: name, - creation: new Date(), - administrator: email - }; - - realmModel.add(newRealm, (err, dbRealm) => { - if (callback) { - callback(err, dbRealm); - } - }); + const newRealm = { + name: name, + creation: new Date(), + administrator: email, + }; + + realmModel.add(newRealm, (err, dbRealm) => { + if (callback) { + callback(err, dbRealm); + } + }); } function _checkRealmAccess(email, callback) { - realmModel.findByEmail(email, (err, realms) => { - if (err) { - callback(err); - return; - } + realmModel.findByEmail(email, (err, realms) => { + if (err) { + callback(err); + return; + } - if (!realms) { - realms = []; - } + if (!realms) { + realms = []; + } - callback(null, realms); - }); + callback(null, realms); + }); } -function _createAccountIfNotExist(firstname, lastname, email, realmName, password, callback) { - function checkNotExistingAccount(email, done) { - accountModel.findOne(email, (err, account) => { - if (err) { - logger.error(ResponseTypes.DB_ERROR); - done({status: ResponseTypes.DB_ERROR}); - return; - } - if (account) { - logger.info(ResponseTypes.EMAIL_TAKEN); - done({status: ResponseTypes.EMAIL_TAKEN}); - return; - } - // no account found - done(); - }); - } +function _createAccountIfNotExist( + firstname, + lastname, + email, + realmName, + password, + callback +) { + function checkNotExistingAccount(email, done) { + accountModel.findOne(email, (err, account) => { + if (err) { + logger.error(ResponseTypes.DB_ERROR); + done({ status: ResponseTypes.DB_ERROR }); + return; + } + if (account) { + logger.info(ResponseTypes.EMAIL_TAKEN); + done({ status: ResponseTypes.EMAIL_TAKEN }); + return; + } + // no account found + done(); + }); + } + + function createAccount(firstname, lastname, email, password, done) { + bcrypt.hash(password, 10, (err, hash) => { + if (err) { + logger.error(ResponseTypes.ENCRYPT_ERROR + ': ' + err); + done({ status: ResponseTypes.ENCRYPT_ERROR }); + return; + } + + const account = { + email: email.toLowerCase(), + password: hash, + firstname: firstname, + lastname: lastname, + }; + accountModel.add(account, done); + }); + } - function createAccount(firstname, lastname, email, password, done) { - bcrypt.hash(password, 10, (err, hash) => { - if (err) { - logger.error(ResponseTypes.ENCRYPT_ERROR + ': ' + err); - done({status: ResponseTypes.ENCRYPT_ERROR}); - return; - } - - const account = { - email: email.toLowerCase(), - password: hash, - firstname: firstname, - lastname: lastname - }; - accountModel.add(account, done); - }); + checkNotExistingAccount(email, (err) => { + if (err) { + callback(err); + return; } - - checkNotExistingAccount(email, (err) => { - if (err) { - callback(err); - return; + createAccount(firstname, lastname, email, password, (err) => { + if (err) { + logger.error(ResponseTypes.DB_ERROR + ': ' + err); + callback({ status: ResponseTypes.DB_ERROR }); + return; + } + logger.info('Create realm on the fly for new user ' + email); + _createRealm(realmName || '__default_', email, (error, newRealm) => { + if (error) { + logger.info('Login failed ' + ResponseTypes.DB_ERROR); + callback({ status: ResponseTypes.DB_ERROR }); + return; } - createAccount(firstname, lastname, email, password, (err) => { - if (err) { - logger.error(ResponseTypes.DB_ERROR + ': ' + err); - callback({status: ResponseTypes.DB_ERROR}); - return; - } - logger.info('Create realm on the fly for new user ' + email); - _createRealm(realmName || '__default_', email, (error, newRealm) => { - if (error) { - logger.info('Login failed ' + ResponseTypes.DB_ERROR); - callback({status: ResponseTypes.DB_ERROR}); - return; - } - callback(null, { - account: { - firstname: firstname, - lastname: lastname, - email: email, - realm: newRealm - }, - status: ResponseTypes.SUCCESS - }); - }); + callback(null, { + account: { + firstname: firstname, + lastname: lastname, + email: email, + realm: newRealm, + }, + status: ResponseTypes.SUCCESS, }); + }); }); + }); } //////////////////////////////////////////////////////////////////////////////// // Exported functions //////////////////////////////////////////////////////////////////////////////// const loginManager = { - authenticate(email, password, done) { - if (!email || !password) { - logger.info('Login failed ' + ResponseTypes.MISSING_FIELD); - done(ResponseTypes.MISSING_FIELD); - return; + authenticate(email, password, done) { + if (!email || !password) { + logger.info('Login failed ' + ResponseTypes.MISSING_FIELD); + done(ResponseTypes.MISSING_FIELD); + return; + } + + email = email.toLowerCase(); + + accountModel.findOne(email, (err, account) => { + if (err) { + logger.info('Login failed ' + ResponseTypes.DB_ERROR); + done(ResponseTypes.DB_ERROR); + return; + } + + if (!account) { + logger.info('Login failed ' + ResponseTypes.USER_NOT_FOUND); + done(ResponseTypes.USER_NOT_FOUND); + return; + } + + bcrypt.compare(password, account.password, (error, status) => { + if (error) { + logger.info('Login failed ' + ResponseTypes.ENCRYPT_ERROR); + done(ResponseTypes.ENCRYPT_ERROR); + return; } - email = email.toLowerCase(); + if (status !== true) { + logger.info('Login failed ' + ResponseTypes.INVALID_PASSWORD); + done(ResponseTypes.INVALID_PASSWORD); + return; + } - accountModel.findOne(email, (err, account) => { - if (err) { - logger.info('Login failed ' + ResponseTypes.DB_ERROR); - done(ResponseTypes.DB_ERROR); - return; - } + logger.info('Login successful ' + email); + _checkRealmAccess(email, (err, realms) => { + if (err) { + logger.error(ResponseTypes.DB_ERROR + ': ' + err); + done(ResponseTypes.DB_ERROR); + return; + } - if (!account) { - logger.info('Login failed ' + ResponseTypes.USER_NOT_FOUND); - done(ResponseTypes.USER_NOT_FOUND); - return; - } - - bcrypt.compare(password, account.password, (error, status) => { - if (error) { - logger.info('Login failed ' + ResponseTypes.ENCRYPT_ERROR); - done(ResponseTypes.ENCRYPT_ERROR); - return; - } - - if (status !== true) { - logger.info('Login failed ' + ResponseTypes.INVALID_PASSWORD); - done(ResponseTypes.INVALID_PASSWORD); - return; - } - - logger.info('Login successful ' + email); - _checkRealmAccess(email, (err, realms) => { - if (err) { - logger.error(ResponseTypes.DB_ERROR + ': ' + err); - done(ResponseTypes.DB_ERROR); - return; - } - - if (realms.length === 0) { - logger.error('No realm found for ' + email); - done(ResponseTypes.REALM_NOT_FOUND); - return; - } - - logger.info('Found ' + realms.length + ' realms for ' + email); - const user = { - firstname: account.firstname, - lastname: account.lastname, - email: email, - realms: realms, - realm: realms.length>0 ? realms[0] : undefined - }; - done(null, user); - }); - }); + if (realms.length === 0) { + logger.error('No realm found for ' + email); + done(ResponseTypes.REALM_NOT_FOUND); + return; + } + + logger.info('Found ' + realms.length + ' realms for ' + email); + const user = { + firstname: account.firstname, + lastname: account.lastname, + email: email, + realms: realms, + realm: realms.length > 0 ? realms[0] : undefined, + }; + done(null, user); }); - }, - - logout(req) { - req.logout(); - req.session = null; - }, - - selectRealm(req, res) { - realmModel.findOne(req.params.id, (err, realm) => { - if (err) { - res.json({ - status: ResponseTypes.DB_ERROR - }); - return; - } - req.session.realmId = realm._id; - logger.info('Switch to realm ' + realm.name + ' for ' + req.user.email); - res.json({ - status: ResponseTypes.SUCCESS - }); + }); + }); + }, + + logout(req) { + req.logout(); + req.session = null; + }, + + selectRealm(req, res) { + realmModel.findOne(req.params.id, (err, realm) => { + if (err) { + res.json({ + status: ResponseTypes.DB_ERROR, }); - }, + return; + } + req.session.realmId = realm._id; + logger.info('Switch to realm ' + realm.name + ' for ' + req.user.email); + res.json({ + status: ResponseTypes.SUCCESS, + }); + }); + }, + + getUserByEmail(email, callback) { + accountModel.findOne(email, (err, user) => { + if (err) { + callback(err); + return; + } + callback(err, user); + }); + }, + + updateRequestWithUserFromRefreshToken(req, res, next) { + const refreshToken = req.cookies.refreshToken; + + if (refreshToken) { + try { + const decoded = jwt.verify(refreshToken, config.REFRESH_TOKEN_SECRET); + req.user = decoded.account; + } catch (err) { + logger.warn(err); + } + } + next(); + }, + + updateRequestWithRealmsOfUser(req, res, next) { + if (!req.user) { + delete req.realm; + delete req.realms; + return next(); + } - getUserByEmail(email, callback) { - accountModel.findOne(email, (err, user) => { - if (err) { - callback(err); - return; - } - callback(err, user); - }); - }, - - updateRequestWithUserFromRefreshToken(req, res, next) { - const refreshToken = req.cookies.refreshToken; - - if (refreshToken) { - try { - const decoded = jwt.verify(refreshToken, config.REFRESH_TOKEN_SECRET); - req.user = decoded.account; - } catch (err) { - logger.warn(err); - } + realmModel.findByEmail(req.user.email, (err, realms) => { + if (err) { + return next(err); + } + if (realms) { + req.realms = realms; + if (req.session && req.session.realmId) { + const filteredDbRealms = req.realms.filter( + (realm) => realm._id.toString() === req.session.realmId + ); + if (filteredDbRealms.length > 0) { + req.realm = filteredDbRealms[0]; + } } - next(); - }, - - updateRequestWithRealmsOfUser(req, res, next) { - if (!req.user) { - delete req.realm; - delete req.realms; - return next(); + if (!req.realm) { + req.realm = realms[0]; } - - realmModel.findByEmail(req.user.email, (err, realms) => { - if (err) { - return next(err); - } - if (realms) { - req.realms = realms; - if (req.session && req.session.realmId) { - const filteredDbRealms = req.realms.filter(realm => - realm._id.toString() === req.session.realmId - ); - if (filteredDbRealms.length > 0) { - req.realm = filteredDbRealms[0]; - } - } - if (!req.realm) { - req.realm = realms[0]; - } - } else { - delete req.realms; - } - next(); - }); - } + } else { + delete req.realms; + } + next(); + }); + }, }; if (config.signup) { - loginManager.signup = function(req, res) { - const email = req.param('email'); - const password = req.param('password'); - const firstname = req.param('firstname'); - const lastname = req.param('lastname'); - - logger.info('Request new account: ' + email); + loginManager.signup = function (req, res) { + const email = req.param('email'); + const password = req.param('password'); + const firstname = req.param('firstname'); + const lastname = req.param('lastname'); + + logger.info('Request new account: ' + email); + + if (!email || !password || !firstname || !lastname) { + res.json({ + status: ResponseTypes.MISSING_FIELD, + }); + logger.info(ResponseTypes.MISSING_FIELD); + return; + } - if (!email || !password || !firstname || !lastname) { - res.json({ - status: ResponseTypes.MISSING_FIELD - }); - logger.info(ResponseTypes.MISSING_FIELD); - return; + _createAccountIfNotExist( + firstname, + lastname, + email.toLowerCase(), + null, + password, + (err, account) => { + if (err) { + res.json(err); + return; } - - _createAccountIfNotExist(firstname, lastname, email.toLowerCase(), null, password, (err, account) => { - if (err) { - res.json(err); - return; - } - res.json(account); - }); - }; + res.json(account); + } + ); + }; } if (config.demoMode) { - loginManager.loginDemo = function(req, res) { - const firstname = 'Camel'; - const lastname = 'Aissani'; - const email = 'demo@demo.com'; - const realmName = 'demo'; - const password = 'demo'; - - function logIn(user) { - req.logIn(user, (err) => { - if (err) { - logger.info('Login failed ' + err); - res.redirect('/'); - return; - } - logger.info('Login successful ' + email); - res.redirect('/signedin'); - }); + loginManager.loginDemo = function (req, res) { + const firstname = 'Camel'; + const lastname = 'Aissani'; + const email = 'demo@demo.com'; + const realmName = 'demo'; + const password = 'demo'; + + function logIn(user) { + req.logIn(user, (err) => { + if (err) { + logger.info('Login failed ' + err); + res.redirect('/'); + return; } + logger.info('Login successful ' + email); + res.redirect('/signedin'); + }); + } - _createAccountIfNotExist(firstname, lastname, email, realmName, password, (err, account) => { - if ((err && err.status !== ResponseTypes.EMAIL_TAKEN) && !account) { - logger.info('Login failed', err); + _createAccountIfNotExist( + firstname, + lastname, + email, + realmName, + password, + (err, account) => { + if (err && err.status !== ResponseTypes.EMAIL_TAKEN && !account) { + logger.info('Login failed', err); + res.redirect('/'); + return; + } + + _checkRealmAccess(email, (err, realms) => { + if (err) { + logger.info('Login failed', ResponseTypes.DB_ERROR, err); + res.redirect('/'); + return; + } + + const user = { + firstname, + lastname, + email, + }; + + if (realms.length === 0) { + _createRealm('demo', 'demo@demo.com', (err) => { + if (err) { + logger.info('failed to create realm ' + ResponseTypes.DB_ERROR); res.redirect('/'); return; - } - - _checkRealmAccess(email, (err, realms) => { - if (err) { - logger.info('Login failed', ResponseTypes.DB_ERROR, err); - res.redirect('/'); - return; - } - - const user = { - firstname, - lastname, - email - }; - - if (realms.length === 0) { - _createRealm('demo', 'demo@demo.com', (err) => { - if (err) { - logger.info('failed to create realm ' + ResponseTypes.DB_ERROR); - res.redirect('/'); - return; - } - logIn(user); - }); - } else { - logIn(user); - } + } + logIn(user); }); + } else { + logIn(user); + } }); - }; + } + ); + }; } else { - loginManager.login = function(req, res, next) { - passport.authenticate('local', (err, user, info) => { - if (err) { - return next(err); - } - if (!user) { - res.json({status: info.message}); - return; - } - req.logIn(user, (err) => { - if (err) { - return next(err); - } - res.json({status: ResponseTypes.SUCCESS}); - }); - })(req, res, next); - }; + loginManager.login = function (req, res, next) { + passport.authenticate('local', (err, user, info) => { + if (err) { + return next(err); + } + if (!user) { + res.json({ status: info.message }); + return; + } + req.logIn(user, (err) => { + if (err) { + return next(err); + } + res.json({ status: ResponseTypes.SUCCESS }); + }); + })(req, res, next); + }; } module.exports = loginManager; diff --git a/backend/managers/notificationmanager.js b/backend/managers/notificationmanager.js index 0095c28..90edddc 100644 --- a/backend/managers/notificationmanager.js +++ b/backend/managers/notificationmanager.js @@ -10,165 +10,195 @@ const notificationModel = require('../models/notification'); sugar.extend(); function _buildViewData(currentDate, notifications) { - notifications.forEach((notification) => { - if (!notification.expirationDate) { - notification.expired = true; - } else { - const currentMoment = moment(currentDate).endOf('day'); - const expirationMoment = moment(notification.expirationDate).endOf('day'); - notification.expired = currentMoment.isAfter(expirationMoment); - } - }); + notifications.forEach((notification) => { + if (!notification.expirationDate) { + notification.expired = true; + } else { + const currentMoment = moment(currentDate).endOf('day'); + const expirationMoment = moment(notification.expirationDate).endOf('day'); + notification.expired = currentMoment.isAfter(expirationMoment); + } + }); - return notifications; + return notifications; } //////////////////////////////////////////////////////////////////////////////// // Exported functions //////////////////////////////////////////////////////////////////////////////// function generateId(name) { - return crypto.createHash('md5').update(name).digest('hex'); + return crypto.createHash('md5').update(name).digest('hex'); } const feeders = [ - - //function expiredDocuments(t, realm, callback) { - (t, realm, callback) => { - const notifications = []; - occupantModel.findAll(realm, (errors, occupants) => { - if (errors || (occupants && occupants.length === 0)) { - callback(notifications); - return; - } - - occupants.forEach((occupant) => { - if (occupant.documents && occupant.documents.length > 0) { - occupant.documents.forEach((document) => { - notifications.push({ - type: 'expiredDocument', - notificationId: generateId(occupant._id.toString() + '_document_' + moment(document.expirationDate).format('DD-MM-YYYY') + document.name), - expirationDate: document.expirationDate, - title: occupant.name, - description: t('has expired', { document: document.name, date: moment(document.expirationDate).format('L'), interpolation: { escape: false } }), - actionUrl: '' - }); - }); - } else if (!occupant.terminationDate && occupant.properties) { - occupant.properties.some((p) => { - if (p.property.type !== 'letterbox' && p.property.type !== 'parking') { - notifications.push({ - type: 'warning', - notificationId: generateId(occupant._id.toString() + '_no_document'), - title: occupant.name, - description: t('There are no documents attached to the lease contract. Is the insurance certficate is missing?'), - actionUrl: '' - }); - return true; - } - return false; - }); - } + //function expiredDocuments(t, realm, callback) { + (t, realm, callback) => { + const notifications = []; + occupantModel.findAll(realm, (errors, occupants) => { + if (errors || (occupants && occupants.length === 0)) { + callback(notifications); + return; + } + + occupants.forEach((occupant) => { + if (occupant.documents && occupant.documents.length > 0) { + occupant.documents.forEach((document) => { + notifications.push({ + type: 'expiredDocument', + notificationId: generateId( + occupant._id.toString() + + '_document_' + + moment(document.expirationDate).format('DD-MM-YYYY') + + document.name + ), + expirationDate: document.expirationDate, + title: occupant.name, + description: t('has expired', { + document: document.name, + date: moment(document.expirationDate).format('L'), + interpolation: { escape: false }, + }), + actionUrl: '', }); - callback(notifications); - - }); - }, - //function chequesToCollect(t, realm, callback) { - (t, realm, callback) => { - callback([]); - } + }); + } else if (!occupant.terminationDate && occupant.properties) { + occupant.properties.some((p) => { + if ( + p.property.type !== 'letterbox' && + p.property.type !== 'parking' + ) { + notifications.push({ + type: 'warning', + notificationId: generateId( + occupant._id.toString() + '_no_document' + ), + title: occupant.name, + description: t( + 'There are no documents attached to the lease contract. Is the insurance certficate is missing?' + ), + actionUrl: '', + }); + return true; + } + return false; + }); + } + }); + callback(notifications); + }); + }, + //function chequesToCollect(t, realm, callback) { + (t, realm, callback) => { + callback([]); + }, ]; function all(req, res) { - const realm = req.realm; - const notifications = []; - - function feederLoop(index, endLoopCallback) { - if (index < feeders.length) { - const feederFct = feeders[index]; - feederFct(req.t, realm, (foundNotifications) => { - foundNotifications.forEach((notification) => { - notifications.push(notification); - }); - index++; - feederLoop(index, endLoopCallback); - }); - } else { - endLoopCallback(); - } + const realm = req.realm; + const notifications = []; + + function feederLoop(index, endLoopCallback) { + if (index < feeders.length) { + const feederFct = feeders[index]; + feederFct(req.t, realm, (foundNotifications) => { + foundNotifications.forEach((notification) => { + notifications.push(notification); + }); + index++; + feederLoop(index, endLoopCallback); + }); + } else { + endLoopCallback(); } + } - feederLoop(0, () => { - notificationModel.findAll(realm, (errors, dbNotifications) => { - if (errors) { - res.json({ - errors: errors - }); - return; - } - if (dbNotifications && dbNotifications.length > 0 && notifications && notifications.length > 0) { - dbNotifications.forEach((dbNotification) => { - notifications.forEach((notification) => { - if (dbNotification._id === notification._id) { - notification.changes = dbNotification.changes; - } - }); - }); + feederLoop(0, () => { + notificationModel.findAll(realm, (errors, dbNotifications) => { + if (errors) { + res.json({ + errors: errors, + }); + return; + } + if ( + dbNotifications && + dbNotifications.length > 0 && + notifications && + notifications.length > 0 + ) { + dbNotifications.forEach((dbNotification) => { + notifications.forEach((notification) => { + if (dbNotification._id === notification._id) { + notification.changes = dbNotification.changes; } - res.json(_buildViewData(new Date(), notifications)); + }); }); + } + res.json(_buildViewData(new Date(), notifications)); }); + }); } function update(req, res) { - const date = new Date(); - const user = req.user; - const realm = user.realm; - const notification = notificationModel.schema.filter(req.body); + const date = new Date(); + const user = req.user; + const realm = user.realm; + const notification = notificationModel.schema.filter(req.body); + + notification.findOne(realm, notification._id, (errors, dbNotification) => { + if (errors) { + res.json({ + errors: errors, + }); + return; + } - notification.findOne(realm, notification._id, (errors, dbNotification) => { - if (errors) { - res.json({ - errors: errors - }); - return; - } + if (dbNotification) { + delete dbNotification._id; + } else { + dbNotification = {}; + } - if (dbNotification) { - delete dbNotification._id; - } else { - dbNotification = {}; - } + if (!dbNotification.changes) { + dbNotification.changes = []; + } + dbNotification.changes.push({ + date: date, + email: user.email, + status: notification.status, + }); - if (!dbNotification.changes) { - dbNotification.changes = []; + notificationModel.upsert( + realm, + { + _id: notification._id, + }, + dbNotification, + null, + (errors) => { + if (errors) { + res.json({ + errors: errors, + }); + return; } - dbNotification.changes.push({ - date: date, - email: user.email, - status: notification.status - }); - notificationModel.upsert(realm, { - _id: notification._id - }, dbNotification, null, (errors) => { - if (errors) { - res.json({ - errors: errors - }); - return; - } - - res.json(_buildViewData(date, Object.merge(dbNotification, { - _id: notification._id - }))); - }); - }); + res.json( + _buildViewData( + date, + Object.merge(dbNotification, { + _id: notification._id, + }) + ) + ); + } + ); + }); } module.exports = { - update, - all, - generateId, - feeders, + update, + all, + generateId, + feeders, }; diff --git a/backend/managers/occupantmanager.js b/backend/managers/occupantmanager.js index e823f42..aafa206 100644 --- a/backend/managers/occupantmanager.js +++ b/backend/managers/occupantmanager.js @@ -9,326 +9,359 @@ const propertyModel = require('../models/property'); const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', 12); function _buildPropertyMap(realm, callback) { - propertyModel.findAll(realm, (errors, properties) => { - const propertyMap = {}; - if (properties) { - properties.reduce((acc, property) => { - acc[property._id.toString()] = property; - return acc; - }, propertyMap); - } - callback(errors, propertyMap); - }); + propertyModel.findAll(realm, (errors, properties) => { + const propertyMap = {}; + if (properties) { + properties.reduce((acc, property) => { + acc[property._id.toString()] = property; + return acc; + }, propertyMap); + } + callback(errors, propertyMap); + }); } //////////////////////////////////////////////////////////////////////////////// // Exported functions //////////////////////////////////////////////////////////////////////////////// function add(req, res) { - const realm = req.realm; - const occupant = occupantModel.schema.filter(req.body); - - if (!occupant.isCompany) { - occupant.company = null; - occupant.legalForm = null; - occupant.siret = null; - occupant.capital = null; - occupant.name = occupant.name || occupant.manager; - } else { - occupant.name = occupant.company; - } - - occupant.reference = occupant.reference || nanoid(); + const realm = req.realm; + const occupant = occupantModel.schema.filter(req.body); + + if (!occupant.isCompany) { + occupant.company = null; + occupant.legalForm = null; + occupant.siret = null; + occupant.capital = null; + occupant.name = occupant.name || occupant.manager; + } else { + occupant.name = occupant.company; + } + + occupant.reference = occupant.reference || nanoid(); + + if (!occupant.name) { + return res.status(422).json({ + errors: ['Missing tenant name'], + }); + } - if (!occupant.name) { - return res.status(422).json({ - errors: ['Missing tenant name'] - }); + _buildPropertyMap(realm, (errors, propertyMap) => { + if (errors && errors.length > 0) { + return res.status(404).json({ + errors: errors, + }); } - _buildPropertyMap(realm, (errors, propertyMap) => { - if (errors && errors.length > 0) { - return res.status(404).json({ - errors: errors - }); - } - - // Resolve proprerties - if (occupant.properties) { - occupant.properties.forEach((item) => { - item.property = propertyMap[item.propertyId]; - item.entryDate = item.entryDate || occupant.beginDate; - item.exitDate = item.exitDate || occupant.endDate; - item.rent = item.rent || item.property.price; - item.expenses = item.expenses || (item.property.expense && [{ title: 'general expense', amount: item.property.expense }]) || []; - }); - } - - // Build rents from contract - occupant.rents = []; - if (occupant.beginDate && occupant.endDate && occupant.properties) { - const contract = Contract.create({ - begin: occupant.beginDate, - end: occupant.endDate, - frequency: occupant.frequency || 'months', - properties: occupant.properties - }); + // Resolve proprerties + if (occupant.properties) { + occupant.properties.forEach((item) => { + item.property = propertyMap[item.propertyId]; + item.entryDate = item.entryDate || occupant.beginDate; + item.exitDate = item.exitDate || occupant.endDate; + item.rent = item.rent || item.property.price; + item.expenses = + item.expenses || + (item.property.expense && [ + { title: 'general expense', amount: item.property.expense }, + ]) || + []; + }); + } - occupant.rents = contract.rents; - } + // Build rents from contract + occupant.rents = []; + if (occupant.beginDate && occupant.endDate && occupant.properties) { + const contract = Contract.create({ + begin: occupant.beginDate, + end: occupant.endDate, + frequency: occupant.frequency || 'months', + properties: occupant.properties, + }); + + occupant.rents = contract.rents; + } - occupantModel.add(realm, occupant, (errors, occupant) => { - if (errors) { - return res.status(500).json({ - errors: errors - }); - } - res.json(FD.toOccupantData(occupant)); + occupantModel.add(realm, occupant, (errors, occupant) => { + if (errors) { + return res.status(500).json({ + errors: errors, }); + } + res.json(FD.toOccupantData(occupant)); }); + }); } function update(req, res) { - const realm = req.realm; - const occupantId = req.params.id; - const occupant = occupantModel.schema.filter(req.body); - - if (!occupant.isCompany) { - occupant.company = null; - occupant.legalForm = null; - occupant.siret = null; - occupant.capital = null; - occupant.name = occupant.name || occupant.manager; - } else { - occupant.name = occupant.company; - } + const realm = req.realm; + const occupantId = req.params.id; + const occupant = occupantModel.schema.filter(req.body); + + if (!occupant.isCompany) { + occupant.company = null; + occupant.legalForm = null; + occupant.siret = null; + occupant.capital = null; + occupant.name = occupant.name || occupant.manager; + } else { + occupant.name = occupant.company; + } + + occupant.reference = occupant.reference || nanoid(); + + if (!occupant.name) { + return res.status(422).json({ + errors: ['Missing tenant name'], + }); + } - occupant.reference = occupant.reference || nanoid(); + occupantModel.findOne(realm, occupantId, (errors, dbOccupant) => { + if (errors && errors.length > 0) { + return res.status(404).json({ + errors: errors, + }); + } - if (!occupant.name) { - return res.status(422).json({ - errors: ['Missing tenant name'] - }); + if (dbOccupant.documents) { + occupant.documents = dbOccupant.documents; } - occupantModel.findOne(realm, occupantId, (errors, dbOccupant) => { - if (errors && errors.length > 0) { - return res.status(404).json({ - errors: errors + _buildPropertyMap(realm, (errors, propertyMap) => { + if (errors && errors.length > 0) { + return res.status(404).json({ + errors: errors, + }); + } + + // Resolve proprerties + if (occupant.properties) { + occupant.properties = occupant.properties.map((item) => { + let itemToKeep; + if (dbOccupant.properties) { + dbOccupant.properties.forEach((dbItem) => { + if (dbItem.propertyId === item.propertyId) { + itemToKeep = dbItem; + } }); + } + if (!itemToKeep) { + itemToKeep = { + propertyId: item.propertyId, + property: propertyMap[item.propertyId], + }; + } + itemToKeep.entryDate = item.entryDate || occupant.beginDate; + itemToKeep.exitDate = item.exitDate || occupant.endDate; + itemToKeep.rent = item.rent || itemToKeep.property.price; + itemToKeep.expenses = + item.expenses || + (itemToKeep.property.expense && [ + { title: 'general expense', amount: itemToKeep.property.expense }, + ]) || + []; + return itemToKeep; + }); + } + + // Build rents from contract + occupant.rents = []; + if (occupant.beginDate && occupant.endDate && occupant.properties) { + try { + const contract = { + begin: dbOccupant.beginDate, + end: dbOccupant.endDate, + frequency: occupant.frequency || 'months', + terms: Math.ceil( + moment(dbOccupant.endDate, 'DD/MM/YYYY').diff( + moment(dbOccupant.beginDate, 'DD/MM/YYYY'), + 'months', + true + ) + ), + properties: dbOccupant.properties, + vatRate: dbOccupant.vatRatio, + discount: dbOccupant.discount, + rents: dbOccupant.rents, + }; + + const modification = { + begin: occupant.beginDate, + end: occupant.endDate, + termination: occupant.terminationDate, + properties: occupant.properties, + }; + if (occupant.vatRatio !== undefined) { + modification.vatRate = occupant.vatRatio; + } + if (occupant.discount !== undefined) { + modification.discount = occupant.discount; + } + + const newContract = Contract.update(contract, modification); + occupant.rents = newContract.rents; + } catch (e) { + logger.error(e); + return res.sendStatus(500); } + } - if (dbOccupant.documents) { - occupant.documents = dbOccupant.documents; + occupantModel.update(realm, occupant, (errors) => { + if (errors) { + return res.status(500).json({ + errors: errors, + }); } - - _buildPropertyMap(realm, (errors, propertyMap) => { - if (errors && errors.length > 0) { - return res.status(404).json({ - errors: errors - }); - } - - // Resolve proprerties - if (occupant.properties) { - occupant.properties = occupant.properties.map((item) => { - let itemToKeep; - if (dbOccupant.properties) { - dbOccupant.properties.forEach((dbItem) => { - if (dbItem.propertyId === item.propertyId) { - itemToKeep = dbItem; - } - }); - } - if (!itemToKeep) { - itemToKeep = { - propertyId: item.propertyId, - property: propertyMap[item.propertyId], - }; - } - itemToKeep.entryDate = item.entryDate || occupant.beginDate; - itemToKeep.exitDate = item.exitDate || occupant.endDate; - itemToKeep.rent = item.rent || itemToKeep.property.price; - itemToKeep.expenses = item.expenses || (itemToKeep.property.expense && [{ title: 'general expense', amount: itemToKeep.property.expense }]) || []; - return itemToKeep; - }); - } - - // Build rents from contract - occupant.rents = []; - if (occupant.beginDate && occupant.endDate && occupant.properties) { - try { - const contract = { - begin: dbOccupant.beginDate, - end: dbOccupant.endDate, - frequency: occupant.frequency || 'months', - terms: Math.ceil( - moment(dbOccupant.endDate, 'DD/MM/YYYY') - .diff(moment(dbOccupant.beginDate, 'DD/MM/YYYY'), 'months', true)), - properties: dbOccupant.properties, - vatRate: dbOccupant.vatRatio, - discount: dbOccupant.discount, - rents: dbOccupant.rents - }; - - const modification = { - begin: occupant.beginDate, - end: occupant.endDate, - termination: occupant.terminationDate, - properties: occupant.properties, - }; - if (occupant.vatRatio !== undefined) { - modification.vatRate = occupant.vatRatio; - } - if (occupant.discount !== undefined) { - modification.discount = occupant.discount; - } - - const newContract = Contract.update(contract, modification); - occupant.rents = newContract.rents; - } catch (e) { - logger.error(e); - return res.sendStatus(500); - } - } - - occupantModel.update(realm, occupant, (errors) => { - if (errors) { - return res.status(500).json({ - errors: errors - }); - } - res.json(FD.toOccupantData(occupant)); - }); - }); + res.json(FD.toOccupantData(occupant)); + }); }); + }); } function remove(req, res) { - const realm = req.realm; - const occupantIds = req.params.ids.split(','); - - function releaseRent(callback) { - const occupantfilters = occupantIds.map(_id => { return { _id }; }); + const realm = req.realm; + const occupantIds = req.params.ids.split(','); - occupantModel.findFilter(realm, { - $query: { - $or: occupantfilters - } - }, (errors, occupants) => { - if (errors) { - return res.status(500).json({ - errors - }); - } + function releaseRent(callback) { + const occupantfilters = occupantIds.map((_id) => { + return { _id }; + }); - if (!occupants || !occupants.length) { - return res.sendStatus(404); - } + occupantModel.findFilter( + realm, + { + $query: { + $or: occupantfilters, + }, + }, + (errors, occupants) => { + if (errors) { + return res.status(500).json({ + errors, + }); + } - if (occupants) { - const occupantsWithPaidRents = occupants.filter(occupant => { - return occupant.rents - .some(rent => (rent.payments && rent.payments.some(payment => payment.amount > 0)) || - rent.discounts.some(discount => discount.origin === 'settlement')); - }); - if (occupantsWithPaidRents.length > 0) { - // TODO: to localize - return res.status(422).json({ - errors: ['Impossible de supprimer le locataire : ' + occupantsWithPaidRents[0].name + '. Des loyers ont été encaissés.'] - }); - } - occupantModel.remove( - realm, - occupants.map(occupant => occupant._id.toString()), - errors => { - if (errors) { - return res.status(500).json({ - errors - }); - } - callback(); - }); - } - }); - } + if (!occupants || !occupants.length) { + return res.sendStatus(404); + } - releaseRent(() => { - occupantModel.remove(realm, occupantIds, (errors) => { - if (errors) { + if (occupants) { + const occupantsWithPaidRents = occupants.filter((occupant) => { + return occupant.rents.some( + (rent) => + (rent.payments && + rent.payments.some((payment) => payment.amount > 0)) || + rent.discounts.some( + (discount) => discount.origin === 'settlement' + ) + ); + }); + if (occupantsWithPaidRents.length > 0) { + // TODO: to localize + return res.status(422).json({ + errors: [ + 'Impossible de supprimer le locataire : ' + + occupantsWithPaidRents[0].name + + '. Des loyers ont été encaissés.', + ], + }); + } + occupantModel.remove( + realm, + occupants.map((occupant) => occupant._id.toString()), + (errors) => { + if (errors) { return res.status(500).json({ - errors + errors, }); + } + callback(); } - res.sendStatus(200); + ); + } + } + ); + } + + releaseRent(() => { + occupantModel.remove(realm, occupantIds, (errors) => { + if (errors) { + return res.status(500).json({ + errors, }); + } + res.sendStatus(200); }); + }); } function all(req, res) { - const realm = req.realm; - occupantModel.findAll(realm, (errors, occupants) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } + const realm = req.realm; + occupantModel.findAll(realm, (errors, occupants) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, + }); + } - res.json(occupants.map(occupant => FD.toOccupantData(occupant))); - }); + res.json(occupants.map((occupant) => FD.toOccupantData(occupant))); + }); } function one(req, res) { - const realm = req.realm; - const occupantId = req.params.id; - occupantModel.findOne(realm, occupantId, (errors, dbOccupant) => { - if (errors && errors.length > 0) { - return res.status(404).json({ - errors: errors - }); - } + const realm = req.realm; + const occupantId = req.params.id; + occupantModel.findOne(realm, occupantId, (errors, dbOccupant) => { + if (errors && errors.length > 0) { + return res.status(404).json({ + errors: errors, + }); + } - res.json(FD.toOccupantData(dbOccupant)); - }); + res.json(FD.toOccupantData(dbOccupant)); + }); } function overview(req, res) { - const realm = req.realm; - let result = { - countAll: 0, - countActive: 0, - countInactive: 0 - }; - const currentDate = moment(); - - occupantModel.findAll(realm, (errors, occupants) => { - if (errors && errors.length > 0) { - return res.status(404).json({ - errors: errors - }); - } + const realm = req.realm; + let result = { + countAll: 0, + countActive: 0, + countInactive: 0, + }; + const currentDate = moment(); + + occupantModel.findAll(realm, (errors, occupants) => { + if (errors && errors.length > 0) { + return res.status(404).json({ + errors: errors, + }); + } - if (occupants) { - result.countAll = occupants.length; - result = occupants.reduce((acc, occupant) => { - const endMoment = moment(occupant.terminationDate || occupant.endDate, 'DD/MM/YYYY'); - if (endMoment.isBefore(currentDate, 'day')) { - acc.countInactive++; - } else { - acc.countActive++; - } - return acc; - }, result); + if (occupants) { + result.countAll = occupants.length; + result = occupants.reduce((acc, occupant) => { + const endMoment = moment( + occupant.terminationDate || occupant.endDate, + 'DD/MM/YYYY' + ); + if (endMoment.isBefore(currentDate, 'day')) { + acc.countInactive++; + } else { + acc.countActive++; } - res.json(result); - }); + return acc; + }, result); + } + res.json(result); + }); } module.exports = { - add, - update, - remove, - one, - all, - overview + add, + update, + remove, + one, + all, + overview, }; diff --git a/backend/managers/ownermanager.js b/backend/managers/ownermanager.js index dcfc121..06cfeca 100644 --- a/backend/managers/ownermanager.js +++ b/backend/managers/ownermanager.js @@ -6,82 +6,86 @@ const realmModel = require('../models/realm'); // Exported functions //////////////////////////////////////////////////////////////////////////////// function all(req, res) { - const realm = req.realm; - realmModel.findOne(realm._id, (errors, dbRealm) => { - if (errors && errors.length > 0) { - res.json({ - errors: errors - }); - return; - } - res.json({ - _id: dbRealm._id, - isCompany: dbRealm.isCompany, - company: dbRealm.companyInfo.name, - legalForm: dbRealm.companyInfo.legalStructure, - capital: dbRealm.companyInfo.capital, - siret: dbRealm.companyInfo.ein, - dos: dbRealm.companyInfo.dos, - vatNumber: dbRealm.companyInfo.vatNumber, - manager: dbRealm.companyInfo.legalRepresentative, - street1: dbRealm.addresses[0].street1, - street2: dbRealm.addresses[0].street2, - zipCode: dbRealm.addresses[0].zipCode, - city: dbRealm.addresses[0].city, - state: dbRealm.addresses[0].state, - country: dbRealm.addresses[0].country, - contact: dbRealm.contacts[0].name, - email: dbRealm.contacts[0].email, - phone1: dbRealm.contacts[0].phone1, - phone2: dbRealm.contacts[0].phone2, - bank: dbRealm.bankInfo.name, - rib: dbRealm.bankInfo.iban, - }); + const realm = req.realm; + realmModel.findOne(realm._id, (errors, dbRealm) => { + if (errors && errors.length > 0) { + res.json({ + errors: errors, + }); + return; + } + res.json({ + _id: dbRealm._id, + isCompany: dbRealm.isCompany, + company: dbRealm.companyInfo.name, + legalForm: dbRealm.companyInfo.legalStructure, + capital: dbRealm.companyInfo.capital, + siret: dbRealm.companyInfo.ein, + dos: dbRealm.companyInfo.dos, + vatNumber: dbRealm.companyInfo.vatNumber, + manager: dbRealm.companyInfo.legalRepresentative, + street1: dbRealm.addresses[0].street1, + street2: dbRealm.addresses[0].street2, + zipCode: dbRealm.addresses[0].zipCode, + city: dbRealm.addresses[0].city, + state: dbRealm.addresses[0].state, + country: dbRealm.addresses[0].country, + contact: dbRealm.contacts[0].name, + email: dbRealm.contacts[0].email, + phone1: dbRealm.contacts[0].phone1, + phone2: dbRealm.contacts[0].phone2, + bank: dbRealm.bankInfo.name, + rib: dbRealm.bankInfo.iban, }); + }); } function update(req, res) { - const realm = req.realm; - const owner = req.body; + const realm = req.realm; + const owner = req.body; - realm.isCompany = owner.isCompany; - realm.companyInfo = { - name: owner.company, - legalStructure: owner.legalForm, - capital: owner.capital, - ein: owner.siret, - dos: owner.dos, - vatNumber: owner.vatNumber, - legalRepresentative: owner.manager - }; + realm.isCompany = owner.isCompany; + realm.companyInfo = { + name: owner.company, + legalStructure: owner.legalForm, + capital: owner.capital, + ein: owner.siret, + dos: owner.dos, + vatNumber: owner.vatNumber, + legalRepresentative: owner.manager, + }; - realm.addresses = [{ - street1: owner.street1, - street2: owner.street2, - zipCode: owner.zipCode, - city: owner.city, - state: owner.state, - country: owner.country - }]; + realm.addresses = [ + { + street1: owner.street1, + street2: owner.street2, + zipCode: owner.zipCode, + city: owner.city, + state: owner.state, + country: owner.country, + }, + ]; - realm.contacts = [{ - name: owner.contact, - email: owner.email, - phone1: owner.phone1, - phone2: owner.phone2 - }]; + realm.contacts = [ + { + name: owner.contact, + email: owner.email, + phone1: owner.phone1, + phone2: owner.phone2, + }, + ]; - realm.bankInfo = { - name: owner.bank, - iban: owner.rib - }; + realm.bankInfo = { + name: owner.bank, + iban: owner.rib, + }; - realmModel.update(realmModel.schema.filter(realm), (errors) => { - res.json({errors: errors}); - }); + realmModel.update(realmModel.schema.filter(realm), (errors) => { + res.json({ errors: errors }); + }); } module.exports = { - all, - update + all, + update, }; diff --git a/backend/managers/propertymanager.js b/backend/managers/propertymanager.js index 2a7dcd1..5e6f276 100644 --- a/backend/managers/propertymanager.js +++ b/backend/managers/propertymanager.js @@ -6,186 +6,208 @@ const propertyModel = require('../models/property'); const occupantModel = require('../models/occupant'); function _toPropertiesData(realm, inputProperties, callback) { - occupantModel.findFilter( - realm, - { - properties: { - $elemMatch: { - propertyId: { $in: inputProperties.map(property => property._id.toString()) } - } - } + occupantModel.findFilter( + realm, + { + properties: { + $elemMatch: { + propertyId: { + $in: inputProperties.map((property) => property._id.toString()), + }, }, - (errors, occupants) => { - if (errors) { - callback(errors); - return; - } - callback(null, inputProperties.map(property => { - return FD.toProperty( - property, - occupants - .reduce((acc, occupant) => { - const occupant_property = occupant.properties.find(currentProperty => currentProperty.propertyId === property._id.toString()); - if (occupant_property) { - if (!acc.occupant) { - acc.occupant = occupant; - } else { - const acc_property = acc.occupant.properties.find(currentProperty => currentProperty.propertyId === property._id.toString()); - const beginDate = moment(occupant_property.entryDate, 'DD/MM/YYYY').startOf('day'); - const lastBeginDate = moment(acc_property.entryDate, 'DD/MM/YYYY').startOf('day'); - if (beginDate.isAfter(lastBeginDate)) { - acc.occupant = occupant; - } - } - } - return acc; - }, { occupant: null }) - .occupant, - occupants - .filter(({ properties }) => properties.map(({ propertyId }) => propertyId).includes(property._id)) - .sort((occ1, occ2) => { - const m1 = moment(occ1.beginDate, 'DD/MM/YYYY'); - const m2 = moment(occ2.beginDate, 'DD/MM/YYYY'); - return m1.isBefore(m2) ? 1 : -1; - }) + }, + }, + (errors, occupants) => { + if (errors) { + callback(errors); + return; + } + callback( + null, + inputProperties.map((property) => { + return FD.toProperty( + property, + occupants.reduce( + (acc, occupant) => { + const occupant_property = occupant.properties.find( + (currentProperty) => + currentProperty.propertyId === property._id.toString() ); - })); - } - ); + if (occupant_property) { + if (!acc.occupant) { + acc.occupant = occupant; + } else { + const acc_property = acc.occupant.properties.find( + (currentProperty) => + currentProperty.propertyId === property._id.toString() + ); + const beginDate = moment( + occupant_property.entryDate, + 'DD/MM/YYYY' + ).startOf('day'); + const lastBeginDate = moment( + acc_property.entryDate, + 'DD/MM/YYYY' + ).startOf('day'); + if (beginDate.isAfter(lastBeginDate)) { + acc.occupant = occupant; + } + } + } + return acc; + }, + { occupant: null } + ).occupant, + occupants + .filter(({ properties }) => + properties + .map(({ propertyId }) => propertyId) + .includes(property._id) + ) + .sort((occ1, occ2) => { + const m1 = moment(occ1.beginDate, 'DD/MM/YYYY'); + const m2 = moment(occ2.beginDate, 'DD/MM/YYYY'); + return m1.isBefore(m2) ? 1 : -1; + }) + ); + }) + ); + } + ); } //////////////////////////////////////////////////////////////////////////////// // Exported functions //////////////////////////////////////////////////////////////////////////////// function add(req, res) { - const realm = req.realm; - const property = propertyModel.schema.filter(req.body); - - propertyModel.add(realm, property, (errors, dbProperty) => { - if (errors) { - return res.status(500).json({ errors: errors }); - } - _toPropertiesData(realm, [dbProperty], (errors, properties) => { - if (errors && errors.length > 0) { - return res.status(500).json({ errors: errors }); - } - res.json(properties[0]); - }); + const realm = req.realm; + const property = propertyModel.schema.filter(req.body); + + propertyModel.add(realm, property, (errors, dbProperty) => { + if (errors) { + return res.status(500).json({ errors: errors }); + } + _toPropertiesData(realm, [dbProperty], (errors, properties) => { + if (errors && errors.length > 0) { + return res.status(500).json({ errors: errors }); + } + res.json(properties[0]); }); + }); } function update(req, res) { - const realm = req.realm; - const property = propertyModel.schema.filter(req.body); - - propertyModel.update(realm, property, (errors) => { - if (errors) { - return res.status(500).json({ errors: errors }); - } - _toPropertiesData(realm, [property], (errors, properties) => { - if (errors && errors.length > 0) { - return res.status(500).json({ errors: errors }); - } - res.json(properties[0]); - }); + const realm = req.realm; + const property = propertyModel.schema.filter(req.body); + + propertyModel.update(realm, property, (errors) => { + if (errors) { + return res.status(500).json({ errors: errors }); + } + _toPropertiesData(realm, [property], (errors, properties) => { + if (errors && errors.length > 0) { + return res.status(500).json({ errors: errors }); + } + res.json(properties[0]); }); + }); } function remove(req, res) { - const realm = req.realm; - const ids = req.params.ids.split(','); - - propertyModel.remove(realm, ids, (errors) => { - if (errors) { - return res.status(500).json({ errors: errors }); - } - res.sendStatus(200); // better to return 204 - }); + const realm = req.realm; + const ids = req.params.ids.split(','); + + propertyModel.remove(realm, ids, (errors) => { + if (errors) { + return res.status(500).json({ errors: errors }); + } + res.sendStatus(200); // better to return 204 + }); } function all(req, res) { - const realm = req.realm; - - propertyModel.findAll(realm, (errors, properties) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - - _toPropertiesData(realm, properties, (errors, properties) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - res.json(properties); + const realm = req.realm; + + propertyModel.findAll(realm, (errors, properties) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, + }); + } + + _toPropertiesData(realm, properties, (errors, properties) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, }); + } + res.json(properties); }); + }); } function one(req, res) { - const realm = req.realm; - const tenantId = req.params.id; - - propertyModel.findOne(realm, tenantId, (errors, dbProperty) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - - _toPropertiesData(realm, dbProperty, (errors, property) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - res.json(property); + const realm = req.realm; + const tenantId = req.params.id; + + propertyModel.findOne(realm, tenantId, (errors, dbProperty) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, + }); + } + + _toPropertiesData(realm, dbProperty, (errors, property) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, }); + } + res.json(property); }); + }); } function overview(req, res) { - const realm = req.realm; - let result = { - countAll: 0, - countFree: 0, - countBusy: 0 - }; - - propertyModel.findAll(realm, (errors, properties) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - - _toPropertiesData(realm, properties, (errors, properties) => { - if (errors && errors.length > 0) { - return res.status(500).json({ - errors: errors - }); - } - result.countAll = properties.length; - properties.reduce((acc, property) => { - if (property.available) { - acc.countFree++; - } else { - acc.countBusy++; - } - return acc; - }, result); - res.json(result); + const realm = req.realm; + let result = { + countAll: 0, + countFree: 0, + countBusy: 0, + }; + + propertyModel.findAll(realm, (errors, properties) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, + }); + } + + _toPropertiesData(realm, properties, (errors, properties) => { + if (errors && errors.length > 0) { + return res.status(500).json({ + errors: errors, }); + } + result.countAll = properties.length; + properties.reduce((acc, property) => { + if (property.available) { + acc.countFree++; + } else { + acc.countBusy++; + } + return acc; + }, result); + res.json(result); }); + }); } module.exports = { - add, - update, - remove, - all, - one, - overview + add, + update, + remove, + all, + one, + overview, }; diff --git a/backend/managers/realmmanager.js b/backend/managers/realmmanager.js index ec4ae54..09794e9 100644 --- a/backend/managers/realmmanager.js +++ b/backend/managers/realmmanager.js @@ -4,139 +4,175 @@ const crypto = require('../utils/crypto'); const SECRET_PLACEHOLDER = '**********'; -const _hasRequiredFields = realm => { - return ( - realm.name && - realm.members && - realm.members.find(({ role }) => role === 'administrator') && - realm.locale && - realm.currency - ); +const _hasRequiredFields = (realm) => { + return ( + realm.name && + realm.members && + realm.members.find(({ role }) => role === 'administrator') && + realm.locale && + realm.currency + ); }; const _isNameAlreadyTaken = (realm, realms = []) => { - return realms.map(({ name }) => - name.trim().toLowerCase()).includes(realm.name.trim().toLowerCase()); + return realms + .map(({ name }) => name.trim().toLowerCase()) + .includes(realm.name.trim().toLowerCase()); }; -const _escapeSecrets = realm => { - if (realm.thirdParties && realm.thirdParties.mailgun && realm.thirdParties.mailgun.apiKey) { - realm.thirdParties.mailgun.apiKey = SECRET_PLACEHOLDER; - } - return realm; +const _escapeSecrets = (realm) => { + if ( + realm.thirdParties && + realm.thirdParties.mailgun && + realm.thirdParties.mailgun.apiKey + ) { + realm.thirdParties.mailgun.apiKey = SECRET_PLACEHOLDER; + } + return realm; }; module.exports = { - add(req, res) { - const newRealm = realmModel.schema.filter(req.body); - - if (!_hasRequiredFields(newRealm)) { - return res.status(422).json({ error: 'missing fields' }); - } - - if (_isNameAlreadyTaken(newRealm, req.realms)) { - return res.status(409).json({ error: 'organization name already exists' }); - } - - if (newRealm.thirdParties && - newRealm.thirdParties.mailgun && - newRealm.thirdParties.mailgun.apiKey ) { - if (newRealm.thirdParties.mailgun.apiKey !== SECRET_PLACEHOLDER) { - newRealm.thirdParties.mailgun.apiKey = crypto.encrypt(newRealm.thirdParties.mailgun.apiKey); - } else { - delete newRealm.thirdParties.mailgun.apiKey; - } - } - - realmModel.add(newRealm, (errors, realm) => { - if (errors) { - return res.status(500).json({ - errors: errors - }); - } - res.status(201).json(_escapeSecrets(realm)); + add(req, res) { + const newRealm = realmModel.schema.filter(req.body); + + if (!_hasRequiredFields(newRealm)) { + return res.status(422).json({ error: 'missing fields' }); + } + + if (_isNameAlreadyTaken(newRealm, req.realms)) { + return res + .status(409) + .json({ error: 'organization name already exists' }); + } + + if ( + newRealm.thirdParties && + newRealm.thirdParties.mailgun && + newRealm.thirdParties.mailgun.apiKey + ) { + if (newRealm.thirdParties.mailgun.apiKey !== SECRET_PLACEHOLDER) { + newRealm.thirdParties.mailgun.apiKey = crypto.encrypt( + newRealm.thirdParties.mailgun.apiKey + ); + } else { + delete newRealm.thirdParties.mailgun.apiKey; + } + } + + realmModel.add(newRealm, (errors, realm) => { + if (errors) { + return res.status(500).json({ + errors: errors, }); - }, - async update(req, res) { - const mailgunApiKeyUpdated = req.body.thirdParties && req.body.thirdParties.mailgun && req.body.thirdParties.mailgun.apiKeyUpdated; - const updatedRealm = realmModel.schema.filter(req.body); - - if (req.realm._id !== updatedRealm._id) { - return res.status(403).json({ error: 'only current selected organizaton can be updated' }); - } - - const currentMember = req.realm.members.find(({ email }) => email = req.user.email); - if (!currentMember) { - return res.status(403).json({ error: 'current user is not a member of the organization' }); - } - - if (currentMember.role !== 'administrator') { - return res.status(403).json({ error: 'only administrator member can update the organization' }); - } - - if (!_hasRequiredFields(updatedRealm)) { - return res.status(422).json({ error: 'missing fields' }); - } - - if (updatedRealm.name !== req.realm.name && _isNameAlreadyTaken(updatedRealm, req.realms)) { - return res.status(409).json({ error: 'organization name already exists' }); - } - - if (mailgunApiKeyUpdated && - updatedRealm.thirdParties && - updatedRealm.thirdParties.mailgun && - updatedRealm.thirdParties.mailgun.apiKey ) { - if (updatedRealm.thirdParties.mailgun.apiKey !== SECRET_PLACEHOLDER) { - updatedRealm.thirdParties.mailgun.apiKey = crypto.encrypt(updatedRealm.thirdParties.mailgun.apiKey); - } else { - delete updatedRealm.thirdParties.mailgun.apiKey; - } - } - - const usernameMap = {}; - try { - await new Promise((resolve, reject) => { - accountModel.findAll((errors, accounts = []) => { - resolve(accounts.reduce((acc, { email, firstname, lastname }) => { - acc[email] = `${firstname} ${lastname}`; - return acc; - }, usernameMap)); - }); - }); - } catch(error) { - console.error(error); - } - - updatedRealm.members.forEach(member => { - const name = usernameMap[member.email]; - member.name = name || ''; - member.registered = !!name; + } + res.status(201).json(_escapeSecrets(realm)); + }); + }, + async update(req, res) { + const mailgunApiKeyUpdated = + req.body.thirdParties && + req.body.thirdParties.mailgun && + req.body.thirdParties.mailgun.apiKeyUpdated; + const updatedRealm = realmModel.schema.filter(req.body); + + if (req.realm._id !== updatedRealm._id) { + return res + .status(403) + .json({ error: 'only current selected organizaton can be updated' }); + } + + const currentMember = req.realm.members.find( + ({ email }) => email === req.user.email + ); + if (!currentMember) { + return res + .status(403) + .json({ error: 'current user is not a member of the organization' }); + } + + if (currentMember.role !== 'administrator') { + return res.status(403).json({ + error: 'only administrator member can update the organization', + }); + } + + if (!_hasRequiredFields(updatedRealm)) { + return res.status(422).json({ error: 'missing fields' }); + } + + if ( + updatedRealm.name !== req.realm.name && + _isNameAlreadyTaken(updatedRealm, req.realms) + ) { + return res + .status(409) + .json({ error: 'organization name already exists' }); + } + + if ( + mailgunApiKeyUpdated && + updatedRealm.thirdParties && + updatedRealm.thirdParties.mailgun && + updatedRealm.thirdParties.mailgun.apiKey + ) { + if (updatedRealm.thirdParties.mailgun.apiKey !== SECRET_PLACEHOLDER) { + updatedRealm.thirdParties.mailgun.apiKey = crypto.encrypt( + updatedRealm.thirdParties.mailgun.apiKey + ); + } else { + delete updatedRealm.thirdParties.mailgun.apiKey; + } + } + + const usernameMap = {}; + try { + await new Promise((resolve, reject) => { + accountModel.findAll((errors, accounts = []) => { + if (errors) { + return reject(errors); + } + resolve( + accounts.reduce((acc, { email, firstname, lastname }) => { + acc[email] = `${firstname} ${lastname}`; + return acc; + }, usernameMap) + ); }); + }); + } catch (error) { + console.error(error); + } + + updatedRealm.members.forEach((member) => { + const name = usernameMap[member.email]; + member.name = name || ''; + member.registered = !!name; + }); - realmModel.update(updatedRealm, (errors, realm) => { - if (errors) { - return res.status(500).json({ - errors: errors - }); - } - res.status(200).json(_escapeSecrets(realm)); + realmModel.update(updatedRealm, (errors, realm) => { + if (errors) { + return res.status(500).json({ + errors: errors, }); - }, - remove(req, res) { }, - one(req, res) { - const realmId = req.params.id; - if (!realmId) { - return res.sendStatus(404); - } - - const realm = req.realms.find(({ _id }) => _id.toString() === realmId); - if (!realm) { - return res.sendStatus(404); - } - - res.json(_escapeSecrets(realm)); - }, - all(req, res) { - res.json(req.realms.map(realm => _escapeSecrets(realm))); + } + res.status(200).json(_escapeSecrets(realm)); + }); + }, + remove(/*req, res*/) {}, + one(req, res) { + const realmId = req.params.id; + if (!realmId) { + return res.sendStatus(404); } + + const realm = req.realms.find(({ _id }) => _id.toString() === realmId); + if (!realm) { + return res.sendStatus(404); + } + + res.json(_escapeSecrets(realm)); + }, + all(req, res) { + res.json(req.realms.map((realm) => _escapeSecrets(realm))); + }, }; diff --git a/backend/managers/rentmanager.js b/backend/managers/rentmanager.js index aa3fa36..394096a 100644 --- a/backend/managers/rentmanager.js +++ b/backend/managers/rentmanager.js @@ -8,353 +8,375 @@ const occupantModel = require('../models/occupant'); const config = require('../../config'); const _findOccupants = (realm, occupantId, startTerm, endTerm) => { - return new Promise((resolve, reject) => { - const filter = { - '$query': { - '$and': [] - } - }; - if (occupantId) { - filter['$query']['$and'].push({ '_id': occupantId }); - } - if (startTerm && endTerm) { - filter['$query']['$and'].push({ 'rents.term': { '$gte': startTerm } }); - filter['$query']['$and'].push({ 'rents.term': { '$lte': endTerm } }); - } else if (startTerm) { - filter['$query']['$and'].push({ 'rents.term': startTerm }); - } - occupantModel.findFilter(realm, filter, (errors, occupants) => { - if (errors && errors.length > 0) { - return reject(errors); + return new Promise((resolve, reject) => { + const filter = { + $query: { + $and: [], + }, + }; + if (occupantId) { + filter['$query']['$and'].push({ _id: occupantId }); + } + if (startTerm && endTerm) { + filter['$query']['$and'].push({ 'rents.term': { $gte: startTerm } }); + filter['$query']['$and'].push({ 'rents.term': { $lte: endTerm } }); + } else if (startTerm) { + filter['$query']['$and'].push({ 'rents.term': startTerm }); + } + occupantModel.findFilter(realm, filter, (errors, occupants) => { + if (errors && errors.length > 0) { + return reject(errors); + } + resolve( + occupants + .map((occupant) => { + if (startTerm && endTerm) { + occupant.rents = occupant.rents.filter( + (rent) => rent.term >= startTerm && rent.term <= endTerm + ); + } else if (startTerm) { + occupant.rents = occupant.rents.filter( + (rent) => rent.term === startTerm + ); } - resolve(occupants - .map(occupant => { - if (startTerm && endTerm) { - occupant.rents = occupant.rents.filter(rent => rent.term >= startTerm && rent.term <= endTerm); - } else if (startTerm) { - occupant.rents = occupant.rents.filter(rent => rent.term === startTerm); - } - return occupant; - }) - .sort((o1, o2) => { - const name1 = o1.isCompany ? o1.company : o1.name; - const name2 = o2.isCompany ? o2.company : o2.name; - - return name1.localeCompare(name2); - }) - ); - }); + return occupant; + }) + .sort((o1, o2) => { + const name1 = o1.isCompany ? o1.company : o1.name; + const name2 = o2.isCompany ? o2.company : o2.name; + + return name1.localeCompare(name2); + }) + ); }); + }); }; const _getEmailStatus = async (startTerm, endTerm) => { - try { - let emailEndPoint = `${config.EMAILER_URL}/status/${startTerm}`; - if (endTerm) { - emailEndPoint = `${config.EMAILER_URL}/status/${startTerm}/${endTerm}`; - } - logger.debug(`get email status ${emailEndPoint}`); - const response = await axios.get(emailEndPoint); - logger.debug(response.data); - return response.data.reduce((acc, status) => { - const data = { - sentTo: status.sentTo, - sentDate: status.sentDate - }; - if (!acc[status.recordId]) { - acc[status.recordId] = { [status.templateName]: [] }; - } - let documents = acc[status.recordId][status.templateName]; - if (!documents) { - documents = []; - acc[status.recordId][status.templateName] = documents; - } - documents.push(data); - return acc; - }, {}); - } catch (error) { - if (config.demoMode) { - logger.info('email status fallback workflow activated in demo mode'); - return {}; - } else { - throw error.data; - } + try { + let emailEndPoint = `${config.EMAILER_URL}/status/${startTerm}`; + if (endTerm) { + emailEndPoint = `${config.EMAILER_URL}/status/${startTerm}/${endTerm}`; + } + logger.debug(`get email status ${emailEndPoint}`); + const response = await axios.get(emailEndPoint); + logger.debug(response.data); + return response.data.reduce((acc, status) => { + const data = { + sentTo: status.sentTo, + sentDate: status.sentDate, + }; + if (!acc[status.recordId]) { + acc[status.recordId] = { [status.templateName]: [] }; + } + let documents = acc[status.recordId][status.templateName]; + if (!documents) { + documents = []; + acc[status.recordId][status.templateName] = documents; + } + documents.push(data); + return acc; + }, {}); + } catch (error) { + if (config.demoMode) { + logger.info('email status fallback workflow activated in demo mode'); + return {}; + } else { + throw error.data; } + } }; const _getRentsDataByTerm = async (realm, currentDate, frequency) => { - const startTerm = Number(currentDate.startOf(frequency).format('YYYYMMDDHH')); - const endTerm = Number(currentDate.endOf(frequency).format('YYYYMMDDHH')); - - const [dbOccupants, emailStatus = {}] = await Promise.all([ - _findOccupants(realm, null, startTerm, endTerm), - _getEmailStatus(startTerm, endTerm).catch(logger.error) - ]); - - // compute rents - const rents = dbOccupants.reduce((acc, occupant) => { - acc.push(...occupant.rents - .filter(rent => rent.term >= startTerm && rent.term <= endTerm) - .map(rent => FD.toRentData( - rent, - occupant, - emailStatus && emailStatus[occupant._id] - )) - ); - return acc; - }, []); - - // compute rents overview - const overview = { - countAll: 0, - countPaid: 0, - countPartiallyPaid: 0, - countNotPaid: 0, - totalToPay: 0, - totalPaid: 0, - totalNotPaid: 0 - }; - rents.reduce((acc, rent) => { - if (rent.totalAmount <= 0 || rent.newBalance >= 0) { - acc.countPaid++; - } else if (rent.payment > 0) { - acc.countPartiallyPaid++; - } else { - acc.countNotPaid++; - } - acc.countAll++; - acc.totalToPay += rent.totalToPay; - acc.totalPaid += rent.payment; - acc.totalNotPaid -= rent.newBalance; - return acc; - }, overview); - - return { overview, rents }; + const startTerm = Number(currentDate.startOf(frequency).format('YYYYMMDDHH')); + const endTerm = Number(currentDate.endOf(frequency).format('YYYYMMDDHH')); + + const [dbOccupants, emailStatus = {}] = await Promise.all([ + _findOccupants(realm, null, startTerm, endTerm), + _getEmailStatus(startTerm, endTerm).catch(logger.error), + ]); + + // compute rents + const rents = dbOccupants.reduce((acc, occupant) => { + acc.push( + ...occupant.rents + .filter((rent) => rent.term >= startTerm && rent.term <= endTerm) + .map((rent) => + FD.toRentData( + rent, + occupant, + emailStatus && emailStatus[occupant._id] + ) + ) + ); + return acc; + }, []); + + // compute rents overview + const overview = { + countAll: 0, + countPaid: 0, + countPartiallyPaid: 0, + countNotPaid: 0, + totalToPay: 0, + totalPaid: 0, + totalNotPaid: 0, + }; + rents.reduce((acc, rent) => { + if (rent.totalAmount <= 0 || rent.newBalance >= 0) { + acc.countPaid++; + } else if (rent.payment > 0) { + acc.countPartiallyPaid++; + } else { + acc.countNotPaid++; + } + acc.countAll++; + acc.totalToPay += rent.totalToPay; + acc.totalPaid += rent.payment; + acc.totalNotPaid -= rent.newBalance; + return acc; + }, overview); + + return { overview, rents }; }; //////////////////////////////////////////////////////////////////////////////// // Exported functions //////////////////////////////////////////////////////////////////////////////// const update = async (req, res) => { - const realm = req.realm; - const paymentData = rentModel.paymentSchema.filter(req.body); - const term = `01/${paymentData.month}/${paymentData.year} 00:00`; - - try { - res.json(await _updateByTerm(realm, term, paymentData)); - } catch (errors) { - console.error(errors); - res.status(500).json({ errors }); - } + const realm = req.realm; + const paymentData = rentModel.paymentSchema.filter(req.body); + const term = `01/${paymentData.month}/${paymentData.year} 00:00`; + + try { + res.json(await _updateByTerm(realm, term, paymentData)); + } catch (errors) { + console.error(errors); + res.status(500).json({ errors }); + } }; const updateByTerm = async (req, res) => { - const realm = req.realm; - const term = moment(req.params.term, 'YYYYMMDDHH').format('DD/MM/YYYY HH:00'); - const paymentData = rentModel.paymentSchema.filter(req.body); - - try { - res.json(await _updateByTerm(realm, term, paymentData)); - } catch (errors) { - console.error(errors); - res.status(500).json({ errors }); - } + const realm = req.realm; + const term = moment(req.params.term, 'YYYYMMDDHH').format('DD/MM/YYYY HH:00'); + const paymentData = rentModel.paymentSchema.filter(req.body); + + try { + res.json(await _updateByTerm(realm, term, paymentData)); + } catch (errors) { + console.error(errors); + res.status(500).json({ errors }); + } }; const _updateByTerm = async (realm, term, paymentData) => { - if (!paymentData.promo && paymentData.promo <= 0) { - paymentData.promo = 0; - paymentData.notepromo = null; + if (!paymentData.promo && paymentData.promo <= 0) { + paymentData.promo = 0; + paymentData.notepromo = null; + } + + if (!paymentData.extracharge && paymentData.extracharge <= 0) { + paymentData.extracharge = 0; + paymentData.noteextracharge = null; + } + + const dbOccupant = await new Promise((resolve, reject) => { + occupantModel.findOne(realm, paymentData._id, (errors, dbOccupant) => { + if (errors && errors.length > 0) { + return reject({ errors }); + } + resolve(dbOccupant); + }); + }); + + const contract = { + frequency: dbOccupant.frequency || 'months', + begin: dbOccupant.beginDate, + end: dbOccupant.endDate, + discount: dbOccupant.discount || 0, + vatRate: dbOccupant.vatRatio, + properties: dbOccupant.properties, + rents: dbOccupant.rents, + }; + + const settlements = { + payments: [], + debts: [], + discounts: [], + description: '', + }; + + if (paymentData) { + if (paymentData.payments && paymentData.payments.length) { + settlements.payments = paymentData.payments + .filter(({ amount }) => amount && Number(amount) > 0) + .map((payment) => ({ + date: payment.date || '', + amount: Number(payment.amount), + type: payment.type || '', + reference: payment.reference || '', + description: payment.description || '', + })); } - if (!paymentData.extracharge && paymentData.extracharge <= 0) { - paymentData.extracharge = 0; - paymentData.noteextracharge = null; + if (paymentData.promo) { + settlements.discounts.push({ + origin: 'settlement', + description: paymentData.notepromo || '', + amount: + paymentData.promo * + (contract.vatRate ? 1 / (1 + contract.vatRate) : 1), + }); } - const dbOccupant = await new Promise((resolve, reject) => { - occupantModel.findOne(realm, paymentData._id, (errors, dbOccupant) => { - if (errors && errors.length > 0) { - return reject({ errors }); - } - resolve(dbOccupant); - }); - }); - - const contract = { - frequency: dbOccupant.frequency || 'months', - begin: dbOccupant.beginDate, - end: dbOccupant.endDate, - discount: dbOccupant.discount || 0, - vatRate: dbOccupant.vatRatio, - properties: dbOccupant.properties, - rents: dbOccupant.rents - }; - - const settlements = { - payments: [], - debts: [], - discounts: [], - description: '' - }; - - if (paymentData) { - if (paymentData.payments && paymentData.payments.length) { - settlements.payments = paymentData.payments - .filter(({ amount }) => amount && Number(amount) > 0) - .map(payment => ({ - date: payment.date || '', - amount: Number(payment.amount), - type: payment.type || '', - reference: payment.reference || '', - description: payment.description || '' - })); - } - - if (paymentData.promo) { - settlements.discounts.push({ - origin: 'settlement', - description: paymentData.notepromo || '', - amount: paymentData.promo * (contract.vatRate ? (1 / (1 + contract.vatRate)) : 1) - }); - } - - if (paymentData.extracharge) { - settlements.debts.push({ - description: paymentData.noteextracharge || '', - amount: paymentData.extracharge * (contract.vatRate ? (1 / (1 + contract.vatRate)) : 1) - }); - } - - if (paymentData.description) { - settlements.description = paymentData.description; - } + if (paymentData.extracharge) { + settlements.debts.push({ + description: paymentData.noteextracharge || '', + amount: + paymentData.extracharge * + (contract.vatRate ? 1 / (1 + contract.vatRate) : 1), + }); } - dbOccupant.rents = Contract.payTerm(contract, term, settlements).rents; - - return await new Promise((resolve, reject) => { - const termAsNumber = Number(moment(term, 'DD/MM/YYYY HH:00').format('YYYYMMDDHH')); - occupantModel.update(realm, dbOccupant, (errors) => { - if (errors) { - return reject({ errors }); - } - const rent = dbOccupant.rents.filter(rent => rent.term === termAsNumber)[0]; - resolve(FD.toRentData(rent, dbOccupant)); - }); + if (paymentData.description) { + settlements.description = paymentData.description; + } + } + + dbOccupant.rents = Contract.payTerm(contract, term, settlements).rents; + + return await new Promise((resolve, reject) => { + const termAsNumber = Number( + moment(term, 'DD/MM/YYYY HH:00').format('YYYYMMDDHH') + ); + occupantModel.update(realm, dbOccupant, (errors) => { + if (errors) { + return reject({ errors }); + } + const rent = dbOccupant.rents.filter( + (rent) => rent.term === termAsNumber + )[0]; + resolve(FD.toRentData(rent, dbOccupant)); }); + }); }; const rentsOfOccupant = async (req, res) => { - const realm = req.realm; - const { id } = req.params; - const term = Number(moment().format('YYYYMMDDHH')); - - try { - const dbOccupants = await _findOccupants(realm, id); - if (!dbOccupants.length) { - return res.sendStatus(404); - } - - const dbOccupant = dbOccupants[0]; - const rentsToReturn = dbOccupant.rents.map(currentRent => { - const rent = FD.toRentData(currentRent); - if (currentRent.term === term) { - rent.active = 'active'; - } - rent.vatRatio = dbOccupant.vatRatio; - return rent; - }); - - res.json({ - occupant: FD.toOccupantData(dbOccupant), - rents: rentsToReturn - }); - } catch (errors) { - logger.error(errors); - res.status(500).json({ errors }); + const realm = req.realm; + const { id } = req.params; + const term = Number(moment().format('YYYYMMDDHH')); + + try { + const dbOccupants = await _findOccupants(realm, id); + if (!dbOccupants.length) { + return res.sendStatus(404); } + + const dbOccupant = dbOccupants[0]; + const rentsToReturn = dbOccupant.rents.map((currentRent) => { + const rent = FD.toRentData(currentRent); + if (currentRent.term === term) { + rent.active = 'active'; + } + rent.vatRatio = dbOccupant.vatRatio; + return rent; + }); + + res.json({ + occupant: FD.toOccupantData(dbOccupant), + rents: rentsToReturn, + }); + } catch (errors) { + logger.error(errors); + res.status(500).json({ errors }); + } }; const rentOfOccupant = async (req, res) => { - const realm = req.realm; - const { id, month, year } = req.params; - const term = Number(moment(`${month}/${year}`, 'MM/YYYY').startOf('month').format('YYYYMMDDHH')); - try { - res.json(await _rentOfOccupant(realm, id, term)); - } catch (errors) { - logger.error(errors); - res.status(errors.status || 500).json({ errors }); - } + const realm = req.realm; + const { id, month, year } = req.params; + const term = Number( + moment(`${month}/${year}`, 'MM/YYYY').startOf('month').format('YYYYMMDDHH') + ); + try { + res.json(await _rentOfOccupant(realm, id, term)); + } catch (errors) { + logger.error(errors); + res.status(errors.status || 500).json({ errors }); + } }; const rentOfOccupantByTerm = async (req, res) => { - const realm = req.realm; - const { id, term } = req.params; - try { - res.json(await _rentOfOccupant(realm, id, term)); - } catch (errors) { - res.status(errors.status || 500).json({ errors }); - } + const realm = req.realm; + const { id, term } = req.params; + try { + res.json(await _rentOfOccupant(realm, id, term)); + } catch (errors) { + res.status(errors.status || 500).json({ errors }); + } }; const _rentOfOccupant = async (realm, tenantId, term) => { - const dbOccupants = await _findOccupants(realm, tenantId, Number(term)); - if (!dbOccupants.length) { - throw { status: 404, error: 'tenant not found' }; - } - const dbOccupant = dbOccupants[0]; - - if (!dbOccupant.rents.length) { - throw { status: 404, error: 'rent not found' }; - } - const rent = FD.toRentData(dbOccupant.rents[0], dbOccupant); - if (rent.term === Number(moment().format('YYYYMMDDHH'))) { - rent.active = 'active'; - } - rent.vatRatio = dbOccupant.vatRatio; - - return rent; + const dbOccupants = await _findOccupants(realm, tenantId, Number(term)); + if (!dbOccupants.length) { + throw { status: 404, error: 'tenant not found' }; + } + const dbOccupant = dbOccupants[0]; + + if (!dbOccupant.rents.length) { + throw { status: 404, error: 'rent not found' }; + } + const rent = FD.toRentData(dbOccupant.rents[0], dbOccupant); + if (rent.term === Number(moment().format('YYYYMMDDHH'))) { + rent.active = 'active'; + } + rent.vatRatio = dbOccupant.vatRatio; + + return rent; }; const all = async (req, res) => { - const realm = req.realm; + const realm = req.realm; + + let currentDate = moment().startOf('month'); + if (req.params.year && req.params.month) { + currentDate = moment(`${req.params.month}/${req.params.year}`, 'MM/YYYY'); + } + + try { + res.json(await _getRentsDataByTerm(realm, currentDate, 'months')); + } catch (errors) { + logger.error(errors); + res.status(500).json({ errors }); + } +}; +const overview = async (req, res) => { + try { + const realm = req.realm; let currentDate = moment().startOf('month'); if (req.params.year && req.params.month) { - currentDate = moment(`${req.params.month}/${req.params.year}`, 'MM/YYYY'); - } - - try { - res.json(await _getRentsDataByTerm(realm, currentDate, 'months')); - } catch (errors) { - logger.error(errors); - res.status(500).json({ errors }); + currentDate = moment(`${req.params.month}/${req.params.year}`, 'MM/YYYY'); } -}; -const overview = async (req, res) => { - try { - const realm = req.realm; - let currentDate = moment().startOf('month'); - if (req.params.year && req.params.month) { - currentDate = moment(`${req.params.month}/${req.params.year}`, 'MM/YYYY'); - } - - const { overview } = await _getRentsDataByTerm(realm, currentDate, 'months'); - res.json(overview); - } catch (errors) { - logger.error(errors); - res.status(500).json({ errors }); - } + const { overview } = await _getRentsDataByTerm( + realm, + currentDate, + 'months' + ); + res.json(overview); + } catch (errors) { + logger.error(errors); + res.status(500).json({ errors }); + } }; module.exports = { - update, - updateByTerm, - rentsOfOccupant, - rentOfOccupant, - rentOfOccupantByTerm, - all, - overview + update, + updateByTerm, + rentsOfOccupant, + rentOfOccupant, + rentOfOccupantByTerm, + all, + overview, }; diff --git a/backend/managers/templatemanager.js b/backend/managers/templatemanager.js new file mode 100644 index 0000000..9c2bb3b --- /dev/null +++ b/backend/managers/templatemanager.js @@ -0,0 +1,84 @@ +const config = require('../../config'); +const axios = require('axios'); + +//TODO: if no added value to have the api in loca then move that in nginx config + +const pdfGeneratorUrl = `${config.PDFGENERATOR_URL}/templates`; + +//////////////////////////////////////////////////////////////////////////////// +// Exported functions +//////////////////////////////////////////////////////////////////////////////// +const all = async (req, res) => { + const { language } = req; + + const response = await axios.get(pdfGeneratorUrl, { + headers: { + organizationId: req.headers.organizationid, + 'Accept-Language': language, + }, + }); + + res.json(response.data); +}; + +const one = async (req, res) => { + const { language } = req; + const { id } = req.params; + + const response = await axios.get(`${pdfGeneratorUrl}/${id}`, { + headers: { + organizationId: req.headers.organizationid, + 'Accept-Language': language, + }, + }); + + res.json(response.data); +}; + +const add = async (req, res) => { + const { language } = req; + + const response = await axios.post(pdfGeneratorUrl, req.body, { + headers: { + organizationId: req.headers.organizationid, + 'Accept-Language': language, + }, + }); + + res.json(response.data); +}; + +const update = async (req, res) => { + const { language } = req; + + const response = await axios.put(pdfGeneratorUrl, req.body, { + headers: { + organizationId: req.headers.organizationid, + 'Accept-Language': language, + }, + }); + + res.json(response.data); +}; + +const remove = async (req, res) => { + const { language } = req; + const { id } = req.params; + + const response = await axios.delete(`${pdfGeneratorUrl}/${id}`, { + headers: { + organizationId: req.headers.organizationid, + 'Accept-Language': language, + }, + }); + + res.json(response.data); +}; + +module.exports = { + all, + one, + add, + update, + remove, +}; diff --git a/backend/models/account.js b/backend/models/account.js index 1166caa..00e8a9d 100644 --- a/backend/models/account.js +++ b/backend/models/account.js @@ -4,43 +4,47 @@ const Model = require('./model'); const OF = require('./objectfilter'); class AccountModel extends Model { - constructor() { - super('accounts'); - this.schema = new OF({ - email: String, - password: String, - firstname: String, - lastname: String, - creation: String - }); - } + constructor() { + super('accounts'); + this.schema = new OF({ + email: String, + password: String, + firstname: String, + lastname: String, + creation: String, + }); + } - findOne(email, callback) { - super.findFilter(null, { - email: email.toLowerCase() - }, (errors, accounts) => { - if (errors) { - callback(errors); - } else if (!accounts || accounts.length === 0) { - callback(null, null); - } else { - callback(null, accounts[0]); - } - }); - } + findOne(email, callback) { + super.findFilter( + null, + { + email: email.toLowerCase(), + }, + (errors, accounts) => { + if (errors) { + callback(errors); + } else if (!accounts || accounts.length === 0) { + callback(null, null); + } else { + callback(null, accounts[0]); + } + } + ); + } - add(item, callback) { - super.add(null, item, callback); - } + add(item, callback) { + super.add(null, item, callback); + } - findAll(callback) { - super.findAll(null, function(errors, accounts) { - if (errors) { - return callback(errors); - } - callback(null, accounts); - }); - } + findAll(callback) { + super.findAll(null, function (errors, accounts) { + if (errors) { + return callback(errors); + } + callback(null, accounts); + }); + } } module.exports = new AccountModel(); diff --git a/backend/models/db.js b/backend/models/db.js index 8bccefe..700083a 100644 --- a/backend/models/db.js +++ b/backend/models/db.js @@ -7,244 +7,266 @@ const logger = require('winston'); require('sugar').extend(); function stringId2ObjectId(obj) { - if (obj) { - Object.keys(obj).forEach(key => { - if (key === '_id') { - if (typeof obj[key] == 'string') { - obj[key] = mongojs.ObjectId(obj[key]); - } else if (typeof obj[key] == 'object' && obj[key].$in) { - obj[key].$in = obj[key].$in.map(_id => mongojs.ObjectId(_id)); - } - } else if (typeof obj[key] == 'object') { - obj[key] = stringId2ObjectId(obj[key]); - } - }); - } - return obj; + if (obj) { + Object.keys(obj).forEach((key) => { + if (key === '_id') { + if (typeof obj[key] == 'string') { + obj[key] = mongojs.ObjectId(obj[key]); + } else if (typeof obj[key] == 'object' && obj[key].$in) { + obj[key].$in = obj[key].$in.map((_id) => mongojs.ObjectId(_id)); + } + } else if (typeof obj[key] == 'object') { + obj[key] = stringId2ObjectId(obj[key]); + } + }); + } + return obj; } function buildFilter(realm, inputfilter) { - const filter = stringId2ObjectId(inputfilter); - if (realm) { - const realmFilter = { - realmId: realm._id - }; - const andArray = filter.$query ? filter.$query.$and : null; - if (andArray) { - andArray.push(realmFilter); - } else { - if (!filter.$query) { - filter.$query = {}; - } - filter.$query.$and = [realmFilter]; - } + const filter = stringId2ObjectId(inputfilter); + if (realm) { + const realmFilter = { + realmId: realm._id, + }; + const andArray = filter.$query ? filter.$query.$and : null; + if (andArray) { + andArray.push(realmFilter); + } else { + if (!filter.$query) { + filter.$query = {}; + } + filter.$query.$and = [realmFilter]; } - return filter; + } + return filter; } function logDBError(err) { - logger.error(new Error().stack); - logger.error(err); + logger.error(new Error().stack); + logger.error(err); } const collections = []; let db; module.exports = { - init() { - if (!db) { - return new Promise((resolve, reject) => { - logger.debug(`connecting database ${config.database}...`); - db = mongojs(config.database, collections); - db.listCollections(() => { }); // Run this command to force connection a this stage - db.on('connect', function () { - logger.info(`connected to ${config.database}`); - resolve(db); - }); - - db.on('error', function (err) { - logger.error(`cannot connect to ${config.database}`); - reject(err); - }); - }); - } - logger.debug('database already connected'); - return Promise.resolve(db); - }, - - exists() { - return new Promise((resolve/*, reject*/) => { - db.getCollectionNames((error, collectionNames) => { - resolve(!(error || !collectionNames || collectionNames.length === 0)); - }); + init() { + if (!db) { + return new Promise((resolve, reject) => { + logger.debug(`connecting database ${config.database}...`); + db = mongojs(config.database, collections); + db.listCollections(() => {}); // Run this command to force connection a this stage + db.on('connect', function () { + logger.info(`connected to ${config.database}`); + resolve(db); }); - }, - addCollection(collection) { - if (collections.indexOf(collection.toLowerCase()) >= 0) { - logger.silly(`db collection ${collection} already added`); - return; - } - collections.push(collection.toLowerCase()); - logger.silly(`db collections have been updated ${collections}`); - }, - - findItemById(realm, collection, id, callback) { - logger.info(`find item by id in collection ${collection} ${realm && realm.length > 0 ? 'in realm: ' + realm.name : ''}`); - const query = buildFilter(realm, { - $query: { - _id: id - } + db.on('error', function (err) { + logger.error(`cannot connect to ${config.database}`); + reject(err); }); - logger.debug(`\tfilter is ${JSON.stringify(query)}`); - - db[collection].find(query, function (err, dbItems) { - if (err || !dbItems || dbItems.length < 0) { - if (err) { - logDBError(err); - } - callback(['item has not been found in database']); - return; - } - logger.silly('\treturned values', dbItems.join('\n')); - callback([], dbItems); - }); - }, + }); + } + logger.debug('database already connected'); + return Promise.resolve(db); + }, - listWithFilter(realm, collection, filter, callback) { - logger.info(`find items in collection: ${collection}${realm && realm.length > 0 ? ' in realm: ' + realm.name : ''}`); - const query = buildFilter(realm, filter); - if (query) { - logger.debug(`\tfilter is ${JSON.stringify(query)}`); - } - db[collection].find(query, function (err, dbItems) { - if (err) { - logDBError(err); - callback(['item has not been found in database']); - return; - } - if (dbItems) { - logger.silly('\treturned values', dbItems.join('\n')); - } else { - logger.silly('\treturned an empty list'); - } - callback([], dbItems || []); - }); - }, + exists() { + return new Promise((resolve /*, reject*/) => { + db.getCollectionNames((error, collectionNames) => { + resolve(!(error || !collectionNames || collectionNames.length === 0)); + }); + }); + }, + + addCollection(collection) { + if (collections.indexOf(collection.toLowerCase()) >= 0) { + logger.silly(`db collection ${collection} already added`); + return; + } + collections.push(collection.toLowerCase()); + logger.silly(`db collections have been updated ${collections}`); + }, - add(realm, collection, item, callback) { - logger.info(`insert item in collection ${collection} ${realm && realm.length > 0 ? 'in realm: ' + realm.name : ''}`); + findItemById(realm, collection, id, callback) { + logger.info( + `find item by id in collection ${collection} ${ + realm && realm.length > 0 ? 'in realm: ' + realm.name : '' + }` + ); + const query = buildFilter(realm, { + $query: { + _id: id, + }, + }); + logger.debug(`\tfilter is ${JSON.stringify(query)}`); - item._id = mongojs.ObjectId(); - if (realm) { - item.realmName = realm.name; - item.realmId = realm._id; + db[collection].find(query, function (err, dbItems) { + if (err || !dbItems || dbItems.length < 0) { + if (err) { + logDBError(err); } - logger.debug('\titem is', item); - db[collection].save(item, function (err, saved) { - if (err || !saved) { - if (err) { - logDBError(err); - } - callback(['item not added in database']); - return; - } - item._id = item._id.toString(); - logger.silly('\treturned values is', item); - callback([], item); - }); - }, - - update(realm, collection, item, callback) { - logger.info(`update items in collection: ${collection}${realm && realm.length > 0 ? ' in realm: ' + realm.name : ''}`); - const _id = item._id.toString(); - delete item._id; - const filter = buildFilter(null, { _id }); - const itemToUpdate = { - $set: item - }; - if (realm) { - itemToUpdate.$set = Object.merge(item, { - realmName: realm.name, - realmId: realm._id - }); + callback(['item has not been found in database']); + return; + } + logger.silly('\treturned values', dbItems.join('\n')); + callback([], dbItems); + }); + }, + + listWithFilter(realm, collection, filter, callback) { + logger.info( + `find items in collection: ${collection}${ + realm && realm.length > 0 ? ' in realm: ' + realm.name : '' + }` + ); + const query = buildFilter(realm, filter); + if (query) { + logger.debug(`\tfilter is ${JSON.stringify(query)}`); + } + db[collection].find(query, function (err, dbItems) { + if (err) { + logDBError(err); + callback(['item has not been found in database']); + return; + } + if (dbItems) { + logger.silly('\treturned values', dbItems.join('\n')); + } else { + logger.silly('\treturned an empty list'); + } + callback([], dbItems || []); + }); + }, + + add(realm, collection, item, callback) { + logger.info( + `insert item in collection ${collection} ${ + realm && realm.length > 0 ? 'in realm: ' + realm.name : '' + }` + ); + + item._id = mongojs.ObjectId(); + if (realm) { + item.realmName = realm.name; + item.realmId = realm._id; + } + logger.debug('\titem is', item); + db[collection].save(item, function (err, saved) { + if (err || !saved) { + if (err) { + logDBError(err); } - logger.debug(`\tfilter is ${JSON.stringify(filter)}`); - logger.silly(`\titem to update is ${JSON.stringify(itemToUpdate)}`); - - db[collection].update( - filter, - itemToUpdate, { - multi: true - }, - (err, saved) => { - if (err || !saved) { - if (err) { - logDBError(err); - } - callback(['item has not been updated in database']); - return; - } - item._id = _id; - logger.silly('\treturned value is', item); - callback([], item); - } - ); - }, - - upsert(realm, collection, query, fieldsToSet, fieldsToSetOnInsert, callback) { - logger.info(`upsert in collection ${collection} ${realm && realm.length > 0 ? 'in realm: ' + realm.name : ''}`); - - const fieldsToUpdate = { - $set: fieldsToSet, - $setOnInsert: fieldsToSetOnInsert - }; - if (realm) { - fieldsToUpdate.$set = Object.merge(fieldsToSet, { - realmName: realm.name, - realmId: realm._id - }); + callback(['item not added in database']); + return; + } + item._id = item._id.toString(); + logger.silly('\treturned values is', item); + callback([], item); + }); + }, + + update(realm, collection, item, callback) { + logger.info( + `update items in collection: ${collection}${ + realm && realm.length > 0 ? ' in realm: ' + realm.name : '' + }` + ); + const _id = item._id.toString(); + delete item._id; + const filter = buildFilter(null, { _id }); + const itemToUpdate = { + $set: item, + }; + if (realm) { + itemToUpdate.$set = Object.merge(item, { + realmName: realm.name, + realmId: realm._id, + }); + } + logger.debug(`\tfilter is ${JSON.stringify(filter)}`); + logger.silly(`\titem to update is ${JSON.stringify(itemToUpdate)}`); + + db[collection].update( + filter, + itemToUpdate, + { + multi: true, + }, + (err, saved) => { + if (err || !saved) { + if (err) { + logDBError(err); + } + callback(['item has not been updated in database']); + return; } - const options = { - upsert: true - }; - - logger.debug(`\tfilter is ${JSON.stringify(query)}`); - logger.silly(`\titem to update is ${JSON.stringify(fieldsToSet)}`); - logger.silly(`\titem to insert is ${JSON.stringify(fieldsToSetOnInsert)}`); - db[collection].update( - query, - fieldsToUpdate, - options, - (err, saved) => { - if (err || !saved) { - if (err) { - logDBError(err); - } - callback(['item has not been updated in database']); - return; - } - callback([]); - } - ); - }, - - remove(realm, collection, items, callback) { - logger.info(`remove items in collection: ${collection}${realm && realm.length > 0 ? 'in realm: ' + realm.name : ''}`); - const filter = buildFilter(null, { - $or: items.map(item => { return { _id: item }; }) - }); + item._id = _id; + logger.silly('\treturned value is', item); + callback([], item); + } + ); + }, - logger.debug(`\tfilter is ${JSON.stringify(filter)}`); - db[collection].remove(filter, function (err, deleted) { - if (err || !deleted) { - if (err) { - logDBError(err); - } - callback(['item has not been deleted in database']); - return; - } - callback([]); - }); + upsert(realm, collection, query, fieldsToSet, fieldsToSetOnInsert, callback) { + logger.info( + `upsert in collection ${collection} ${ + realm && realm.length > 0 ? 'in realm: ' + realm.name : '' + }` + ); + + const fieldsToUpdate = { + $set: fieldsToSet, + $setOnInsert: fieldsToSetOnInsert, + }; + if (realm) { + fieldsToUpdate.$set = Object.merge(fieldsToSet, { + realmName: realm.name, + realmId: realm._id, + }); } + const options = { + upsert: true, + }; + + logger.debug(`\tfilter is ${JSON.stringify(query)}`); + logger.silly(`\titem to update is ${JSON.stringify(fieldsToSet)}`); + logger.silly(`\titem to insert is ${JSON.stringify(fieldsToSetOnInsert)}`); + db[collection].update(query, fieldsToUpdate, options, (err, saved) => { + if (err || !saved) { + if (err) { + logDBError(err); + } + callback(['item has not been updated in database']); + return; + } + callback([]); + }); + }, + + remove(realm, collection, items, callback) { + logger.info( + `remove items in collection: ${collection}${ + realm && realm.length > 0 ? 'in realm: ' + realm.name : '' + }` + ); + const filter = buildFilter(null, { + $or: items.map((item) => { + return { _id: item }; + }), + }); + + logger.debug(`\tfilter is ${JSON.stringify(filter)}`); + db[collection].remove(filter, function (err, deleted) { + if (err || !deleted) { + if (err) { + logDBError(err); + } + callback(['item has not been deleted in database']); + return; + } + callback([]); + }); + }, }; diff --git a/backend/models/document.js b/backend/models/document.js index 9d0343f..d0dd30a 100644 --- a/backend/models/document.js +++ b/backend/models/document.js @@ -2,12 +2,12 @@ const OF = require('./objectfilter'); class DocumentModel { - constructor() { - this.schema = new OF({ - _id: String, - documents: Array - }); - } + constructor() { + this.schema = new OF({ + _id: String, + documents: Array, + }); + } } module.exports = new DocumentModel(); diff --git a/backend/models/lease.js b/backend/models/lease.js index 6be1475..bb7b264 100644 --- a/backend/models/lease.js +++ b/backend/models/lease.js @@ -3,31 +3,34 @@ const OF = require('./objectfilter'); const Model = require('./model'); class LeaseModel extends Model { - constructor() { - super('leases'); - this.schema = new OF({ - _id: String, - name: String, - description: String, - numberOfTerms: Number, - timeRange: String, // days, weeks, months, years - active: Boolean, - system: Boolean - }); - } + constructor() { + super('leases'); + this.schema = new OF({ + _id: String, + name: String, + description: String, + numberOfTerms: Number, + timeRange: String, // days, weeks, months, years + active: Boolean, + system: Boolean, + }); + } - findAll(realm, callback) { - super.findAll(realm, (errors, leases) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } + findAll(realm, callback) { + super.findAll(realm, (errors, leases) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } - callback(null, leases.sort((p1, p2) => { - return p1.name.localeCompare(p2.name); - })); - }); - } + callback( + null, + leases.sort((p1, p2) => { + return p1.name.localeCompare(p2.name); + }) + ); + }); + } } module.exports = new LeaseModel(); diff --git a/backend/models/model.js b/backend/models/model.js index ecfc2c1..f1cbcbd 100644 --- a/backend/models/model.js +++ b/backend/models/model.js @@ -1,90 +1,96 @@ 'use strict'; const logger = require('winston'); -const db = require('./db'); +const db = require('./db'); module.exports = class Model { + constructor(collection) { + this.collection = collection; + db.addCollection(collection); + } - constructor(collection) { - this.collection = collection; - db.addCollection(collection); - } + findOne(realm, id, callback) { + db.findItemById(realm, this.collection, id, (errors, dbItems) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } + + const item = dbItems && dbItems.length > 0 ? dbItems[0] : null; + callback(null, this.schema ? this.schema.filter(item) : item); + }); + } - findOne(realm, id, callback) { - db.findItemById(realm, this.collection, id, (errors, dbItems) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } + findAll(realm, callback) { + this.findFilter(realm, {}, callback); + } - const item = (dbItems && dbItems.length > 0) ? dbItems[0] : null; - callback(null, this.schema ? this.schema.filter(item) : item); + findFilter(realm, filter, callback) { + db.listWithFilter(realm, this.collection, filter, (errors, dbItems) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } + const items = dbItems || []; + if (this.schema) { + items.forEach((item, index) => { + items[index] = this.schema.filter(item); }); - } + } + callback(null, items); + }); + } - findAll(realm, callback) { - this.findFilter(realm, {}, callback); - } + upsert(realm, query, fieldsToSet, fieldsToSetOnInsert, callback) { + const updateSchema = this.updateSchema || this.schema; - findFilter(realm, filter, callback) { - db.listWithFilter(realm, this.collection, filter, (errors, dbItems) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } - const items = dbItems || []; - if (this.schema) { - items.forEach((item, index) => { - items[index] = this.schema.filter(item); - }); - } - callback(null, items); - }); + if (!updateSchema.exists(fieldsToSet)) { + logger.error('cannot update', this.collection, fieldsToSet, 'not valid'); + callback(['cannot update database fields not valid']); + return; } - upsert(realm, query, fieldsToSet, fieldsToSetOnInsert, callback) { - const updateSchema = this.updateSchema || this.schema; - - if (!updateSchema.exists(fieldsToSet)) { - logger.error('cannot update', this.collection, fieldsToSet, 'not valid'); - callback(['cannot update database fields not valid']); - return; + db.upsert( + realm, + this.collection, + query, + fieldsToSet, + this.schema.filter(fieldsToSetOnInsert), + (errors) => { + if (errors && errors.length > 0) { + callback(errors); + return; } + callback(null); + } + ); + } - db.upsert(realm, this.collection, query, fieldsToSet, this.schema.filter(fieldsToSetOnInsert), (errors) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } - callback(null); - }); - } + update(realm, item, callback) { + const updateSchema = this.updateSchema || this.schema; + const itemToUpdate = updateSchema.filter(item); + db.update(realm, this.collection, itemToUpdate, (errors, dbItem) => { + if (errors && errors.length > 0) { + return callback(errors); + } + callback(null, this.schema.filter(dbItem)); + }); + } - update(realm, item, callback) { - const updateSchema = this.updateSchema || this.schema; - const itemToUpdate = updateSchema.filter(item); - db.update(realm, this.collection, itemToUpdate, (errors, dbItem) => { - if (errors && errors.length > 0) { - return callback(errors); - } - callback(null, this.schema.filter(dbItem)); - }); - } + add(realm, item, callback) { + const addSchema = this.addSchema || this.schema; + const itemToAdd = addSchema.filter(item); + db.add(realm, this.collection, itemToAdd, (errors, dbItem) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } + callback(null, this.schema.filter(dbItem)); + }); + } - add(realm, item, callback) { - const addSchema = this.addSchema || this.schema; - const itemToAdd = addSchema.filter(item); - db.add(realm, this.collection, itemToAdd, (errors, dbItem) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } - callback(null, this.schema.filter(dbItem)); - }); - } - - remove(realm, ids, callback) { - db.remove(realm, this.collection, ids, (errors) => { - callback(errors && errors.length > 0 ? errors : null); - }); - } + remove(realm, ids, callback) { + db.remove(realm, this.collection, ids, (errors) => { + callback(errors && errors.length > 0 ? errors : null); + }); + } }; diff --git a/backend/models/notification.js b/backend/models/notification.js index 39194fb..b389900 100644 --- a/backend/models/notification.js +++ b/backend/models/notification.js @@ -3,13 +3,13 @@ const OF = require('./objectfilter'); const Model = require('./model'); class NotificationModel extends Model { - constructor() { - super('notifications'); - this.schema = new OF({ - id: String, - status: String - }); - } + constructor() { + super('notifications'); + this.schema = new OF({ + id: String, + status: String, + }); + } } module.exports = new NotificationModel(); diff --git a/backend/models/objectfilter.js b/backend/models/objectfilter.js index 2151437..7ee091f 100644 --- a/backend/models/objectfilter.js +++ b/backend/models/objectfilter.js @@ -4,64 +4,67 @@ const logger = require('winston'); sugar.extend(); module.exports = class ObjectFilter { - constructor(schema) { - this.schema = schema; - } + constructor(schema) { + this.schema = schema; + } - filter(data) { - return Object.keys(this.schema).reduce((filteredData, key) => { - const type = this.schema[key]; - const value = data[key]; + filter(data) { + return Object.keys(this.schema).reduce((filteredData, key) => { + const type = this.schema[key]; + const value = data[key]; - if (typeof value != 'undefined') { - if (type === Boolean) { - if (typeof value == 'string' && (value === 'true' || value === 'false')) { - filteredData[key] = (value === 'true'); - } else if (typeof value == 'boolean') { - filteredData[key] = value; - } - } - - else if (type === Number) { - let number = value; - if (typeof value == 'string') { - number = Number(value.replace(',', '.')); - } - if (!isNaN(number)) { - filteredData[key] = number; - } else { - filteredData[key] = 0; - } - } - - else if (type === Array) { - if (Array.isArray(value)) { - filteredData[key] = value; - } - } - - else if (type === Object) { - if (typeof value == 'object') { - filteredData[key] = value; - } - } - - else if (type === String) { - if (key === '_id' && typeof value == 'object') { - filteredData[key] = value.toString(); - } else if (typeof value == 'string') { - filteredData[key] = value; - } - } - - else { - logger.error('type unsupported ' + type + ' for schema ' + JSON.stringify(this.schema)); - //throw new Error('Cannot valid schema type unsupported ' + type); - } - } else { - logger.silly('undefined value for key ' + key + ' of schema ' + JSON.stringify(this.schema)); - } - return filteredData; - }, {}); - } + if (typeof value != 'undefined') { + if (type === Boolean) { + if ( + typeof value == 'string' && + (value === 'true' || value === 'false') + ) { + filteredData[key] = value === 'true'; + } else if (typeof value == 'boolean') { + filteredData[key] = value; + } + } else if (type === Number) { + let number = value; + if (typeof value == 'string') { + number = Number(value.replace(',', '.')); + } + if (!isNaN(number)) { + filteredData[key] = number; + } else { + filteredData[key] = 0; + } + } else if (type === Array) { + if (Array.isArray(value)) { + filteredData[key] = value; + } + } else if (type === Object) { + if (typeof value == 'object') { + filteredData[key] = value; + } + } else if (type === String) { + if (key === '_id' && typeof value == 'object') { + filteredData[key] = value.toString(); + } else if (typeof value == 'string') { + filteredData[key] = value; + } + } else { + logger.error( + 'type unsupported ' + + type + + ' for schema ' + + JSON.stringify(this.schema) + ); + //throw new Error('Cannot valid schema type unsupported ' + type); + } + } else { + logger.silly( + 'undefined value for key ' + + key + + ' of schema ' + + JSON.stringify(this.schema) + ); + } + return filteredData; + }, {}); + } }; diff --git a/backend/models/occupant.js b/backend/models/occupant.js index dc9c65c..2090319 100644 --- a/backend/models/occupant.js +++ b/backend/models/occupant.js @@ -3,56 +3,59 @@ const OF = require('./objectfilter'); const Model = require('./model'); class OccupantModel extends Model { - constructor() { - super('occupants'); - this.schema = new OF({ - _id: String, - isCompany: Boolean, - company: String, - legalForm: String, - siret: String, - capital: Number, - manager: String, - name: String, - street1: String, - street2: String, - zipCode: String, - city: String, - state: String, - country: String, - contacts: Array, - contract: String, - leaseId: String, - beginDate: String, - endDate: String, - frequency: String, - terminationDate: String, - guarantyPayback: Number, - properties: Array, // [{ propertyId, property: { ... }, entryDate, exitDate, rent, expenses: [title, amount] }] - guaranty: Number, - reference: String, - isVat: Boolean, - vatRatio: Number, - discount: Number, - rents: Array - }); - } + constructor() { + super('occupants'); + this.schema = new OF({ + _id: String, + isCompany: Boolean, + company: String, + legalForm: String, + siret: String, + capital: Number, + manager: String, + name: String, + street1: String, + street2: String, + zipCode: String, + city: String, + state: String, + country: String, + contacts: Array, + contract: String, + leaseId: String, + beginDate: String, + endDate: String, + frequency: String, + terminationDate: String, + guarantyPayback: Number, + properties: Array, // [{ propertyId, property: { ... }, entryDate, exitDate, rent, expenses: [title, amount] }] + guaranty: Number, + reference: String, + isVat: Boolean, + vatRatio: Number, + discount: Number, + rents: Array, + }); + } - findAll(realm, callback) { - super.findAll(realm, (errors, occupants) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } + findAll(realm, callback) { + super.findAll(realm, (errors, occupants) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } - callback(null, occupants.sort((o1, o2) => { - const name1 = (o1.isCompany ? o1.company : o1.name) || ''; - const name2 = (o2.isCompany ? o2.company : o2.name) || ''; + callback( + null, + occupants.sort((o1, o2) => { + const name1 = (o1.isCompany ? o1.company : o1.name) || ''; + const name2 = (o2.isCompany ? o2.company : o2.name) || ''; - return name1.localeCompare(name2); - })); - }); - } + return name1.localeCompare(name2); + }) + ); + }); + } } module.exports = new OccupantModel(); diff --git a/backend/models/property.js b/backend/models/property.js index 110ad5e..81ece54 100644 --- a/backend/models/property.js +++ b/backend/models/property.js @@ -2,45 +2,48 @@ const OF = require('./objectfilter'); const Model = require('./model'); class PropertyModel extends Model { - constructor() { - super('properties'); - this.schema = new OF({ - _id: String, - type: String, - name: String, - description: String, - surface: Number, - phone: String, - digicode: String, - address: Object, // { street1, street2, zipCode, city, state, country } + constructor() { + super('properties'); + this.schema = new OF({ + _id: String, + type: String, + name: String, + description: String, + surface: Number, + phone: String, + digicode: String, + address: Object, // { street1, street2, zipCode, city, state, country } - price: Number, + price: Number, - // TODO moved in Occupant.properties model - expense: Number, + // TODO moved in Occupant.properties model + expense: Number, - // TODO to remove, replaced by address - building: String, - level: String, - location: String, - }); - } + // TODO to remove, replaced by address + building: String, + level: String, + location: String, + }); + } - findAll(realm, callback) { - super.findAll(realm, (errors, properties) => { - if (errors && errors.length > 0) { - callback(errors); - return; - } + findAll(realm, callback) { + super.findAll(realm, (errors, properties) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } - callback(null, properties.sort((p1, p2) => { - if (p1.type === p2.type) { - return p1.name.localeCompare(p2.name); - } - return p1.type.localeCompare(p2.type); - })); - }); - } + callback( + null, + properties.sort((p1, p2) => { + if (p1.type === p2.type) { + return p1.name.localeCompare(p2.name); + } + return p1.type.localeCompare(p2.type); + }) + ); + }); + } } module.exports = new PropertyModel(); diff --git a/backend/models/realm.js b/backend/models/realm.js index 0f2d090..ea40d62 100644 --- a/backend/models/realm.js +++ b/backend/models/realm.js @@ -4,114 +4,114 @@ const Model = require('./model'); const logger = require('winston'); class RealmModel extends Model { - constructor() { - super('realms'); - this.schema = new OF({ - _id: String, - name: String, - members: Array, // [{ name, email, role, registered },] - addresses: Array, // [{ street1, street2, zipCode, city, state, country }, ] - bankInfo: Object, // { name, iban } - contacts: Array, // [{ name, email, phone1, phone2 }] - isCompany: Boolean, - companyInfo: Object, // { name, legalStructure, capital, ein, dos, vatNumber, legalRepresentative } - locale: String, - currency: String, - tenants: Array , // [{ name, emails, access },] - thirdParties: Object, // { mailgun: { apiKey, domain, fromEmail, replyToEmail }} + constructor() { + super('realms'); + this.schema = new OF({ + _id: String, + name: String, + members: Array, // [{ name, email, role, registered },] + addresses: Array, // [{ street1, street2, zipCode, city, state, country }, ] + bankInfo: Object, // { name, iban } + contacts: Array, // [{ name, email, phone1, phone2 }] + isCompany: Boolean, + companyInfo: Object, // { name, legalStructure, capital, ein, dos, vatNumber, legalRepresentative } + locale: String, + currency: String, + tenants: Array, // [{ name, emails, access },] + thirdParties: Object, // { mailgun: { apiKey, domain, fromEmail, replyToEmail }} - // TODO to remove, replaced by companyInfo - creation: String, - company: String, - legalForm: String, - vatNumber: String, - capital: Number, - siret: String, - rcs: String, - manager: String, + // TODO to remove, replaced by companyInfo + creation: String, + company: String, + legalForm: String, + vatNumber: String, + capital: Number, + siret: String, + rcs: String, + manager: String, - // TODO to remove, replaced by bankInfo - bank: String, - rib: String, + // TODO to remove, replaced by bankInfo + bank: String, + rib: String, - // TODO to remove, replaced by contacts - contact: String, - email: String, - phone1: String, - phone2: String, + // TODO to remove, replaced by contacts + contact: String, + email: String, + phone1: String, + phone2: String, - // TODO to remove, replaced by addresses - street1: String, - street2: String, - zipCode: String, - city: String, + // TODO to remove, replaced by addresses + street1: String, + street2: String, + zipCode: String, + city: String, - // TODO to remove, replaced by members - administrator: String, - user1: String, - user2: String, - user3: String, - user4: String, - user5: String, - user6: String, - user7: String, - user8: String, - user9: String, - user10: String - }); - } + // TODO to remove, replaced by members + administrator: String, + user1: String, + user2: String, + user3: String, + user4: String, + user5: String, + user6: String, + user7: String, + user8: String, + user9: String, + user10: String, + }); + } - findOne(id, callback) { - super.findOne(null, id, function(errors, realm) { - if (errors) { - callback(errors); - } else if (!realm) { - callback(['realm not found']); - } else { - callback(null, realm); - } - }); - } + findOne(id, callback) { + super.findOne(null, id, function (errors, realm) { + if (errors) { + callback(errors); + } else if (!realm) { + callback(['realm not found']); + } else { + callback(null, realm); + } + }); + } - findAll(callback) { - super.findAll(null, function(errors, realms) { - if (errors) { - callback(errors); - } else if (!realms || realms.length === 0) { - callback(null, null); - } else { - callback(null, realms); - } - }); - } + findAll(callback) { + super.findAll(null, function (errors, realms) { + if (errors) { + callback(errors); + } else if (!realms || realms.length === 0) { + callback(null, null); + } else { + callback(null, realms); + } + }); + } - findByEmail(email, callback) { - // TODO to optimize: filter should by applied on DB - super.findAll(null, function(errors, realms) { - if (errors) { - callback(errors); - } else if (!realms || realms.length === 0) { - callback(null, null); - } else { - const realmsFound = realms.filter( - realm => realm.members.map(({ email }) => email).includes(email) - ); - callback(null, realmsFound); - } - }); - } + findByEmail(email, callback) { + // TODO to optimize: filter should by applied on DB + super.findAll(null, function (errors, realms) { + if (errors) { + callback(errors); + } else if (!realms || realms.length === 0) { + callback(null, null); + } else { + const realmsFound = realms.filter((realm) => + realm.members.map(({ email }) => email).includes(email) + ); + callback(null, realmsFound); + } + }); + } - add(realm, callback) { - super.add(null, realm, callback); - } + add(realm, callback) { + super.add(null, realm, callback); + } - update(realm, callback) { - super.update(null, realm, callback); - } + update(realm, callback) { + super.update(null, realm, callback); + } - remove() { - logger.error('method not implemented!'); - } + remove() { + logger.error('method not implemented!'); + } } module.exports = new RealmModel(); diff --git a/backend/models/rent.js b/backend/models/rent.js index f29608e..fdacde8 100644 --- a/backend/models/rent.js +++ b/backend/models/rent.js @@ -1,19 +1,19 @@ const OF = require('./objectfilter'); class RentModel { - constructor() { - this.paymentSchema = new OF({ - _id: String, - month: Number, - year: Number, - payments: Array, - description: String, - promo: Number, - notepromo: String, - extracharge: Number, - noteextracharge: String - }); - } + constructor() { + this.paymentSchema = new OF({ + _id: String, + month: Number, + year: Number, + payments: Array, + description: String, + promo: Number, + notepromo: String, + extracharge: Number, + noteextracharge: String, + }); + } } module.exports = new RentModel(); diff --git a/backend/pages/accounting/index.js b/backend/pages/accounting/index.js index efae562..c31aac3 100644 --- a/backend/pages/accounting/index.js +++ b/backend/pages/accounting/index.js @@ -1,7 +1,7 @@ module.exports = () => { - return { - id: 'accounting', - params: '/:year?/:month?', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'accounting', + params: '/:year?/:month?', + restricted: true, + }; +}; diff --git a/backend/pages/accounting/model/index.js b/backend/pages/accounting/model/index.js index 64f090f..38dc923 100644 --- a/backend/pages/accounting/model/index.js +++ b/backend/pages/accounting/model/index.js @@ -1,5 +1,4 @@ - -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/dashboard/index.js b/backend/pages/dashboard/index.js index 1d2b037..2afa32b 100644 --- a/backend/pages/dashboard/index.js +++ b/backend/pages/dashboard/index.js @@ -1,6 +1,6 @@ module.exports = () => { - return { - id: 'dashboard', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'dashboard', + restricted: true, + }; +}; diff --git a/backend/pages/dashboard/model/index.js b/backend/pages/dashboard/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/dashboard/model/index.js +++ b/backend/pages/dashboard/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/ejshelpers.js b/backend/pages/ejshelpers.js index 2dd7767..dcdfdd9 100644 --- a/backend/pages/ejshelpers.js +++ b/backend/pages/ejshelpers.js @@ -1,83 +1,83 @@ const moment = require('moment'); -const _textToNumber = text => { - let value = parseFloat(text); - if (isNaN(value)) { - value = 0; - } - return value; +const _textToNumber = (text) => { + let value = parseFloat(text); + if (isNaN(value)) { + value = 0; + } + return value; }; module.exports = { - formatSurface(text, hideUnit, emptyForZero) { - const value = _textToNumber(text); + formatSurface(text, hideUnit, emptyForZero) { + const value = _textToNumber(text); - if (value === 0 && emptyForZero) { - return ''; - } + if (value === 0 && emptyForZero) { + return ''; + } - if (hideUnit) { - return this.Intl.NumberFormat.format(value); - } + if (hideUnit) { + return this.Intl.NumberFormat.format(value); + } - return `${this.Intl.NumberFormat.format(value)} m2`; - }, + return `${this.Intl.NumberFormat.format(value)} m2`; + }, - formatNumber(text, emptyForZero) { - const value = _textToNumber(text); + formatNumber(text, emptyForZero) { + const value = _textToNumber(text); - if (value === 0 && emptyForZero) { - return ''; - } + if (value === 0 && emptyForZero) { + return ''; + } - return this.Intl.NumberFormat.format(value); - }, + return this.Intl.NumberFormat.format(value); + }, - formatMoney(text, hideCurrency, emptyForZero) { - const value = _textToNumber(text); + formatMoney(text, hideCurrency, emptyForZero) { + const value = _textToNumber(text); - if (value === 0 && emptyForZero) { - return ''; - } + if (value === 0 && emptyForZero) { + return ''; + } - if (hideCurrency) { - return this.Intl.NumberFormat.format(value); - } + if (hideCurrency) { + return this.Intl.NumberFormat.format(value); + } - return this.Intl.NumberFormatCurrency.format(value); - }, + return this.Intl.NumberFormatCurrency.format(value); + }, - formatPercent(text, hidePercent, emptyForZero) { - const value = _textToNumber(text); + formatPercent(text, hidePercent, emptyForZero) { + const value = _textToNumber(text); - if (value === 0 && emptyForZero) { - return ''; - } + if (value === 0 && emptyForZero) { + return ''; + } - if (hidePercent) { - return this.Intl.NumberFormat.format(value); - } + if (hidePercent) { + return this.Intl.NumberFormat.format(value); + } - return this.Intl.NumberFormatPercent.format(value); - }, + return this.Intl.NumberFormatPercent.format(value); + }, - formatMonth(text) { - return moment.months()[parseInt(text, 10)-1]; - }, + formatMonth(text) { + return moment.months()[parseInt(text, 10) - 1]; + }, - formatMonthYear(month, year) { - return moment.monthsShort()[parseInt(month, 10)-1] + ' ' + year; - }, + formatMonthYear(month, year) { + return moment.monthsShort()[parseInt(month, 10) - 1] + ' ' + year; + }, - formatDate(text) { - return moment(text, 'DD/MM/YYYY').format('L'); - }, + formatDate(text) { + return moment(text, 'DD/MM/YYYY').format('L'); + }, - formatDateText(text) { - return moment(text, 'DD/MM/YYYY').format('LL'); - }, + formatDateText(text) { + return moment(text, 'DD/MM/YYYY').format('LL'); + }, - formatDateTime(text) { - return moment(text, 'DD/MM/YYYY HH:MM').format('L LTS'); - } -}; \ No newline at end of file + formatDateTime(text) { + return moment(text, 'DD/MM/YYYY HH:MM').format('L LTS'); + }, +}; diff --git a/backend/pages/index.js b/backend/pages/index.js index 6e7fe04..4e3bab7 100644 --- a/backend/pages/index.js +++ b/backend/pages/index.js @@ -9,31 +9,30 @@ const publicRestrictedList = []; // list of {id, params} const root_pages_dir = path.join(__dirname); fs.readdirSync(root_pages_dir) - .filter(page => fs.lstatSync(path.join(root_pages_dir, page)).isDirectory()) - .forEach(page => { - const pageDesc = Object.assign({ - public: false, - restricted: false, - supportView: true - }, require(`./${page}`)()); + .filter((page) => fs.lstatSync(path.join(root_pages_dir, page)).isDirectory()) + .forEach((page) => { + const pageDesc = Object.assign( + { + public: false, + restricted: false, + supportView: true, + }, + require(`./${page}`)() + ); - if (pageDesc.public && pageDesc.restricted) { - publicRestrictedList.push(pageDesc); - } else if (pageDesc.public) { - publicList.push(pageDesc); - } else { - restrictedList.push(pageDesc); - } - logger.debug(`loaded page ${page}`); - }); + if (pageDesc.public && pageDesc.restricted) { + publicRestrictedList.push(pageDesc); + } else if (pageDesc.public) { + publicList.push(pageDesc); + } else { + restrictedList.push(pageDesc); + } + logger.debug(`loaded page ${page}`); + }); module.exports = { - list: [ - ...publicList, - ...restrictedList, - ...publicRestrictedList - ], - publicList, - restrictedList, - publicRestrictedList + list: [...publicList, ...restrictedList, ...publicRestrictedList], + publicList, + restrictedList, + publicRestrictedList, }; diff --git a/backend/pages/occupant/index.js b/backend/pages/occupant/index.js index 7e1e01d..f8d5b9b 100644 --- a/backend/pages/occupant/index.js +++ b/backend/pages/occupant/index.js @@ -1,6 +1,6 @@ module.exports = () => { - return { - id: 'occupant', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'occupant', + restricted: true, + }; +}; diff --git a/backend/pages/occupant/model/index.js b/backend/pages/occupant/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/occupant/model/index.js +++ b/backend/pages/occupant/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/owner/index.js b/backend/pages/owner/index.js index 36fc582..3690d93 100644 --- a/backend/pages/owner/index.js +++ b/backend/pages/owner/index.js @@ -1,6 +1,6 @@ module.exports = () => { - return { - id: 'owner', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'owner', + restricted: true, + }; +}; diff --git a/backend/pages/owner/model/index.js b/backend/pages/owner/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/owner/model/index.js +++ b/backend/pages/owner/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/print/index.js b/backend/pages/print/index.js index e89ba81..8f9d2f2 100644 --- a/backend/pages/print/index.js +++ b/backend/pages/print/index.js @@ -1,8 +1,8 @@ module.exports = () => { - return { - id: 'print', - params: '/:id/occupants/:ids/:year?/:month?', - supportView: false, - restricted: true - }; -}; \ No newline at end of file + return { + id: 'print', + params: '/:id/occupants/:ids/:year?/:month?', + supportView: false, + restricted: true, + }; +}; diff --git a/backend/pages/print/model/index.js b/backend/pages/print/model/index.js index 9060294..c51a28e 100644 --- a/backend/pages/print/model/index.js +++ b/backend/pages/print/model/index.js @@ -1,21 +1,32 @@ const FD = require('../../../managers/frontdata'); const occupantModel = require('../../../models/occupant'); -module.exports = function(req, callback) { - const realm = req.realm; - const doc = req.params.id; - const month = req.params.month; - const fromMonth = req.params.fromMonth; - const year = req.params.year; - const occupantIds = req.params.ids ? req.params.ids.split(',') : []; +module.exports = function (req, callback) { + const realm = req.realm; + const doc = req.params.id; + const month = req.params.month; + const fromMonth = req.params.fromMonth; + const year = req.params.year; + const occupantIds = req.params.ids ? req.params.ids.split(',') : []; - occupantModel.findFilter(realm, {$query: {_id: {$in: occupantIds}}}, (errors, occupants) => { - if (errors && errors.length>0) { - callback(errors); - return; - } - const data = FD.toPrintData(realm, doc, fromMonth, month, year, occupants); - req.model = Object.assign(req.model, data); - callback(); - }); -}; \ No newline at end of file + occupantModel.findFilter( + realm, + { $query: { _id: { $in: occupantIds } } }, + (errors, occupants) => { + if (errors && errors.length > 0) { + callback(errors); + return; + } + const data = FD.toPrintData( + realm, + doc, + fromMonth, + month, + year, + occupants + ); + req.model = Object.assign(req.model, data); + callback(); + } + ); +}; diff --git a/backend/pages/profile/index.js b/backend/pages/profile/index.js index 30cca51..61ff597 100644 --- a/backend/pages/profile/index.js +++ b/backend/pages/profile/index.js @@ -1,6 +1,6 @@ module.exports = () => { - return { - id: 'profile', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'profile', + restricted: true, + }; +}; diff --git a/backend/pages/profile/model/index.js b/backend/pages/profile/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/profile/model/index.js +++ b/backend/pages/profile/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/property/index.js b/backend/pages/property/index.js index e9fabd4..9dffb8a 100644 --- a/backend/pages/property/index.js +++ b/backend/pages/property/index.js @@ -1,6 +1,6 @@ module.exports = () => { - return { - id: 'property', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'property', + restricted: true, + }; +}; diff --git a/backend/pages/property/model/index.js b/backend/pages/property/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/property/model/index.js +++ b/backend/pages/property/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/realm/index.js b/backend/pages/realm/index.js index dc93989..0d9aba8 100644 --- a/backend/pages/realm/index.js +++ b/backend/pages/realm/index.js @@ -1,6 +1,6 @@ module.exports = () => { - return { - id: 'realm', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'realm', + restricted: true, + }; +}; diff --git a/backend/pages/realm/model/index.js b/backend/pages/realm/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/realm/model/index.js +++ b/backend/pages/realm/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/rent/index.js b/backend/pages/rent/index.js index 2a32313..cb3c1d6 100644 --- a/backend/pages/rent/index.js +++ b/backend/pages/rent/index.js @@ -1,7 +1,7 @@ module.exports = () => { - return { - id: 'rent', - params: '/:year?/:month?', - restricted: true - }; -}; \ No newline at end of file + return { + id: 'rent', + params: '/:year?/:month?', + restricted: true, + }; +}; diff --git a/backend/pages/rent/model/index.js b/backend/pages/rent/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/rent/model/index.js +++ b/backend/pages/rent/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/signin/index.js b/backend/pages/signin/index.js index 76a55a2..feb3adb 100644 --- a/backend/pages/signin/index.js +++ b/backend/pages/signin/index.js @@ -1,10 +1,10 @@ const config = require('../../../config'); module.exports = () => { - if (!config.demoMode) { - return { - id: 'signin', - public: true - }; - } -}; \ No newline at end of file + if (!config.demoMode) { + return { + id: 'signin', + public: true, + }; + } +}; diff --git a/backend/pages/signin/model/index.js b/backend/pages/signin/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/signin/model/index.js +++ b/backend/pages/signin/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/signup/index.js b/backend/pages/signup/index.js index 2686b8b..7eda567 100644 --- a/backend/pages/signup/index.js +++ b/backend/pages/signup/index.js @@ -1,10 +1,10 @@ const config = require('../../../config'); module.exports = () => { - if (config.signup) { - return { - id:'signup', - public: true - }; - } -}; \ No newline at end of file + if (config.signup) { + return { + id: 'signup', + public: true, + }; + } +}; diff --git a/backend/pages/signup/model/index.js b/backend/pages/signup/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/signup/model/index.js +++ b/backend/pages/signup/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/pages/website/index.js b/backend/pages/website/index.js index ed2d2c4..b840542 100644 --- a/backend/pages/website/index.js +++ b/backend/pages/website/index.js @@ -1,7 +1,7 @@ module.exports = () => { - return { - id: 'website', - public: true, - restricted: true - }; -}; \ No newline at end of file + return { + id: 'website', + public: true, + restricted: true, + }; +}; diff --git a/backend/pages/website/model/index.js b/backend/pages/website/model/index.js index 2e3041f..38dc923 100644 --- a/backend/pages/website/model/index.js +++ b/backend/pages/website/model/index.js @@ -1,4 +1,4 @@ -module.exports = function(req, callback) { - req.model = Object.assign({}, req.model); - callback(); -}; \ No newline at end of file +module.exports = function (req, callback) { + req.model = Object.assign({}, req.model); + callback(); +}; diff --git a/backend/routes/api.js b/backend/routes/api.js index 41b08c7..9d818de 100755 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -11,64 +11,64 @@ const notificationManager = require('../managers/notificationmanager'); const accountingManager = require('../managers/accountingmanager'); const emailManager = require('../managers/emailmanager'); -module.exports = function() { - const router = express.Router(); +module.exports = function () { + const router = express.Router(); - router.use((req, res, next) => { - if (!req.session || !req.user) { - return res.sendStatus(401); - } - next(); - }); + router.use((req, res, next) => { + if (!req.session || !req.user) { + return res.sendStatus(401); + } + next(); + }); - const realmsRouter = express.Router(); - realmsRouter.get('/:id', loginManager.selectRealm); - router.use('/realms', realmsRouter); + const realmsRouter = express.Router(); + realmsRouter.get('/:id', loginManager.selectRealm); + router.use('/realms', realmsRouter); - const occupantsRouter = express.Router(); - occupantsRouter.post('/', occupantManager.add); - occupantsRouter.patch('/:id', occupantManager.update); - occupantsRouter.delete('/:ids', occupantManager.remove); - occupantsRouter.get('/', occupantManager.all); - occupantsRouter.get('/overview', occupantManager.overview); - router.use('/occupants', occupantsRouter); + const occupantsRouter = express.Router(); + occupantsRouter.post('/', occupantManager.add); + occupantsRouter.patch('/:id', occupantManager.update); + occupantsRouter.delete('/:ids', occupantManager.remove); + occupantsRouter.get('/', occupantManager.all); + occupantsRouter.get('/overview', occupantManager.overview); + router.use('/occupants', occupantsRouter); - const documentsRouter = express.Router(); - documentsRouter.patch('/:id', documentManager.update); - router.use('/documents', documentsRouter); + const documentsRouter = express.Router(); + documentsRouter.patch('/:id', documentManager.update); + router.use('/documents', documentsRouter); - const notificationsRouter = express.Router(); - notificationsRouter.get('/', notificationManager.all); - router.use('/notifications', notificationsRouter); + const notificationsRouter = express.Router(); + notificationsRouter.get('/', notificationManager.all); + router.use('/notifications', notificationsRouter); - const rentsRouter = express.Router(); - rentsRouter.patch('/:id', rentManager.update); - rentsRouter.get('/occupant/:id', rentManager.rentsOfOccupant); - rentsRouter.get('/:year/:month', rentManager.all); - rentsRouter.get('/overview', rentManager.overview); - rentsRouter.get('/overview/:year/:month', rentManager.overview); - router.use('/rents', rentsRouter); + const rentsRouter = express.Router(); + rentsRouter.patch('/:id', rentManager.update); + rentsRouter.get('/occupant/:id', rentManager.rentsOfOccupant); + rentsRouter.get('/:year/:month', rentManager.all); + rentsRouter.get('/overview', rentManager.overview); + rentsRouter.get('/overview/:year/:month', rentManager.overview); + router.use('/rents', rentsRouter); - const propertiesRouter = express.Router(); - propertiesRouter.post('/', propertyManager.add); - propertiesRouter.patch('/:id', propertyManager.update); - propertiesRouter.delete('/:ids', propertyManager.remove); - propertiesRouter.get('/', propertyManager.all); - propertiesRouter.get('/overview', propertyManager.overview); - router.use('/properties', propertiesRouter); + const propertiesRouter = express.Router(); + propertiesRouter.post('/', propertyManager.add); + propertiesRouter.patch('/:id', propertyManager.update); + propertiesRouter.delete('/:ids', propertyManager.remove); + propertiesRouter.get('/', propertyManager.all); + propertiesRouter.get('/overview', propertyManager.overview); + router.use('/properties', propertiesRouter); - router.get('/accounting/:year', accountingManager.all); + router.get('/accounting/:year', accountingManager.all); - const ownerRouter = express.Router(); - ownerRouter.get('/', ownerManager.all); - ownerRouter.patch('/:id', ownerManager.update); - router.use('/owner', ownerRouter); + const ownerRouter = express.Router(); + ownerRouter.get('/', ownerManager.all); + ownerRouter.patch('/:id', ownerManager.update); + router.use('/owner', ownerRouter); - const emailRouter = express.Router(); - emailRouter.post('/', emailManager.send); - router.use('/emails', emailRouter); + const emailRouter = express.Router(); + emailRouter.post('/', emailManager.send); + router.use('/emails', emailRouter); - const apiRouter = express.Router(); - apiRouter.use('/api', router); - return apiRouter; + const apiRouter = express.Router(); + apiRouter.use('/api', router); + return apiRouter; }; diff --git a/backend/routes/apiv2.js b/backend/routes/apiv2.js index 4c0f969..492d4bb 100755 --- a/backend/routes/apiv2.js +++ b/backend/routes/apiv2.js @@ -14,119 +14,119 @@ const notificationManager = require('../managers/notificationmanager'); const accountingManager = require('../managers/accountingmanager'); const emailManager = require('../managers/emailmanager'); -module.exports = function() { - const router = express.Router(); - - // protect the api access by checking the access token - router.use((req, res, next) => { - if (!req.headers.authorization) { - return res.sendStatus(403); - } - - const accessToken = req.headers.authorization.split(' ')[1]; - if (!accessToken) { - return res.sendStatus(403); - } - - try { - const decoded = jwt.verify(accessToken, config.ACCESS_TOKEN_SECRET); - req.user = decoded.account; - } catch (err) { - logger.warn(err); - return res.sendStatus(403); - } - - next(); - }); - - // update req with the user organizations - router.use((req, res, next) => { - if (req.path !== '/realms' && !req.headers.organizationid) { +module.exports = function () { + const router = express.Router(); + + // protect the api access by checking the access token + router.use((req, res, next) => { + if (!req.headers.authorization) { + return res.sendStatus(403); + } + + const accessToken = req.headers.authorization.split(' ')[1]; + if (!accessToken) { + return res.sendStatus(403); + } + + try { + const decoded = jwt.verify(accessToken, config.ACCESS_TOKEN_SECRET); + req.user = decoded.account; + } catch (err) { + logger.warn(err); + return res.sendStatus(403); + } + + next(); + }); + + // update req with the user organizations + router.use((req, res, next) => { + if (req.path !== '/realms' && !req.headers.organizationid) { + return res.sendStatus(404); + } + + realmModel.findByEmail(req.user.email, (err, realms = []) => { + if (err) { + return next(err); + } + if (realms.length) { + req.realms = realms; + const realmId = req.headers.organizationid; + if (realmId) { + req.realm = req.realms.find( + (realm) => realm._id.toString() === realmId + ); + if (req.path !== '/realms' && !req.realm) { return res.sendStatus(404); + } } - - realmModel.findByEmail(req.user.email, (err, realms = []) => { - if (err) { - return next(err); - } - if (realms.length) { - req.realms = realms; - const realmId = req.headers.organizationid; - if (realmId) { - req.realm = req.realms.find(realm => - realm._id.toString() === realmId - ); - if (req.path !== '/realms' && !req.realm) { - return res.sendStatus(404); - } - } - } else { - delete req.realm; - delete req.realms; - } - next(); - }); + } else { + delete req.realm; + delete req.realms; + } + next(); }); - - const realmsRouter = express.Router(); - realmsRouter.get('/', realmManager.all); - realmsRouter.get('/:id', realmManager.one); - realmsRouter.post('/', realmManager.add); - realmsRouter.patch('/:id', realmManager.update); - router.use('/realms', realmsRouter); - - const leasesRouter = express.Router(); - leasesRouter.get('/', leaseManager.all); - leasesRouter.get('/:id', leaseManager.one); - leasesRouter.post('/', leaseManager.add); - leasesRouter.patch('/:id', leaseManager.update); - leasesRouter.delete('/:ids', leaseManager.remove); - router.use('/leases', leasesRouter); - - const occupantsRouter = express.Router(); - occupantsRouter.get('/', occupantManager.all); - occupantsRouter.get('/:id', occupantManager.one); - occupantsRouter.post('/', occupantManager.add); - occupantsRouter.patch('/:id', occupantManager.update); - occupantsRouter.delete('/:ids', occupantManager.remove); - router.use('/tenants', occupantsRouter); - - const documentsRouter = express.Router(); - documentsRouter.get('/:document/:id/:term', documentManager.get); - documentsRouter.patch('/:id', documentManager.update); - router.use('/documents', documentsRouter); - - const notificationsRouter = express.Router(); - notificationsRouter.get('/', notificationManager.all); - router.use('/notifications', notificationsRouter); - - const rentsRouter = express.Router(); - rentsRouter.patch('/payment/:id/:term', rentManager.updateByTerm); - rentsRouter.get('/tenant/:id', rentManager.rentsOfOccupant); - rentsRouter.get('/tenant/:id/:term', rentManager.rentOfOccupantByTerm); - rentsRouter.get('/:year/:month', rentManager.all); - router.use('/rents', rentsRouter); - - const propertiesRouter = express.Router(); - propertiesRouter.get('/', propertyManager.all); - propertiesRouter.get('/:id', propertyManager.one); - propertiesRouter.post('/', propertyManager.add); - propertiesRouter.patch('/:id', propertyManager.update); - propertiesRouter.delete('/:ids', propertyManager.remove); - router.use('/properties', propertiesRouter); - - router.get('/accounting/:year', accountingManager.all); - - const ownerRouter = express.Router(); - ownerRouter.get('/', ownerManager.all); - ownerRouter.patch('/:id', ownerManager.update); - router.use('/owner', ownerRouter); - - const emailRouter = express.Router(); - emailRouter.post('/', emailManager.send); - router.use('/emails', emailRouter); - - const apiRouter = express.Router(); - apiRouter.use('/api/v2', router); - return apiRouter; + }); + + const realmsRouter = express.Router(); + realmsRouter.get('/', realmManager.all); + realmsRouter.get('/:id', realmManager.one); + realmsRouter.post('/', realmManager.add); + realmsRouter.patch('/:id', realmManager.update); + router.use('/realms', realmsRouter); + + const leasesRouter = express.Router(); + leasesRouter.get('/', leaseManager.all); + leasesRouter.get('/:id', leaseManager.one); + leasesRouter.post('/', leaseManager.add); + leasesRouter.patch('/:id', leaseManager.update); + leasesRouter.delete('/:ids', leaseManager.remove); + router.use('/leases', leasesRouter); + + const occupantsRouter = express.Router(); + occupantsRouter.get('/', occupantManager.all); + occupantsRouter.get('/:id', occupantManager.one); + occupantsRouter.post('/', occupantManager.add); + occupantsRouter.patch('/:id', occupantManager.update); + occupantsRouter.delete('/:ids', occupantManager.remove); + router.use('/tenants', occupantsRouter); + + const documentsRouter = express.Router(); + documentsRouter.get('/:document/:id/:term', documentManager.get); + documentsRouter.patch('/:id', documentManager.update); + router.use('/documents', documentsRouter); + + const notificationsRouter = express.Router(); + notificationsRouter.get('/', notificationManager.all); + router.use('/notifications', notificationsRouter); + + const rentsRouter = express.Router(); + rentsRouter.patch('/payment/:id/:term', rentManager.updateByTerm); + rentsRouter.get('/tenant/:id', rentManager.rentsOfOccupant); + rentsRouter.get('/tenant/:id/:term', rentManager.rentOfOccupantByTerm); + rentsRouter.get('/:year/:month', rentManager.all); + router.use('/rents', rentsRouter); + + const propertiesRouter = express.Router(); + propertiesRouter.get('/', propertyManager.all); + propertiesRouter.get('/:id', propertyManager.one); + propertiesRouter.post('/', propertyManager.add); + propertiesRouter.patch('/:id', propertyManager.update); + propertiesRouter.delete('/:ids', propertyManager.remove); + router.use('/properties', propertiesRouter); + + router.get('/accounting/:year', accountingManager.all); + + const ownerRouter = express.Router(); + ownerRouter.get('/', ownerManager.all); + ownerRouter.patch('/:id', ownerManager.update); + router.use('/owner', ownerRouter); + + const emailRouter = express.Router(); + emailRouter.post('/', emailManager.send); + router.use('/emails', emailRouter); + + const apiRouter = express.Router(); + apiRouter.use('/api/v2', router); + return apiRouter; }; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 3b5cc7a..fc5b15c 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -5,55 +5,59 @@ const logger = require('winston'); const config = require('../../config'); const loginManager = require('../managers/loginmanager'); -module.exports = function() { - //////////////////////////////////////////////////////////////////////////////// - // Set up passport - //////////////////////////////////////////////////////////////////////////////// - passport.use(new passportLocal.Strategy({ +module.exports = function () { + //////////////////////////////////////////////////////////////////////////////// + // Set up passport + //////////////////////////////////////////////////////////////////////////////// + passport.use( + new passportLocal.Strategy( + { usernameField: 'email', - passwordField: 'secretword' - }, - (email, password, done) => { + passwordField: 'secretword', + }, + (email, password, done) => { loginManager.authenticate(email, password, (err, user) => { - if (err) { - return done(null, false, {message: err}); - } - return done(null, user); + if (err) { + return done(null, false, { message: err }); + } + return done(null, user); }); - })); - - passport.serializeUser((user, done) => done(null, user.email)); - passport.deserializeUser(loginManager.getUserByEmail); - - //////////////////////////////////////////////////////////////////////////////// - // Session routes - //////////////////////////////////////////////////////////////////////////////// - const router = express.Router(); - router.use(loginManager.updateRequestWithUserFromRefreshToken); - router.use(loginManager.updateRequestWithRealmsOfUser); - - if (config.signup) { - router.post('/signup', loginManager.signup); - router.post('/signedup', (req, res) => { - res.redirect('/signin'); - }); - } + } + ) + ); + + passport.serializeUser((user, done) => done(null, user.email)); + passport.deserializeUser(loginManager.getUserByEmail); + + //////////////////////////////////////////////////////////////////////////////// + // Session routes + //////////////////////////////////////////////////////////////////////////////// + const router = express.Router(); + router.use(loginManager.updateRequestWithUserFromRefreshToken); + router.use(loginManager.updateRequestWithRealmsOfUser); + + if (config.signup) { + router.post('/signup', loginManager.signup); + router.post('/signedup', (req, res) => { + res.redirect('/signin'); + }); + } - if (config.demoMode) { - router.get('/signin', loginManager.loginDemo); - } else { - router.post('/signin', loginManager.login); - } + if (config.demoMode) { + router.get('/signin', loginManager.loginDemo); + } else { + router.post('/signin', loginManager.login); + } - router.all('/signedin', (req, res) => { - // TODO remove harcoded page dashboard - res.redirect('/dashboard'); - }); + router.all('/signedin', (req, res) => { + // TODO remove harcoded page dashboard + res.redirect('/dashboard'); + }); - router.get('/signout', (req, res) => { - logger.info('sign out and redirect to /'); - req.session.destroy(() => res.redirect('/')); - }); + router.get('/signout', (req, res) => { + logger.info('sign out and redirect to /'); + req.session.destroy(() => res.redirect('/')); + }); - return router; + return router; }; diff --git a/backend/routes/index.js b/backend/routes/index.js index fc7305e..7d36919 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -7,53 +7,55 @@ const page = require('./page'); const pages = require('../pages'); function _shouldBeLogged(req, res, next) { - if (!req.session || !req.user) { - return res.sendStatus(401); - } - next(); + if (!req.session || !req.user) { + return res.sendStatus(401); + } + next(); } function _shouldBeLoggedThenRedirect(req, res, next) { - if (!req.session || !req.user) { - logger.info('redirect to /signin'); - return res.redirect('/signin'); - } - next(); + if (!req.session || !req.user) { + logger.info('redirect to /signin'); + return res.redirect('/signin'); + } + next(); } function _shouldNotBeLoggedThenRedirect(req, res, next) { - if (req.session && req.user) { - // TODO remove harcoded page dashboard - logger.info('redirect to /dashboard'); - return res.redirect('/dashboard'); - } - next(); + if (req.session && req.user) { + // TODO remove harcoded page dashboard + logger.info('redirect to /dashboard'); + return res.redirect('/dashboard'); + } + next(); } module.exports = [ - // control route access - () => pages.restrictedList.reduce((router, pageDesc) => { - const path = `/${pageDesc.id}${pageDesc.params || ''}`; - router.use(path, _shouldBeLoggedThenRedirect); - if (pageDesc.supportView) { - router.use(`/view${path}`, _shouldBeLogged); - } - return router; + // control route access + () => + pages.restrictedList.reduce((router, pageDesc) => { + const path = `/${pageDesc.id}${pageDesc.params || ''}`; + router.use(path, _shouldBeLoggedThenRedirect); + if (pageDesc.supportView) { + router.use(`/view${path}`, _shouldBeLogged); + } + return router; }, express.Router()), - () => pages.publicList.reduce((router, pageDesc) => { - const path = `/${pageDesc.id}${pageDesc.params || ''}`; - router.use(path, _shouldNotBeLoggedThenRedirect); - if (pageDesc.supportView) { - router.use(`/view${path}`, _shouldNotBeLoggedThenRedirect); - } - return router; + () => + pages.publicList.reduce((router, pageDesc) => { + const path = `/${pageDesc.id}${pageDesc.params || ''}`; + router.use(path, _shouldNotBeLoggedThenRedirect); + if (pageDesc.supportView) { + router.use(`/view${path}`, _shouldNotBeLoggedThenRedirect); + } + return router; }, express.Router()), - () => express.Router().use('/signedin', _shouldBeLogged), - () => express.Router().use('/signedup', _shouldNotBeLoggedThenRedirect), - () => express.Router().use('/signout', _shouldBeLogged), - // add routes - auth, - apiV2, - apiV1, - page + () => express.Router().use('/signedin', _shouldBeLogged), + () => express.Router().use('/signedup', _shouldNotBeLoggedThenRedirect), + () => express.Router().use('/signout', _shouldBeLogged), + // add routes + auth, + apiV2, + apiV1, + page, ]; diff --git a/backend/routes/page.js b/backend/routes/page.js index 15a2fc7..7c22137 100644 --- a/backend/routes/page.js +++ b/backend/routes/page.js @@ -6,42 +6,44 @@ const config = require('../../config'); const pages = require('../pages'); function buildModel(pageId, req, callback) { - req.model = { - config, - view: pageId, - isLogged: req.user ? true : false, - isRealmSelected: req.realm ? true : false, - isDefaultRealmSelected: req.realm && req.realm.name === '__default_', - isMultipleRealmsAvailable: req.realms && req.realms.length > 1, - user: req.user, - realm: req.realm, - realms: req.realms, - errors: null - }; - const modelFn = require(path.join('..', 'pages', pageId, 'model')); - modelFn(req, callback); + req.model = { + config, + view: pageId, + isLogged: req.user ? true : false, + isRealmSelected: req.realm ? true : false, + isDefaultRealmSelected: req.realm && req.realm.name === '__default_', + isMultipleRealmsAvailable: req.realms && req.realms.length > 1, + user: req.user, + realm: req.realm, + realms: req.realms, + errors: null, + }; + const modelFn = require(path.join('..', 'pages', pageId, 'model')); + modelFn(req, callback); } -function renderPage(pageId, req, res, pageWithHeaders=true) { - const page = pageWithHeaders ? 'index' : `${pageId}/view/index`; - res.render(page, req.model); +function renderPage(pageId, req, res, pageWithHeaders = true) { + const page = pageWithHeaders ? 'index' : `${pageId}/view/index`; + res.render(page, req.model); } module.exports = function () { - const router = express.Router(); + const router = express.Router(); - pages.list.forEach(page => { - const params = page.params || ''; - const path = page.id === 'website' ? params || '/' : `/${page.id}${params}`; - router.get(path, (req, res) => { - buildModel(page.id, req, () => renderPage(page.id, req, res, page.supportView)); - }); - if (page.supportView) { - router.get(`/view/${page.id}${params}`, (req, res) => { - buildModel(page.id, req, () => renderPage(page.id, req, res, false)); - }); - } + pages.list.forEach((page) => { + const params = page.params || ''; + const path = page.id === 'website' ? params || '/' : `/${page.id}${params}`; + router.get(path, (req, res) => { + buildModel(page.id, req, () => + renderPage(page.id, req, res, page.supportView) + ); }); + if (page.supportView) { + router.get(`/view/${page.id}${params}`, (req, res) => { + buildModel(page.id, req, () => renderPage(page.id, req, res, false)); + }); + } + }); - return router; -}; \ No newline at end of file + return router; +}; diff --git a/backend/utils/crypto.js b/backend/utils/crypto.js index 42b8542..cf45d10 100644 --- a/backend/utils/crypto.js +++ b/backend/utils/crypto.js @@ -1,4 +1,3 @@ - const crypto = require('crypto'); const config = require('../../config'); @@ -10,23 +9,25 @@ const bufferedIV = Buffer.allocUnsafe(16); iv.copy(bufferedIV); function encrypt(text) { - const hashedKey = crypto.createHash('sha256').update(key).digest(); - const cipher = crypto.createCipheriv('aes-256-cbc', hashedKey, bufferedIV); - return [ - cipher.update(text, 'binary', 'hex'), - cipher.final('hex') - ].join(''); + const hashedKey = crypto.createHash('sha256').update(key).digest(); + const cipher = crypto.createCipheriv('aes-256-cbc', hashedKey, bufferedIV); + return [cipher.update(text, 'binary', 'hex'), cipher.final('hex')].join(''); } function decrypt(encryptedText) { - const hashedKey = crypto.createHash('sha256').update(key).digest(); - const decipher = crypto.createDecipheriv('aes-256-cbc', hashedKey, bufferedIV); - return [ - decipher.update(encryptedText, 'hex', 'binary'), - decipher.final('binary') - ].join(''); + const hashedKey = crypto.createHash('sha256').update(key).digest(); + const decipher = crypto.createDecipheriv( + 'aes-256-cbc', + hashedKey, + bufferedIV + ); + return [ + decipher.update(encryptedText, 'hex', 'binary'), + decipher.final('binary'), + ].join(''); } module.exports = { - encrypt, decrypt -}; \ No newline at end of file + encrypt, + decrypt, +}; diff --git a/config/index.js b/config/index.js index 568e4d1..749fd70 100644 --- a/config/index.js +++ b/config/index.js @@ -2,50 +2,64 @@ const path = require('path'); const sugar = require('sugar'); const toBoolean = (value) => { - if (value && typeof (value) !== 'boolean') { - value = value.toLowerCase() === 'true'; - } - return value; + if (value && typeof value !== 'boolean') { + value = value.toLowerCase() === 'true'; + } + return value; }; -const loggerLevel = process.env.LOCA_LOGGER_LEVEL || process.env.LOGGER_LEVEL || 'debug'; +const loggerLevel = + process.env.LOCA_LOGGER_LEVEL || process.env.LOGGER_LEVEL || 'debug'; const nginxPort = process.env.NGINX_PORT || 8080; const appHttpPort = process.env.LOCA_NODEJS_PORT || process.env.PORT || 8080; -const configDir = process.env.LOCA_CONFIG_DIR || process.env.CONFIG_DIR || path.join(__dirname, '..', 'config'); -const demoMode = toBoolean(process.env.LOCA_DEMOMODE || process.env.DEMO_MODE || true); +const configDir = + process.env.LOCA_CONFIG_DIR || + process.env.CONFIG_DIR || + path.join(__dirname, '..', 'config'); +const demoMode = toBoolean( + process.env.LOCA_DEMOMODE || process.env.DEMO_MODE || true +); const restoreDatabase = toBoolean(process.env.RESTORE_DB || true); -const signup = toBoolean(process.env.LOCA_PRODUCTIVE || process.env.SIGNUP || false); +const signup = toBoolean( + process.env.LOCA_PRODUCTIVE || process.env.SIGNUP || false +); const website = require(path.join(configDir, 'website.json')); const config = { - ...website, - loggerLevel, - nginxPort, - appHttpPort, - configDir, - businesslogic: 'FR', - productive: process.env.NODE_ENV === 'production', - signup, - restoreDatabase, - demoMode, - database: process.env.MONGO_URL || process.env.LOCA_DBNAME || process.env.BASE_DB_URL || 'mongodb://localhost/demodb', - EMAILER_URL: process.env.EMAILER_URL || 'http://localhost:8083/emailer', - PDFGENERATOR_URL: process.env.PDFGENERATOR_URL || 'http://localhost:8082/pdfgenerator', - ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET || 'access_token_secret', - REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET || 'refresh_token_secret', - CIPHER_KEY: process.env.CIPHER_KEY || 'cipher_key_secret', - CIPHER_IV_KEY: process.env.CIPHER_IV_KEY || 'cipher_iv_key_secret' + ...website, + loggerLevel, + nginxPort, + appHttpPort, + configDir, + businesslogic: 'FR', + productive: process.env.NODE_ENV === 'production', + signup, + restoreDatabase, + demoMode, + database: + process.env.MONGO_URL || + process.env.LOCA_DBNAME || + process.env.BASE_DB_URL || + 'mongodb://localhost/demodb', + EMAILER_URL: process.env.EMAILER_URL || 'http://localhost:8083/emailer', + PDFGENERATOR_URL: + process.env.PDFGENERATOR_URL || 'http://localhost:8082/pdfgenerator', + ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET || 'access_token_secret', + REFRESH_TOKEN_SECRET: + process.env.REFRESH_TOKEN_SECRET || 'refresh_token_secret', + CIPHER_KEY: process.env.CIPHER_KEY || 'cipher_key_secret', + CIPHER_IV_KEY: process.env.CIPHER_IV_KEY || 'cipher_iv_key_secret', }; module.exports = { - ...config, - log: () => { - const escapedConfig = sugar.Object.clone(config); - escapedConfig.ACCESS_TOKEN_SECRET = '****'; - escapedConfig.REFRESH_TOKEN_SECRET = '****'; - escapedConfig.CIPHER_KEY = '****'; - escapedConfig.CIPHER_IV_KEY = '****'; - return JSON.stringify(escapedConfig, null, 1); - } + ...config, + log: () => { + const escapedConfig = sugar.Object.clone(config); + escapedConfig.ACCESS_TOKEN_SECRET = '****'; + escapedConfig.REFRESH_TOKEN_SECRET = '****'; + escapedConfig.CIPHER_KEY = '****'; + escapedConfig.CIPHER_IV_KEY = '****'; + return JSON.stringify(escapedConfig, null, 1); + }, }; diff --git a/config/website.json b/config/website.json index c3d3133..9554a3f 100644 --- a/config/website.json +++ b/config/website.json @@ -1,48 +1,48 @@ { - "website": { - "company": { - "name": "", - "website": "" - }, - "product": { - "name": "Loca", - "slogan": "Real estate management", - "website": "https://demo.nuageprive.fr/", - "imageUrl": "http://demo.nuageprive.fr/public/images/1.jpg" - }, - "contact": { - "phone": "01.99.99.99.99", - "email": "camel.aissani@nuageprive.fr", - "address": { - "street1": "", - "street2": "", - "zipCode": "", - "city": "" - } - }, - "metatags": { - "type": "website", - "title": "Open source real estate management", - "description": "", - "keywords": "open source software, free software, real estate management, online management, rent management, invoices, rent notices", - "location": "Paris, France", - "rating": "General", - "author": { - "name": "Camel Aissani", - "twitter": "@camelaissani" - } - }, - "author": { - "name": "Camel Aissani", - "website": "http://www.nuageprive.fr", - "twitter": { - "url": "https://twitter.com/camelaissani", - "id": "@camelaissani" - }, - "github": { - "url": "https://github.com/camelaissani", - "id": "camelaissani" - } - } + "website": { + "company": { + "name": "", + "website": "" + }, + "product": { + "name": "Loca", + "slogan": "Real estate management", + "website": "https://demo.nuageprive.fr/", + "imageUrl": "http://demo.nuageprive.fr/public/images/1.jpg" + }, + "contact": { + "phone": "01.99.99.99.99", + "email": "camel.aissani@nuageprive.fr", + "address": { + "street1": "", + "street2": "", + "zipCode": "", + "city": "" + } + }, + "metatags": { + "type": "website", + "title": "Open source real estate management", + "description": "", + "keywords": "open source software, free software, real estate management, online management, rent management, invoices, rent notices", + "location": "Paris, France", + "rating": "General", + "author": { + "name": "Camel Aissani", + "twitter": "@camelaissani" + } + }, + "author": { + "name": "Camel Aissani", + "website": "http://www.nuageprive.fr", + "twitter": { + "url": "https://twitter.com/camelaissani", + "id": "@camelaissani" + }, + "github": { + "url": "https://github.com/camelaissani", + "id": "camelaissani" + } } + } } diff --git a/documentation/ABOUT.md b/documentation/ABOUT.md index f55ebfd..b3fea2b 100644 --- a/documentation/ABOUT.md +++ b/documentation/ABOUT.md @@ -1,19 +1,18 @@ -# About Loca - -Why pay hundreds of dollars for an overpriced, problematic and confusing rental property management software, when you can create a similar and simple app to your taste and preferences, -without any of the complex or complicated features… and best of all, absolutely free to use anytime, anywhere? - -This is the underlying philosophy behind the creation and development of Loca by its Lead Developer, Camel Aissani. -A rental property manager by profession who also enjoys Software Development and Coding as a part time hobby. - -From the initial plan of creating Loca to help himself and his fellow rental property manager cum friend in their businesses, Loca has evolved -into a user-friendly, durable and no-cost solution for rental property management. - -Loca is ideal for small, independent landlords and property managers to effectively manage their businesses, keep accurate and secure rental records, -significantly reduce the need for making frequent, costly phone calls to tenants and vendors and avoid most of the tiresome paperwork associated with managing a rental property. - -Our promise to you our esteemed clients is that Loca will be a welcome solution to your rental management needs, -not an additional problem. - - -Thank You For Using Loca Today. +# About Loca + +Why pay hundreds of dollars for an overpriced, problematic and confusing rental property management software, when you can create a similar and simple app to your taste and preferences, +without any of the complex or complicated features… and best of all, absolutely free to use anytime, anywhere? + +This is the underlying philosophy behind the creation and development of Loca by its Lead Developer, Camel Aissani. +A rental property manager by profession who also enjoys Software Development and Coding as a part time hobby. + +From the initial plan of creating Loca to help himself and his fellow rental property manager cum friend in their businesses, Loca has evolved +into a user-friendly, durable and no-cost solution for rental property management. + +Loca is ideal for small, independent landlords and property managers to effectively manage their businesses, keep accurate and secure rental records, +significantly reduce the need for making frequent, costly phone calls to tenants and vendors and avoid most of the tiresome paperwork associated with managing a rental property. + +Our promise to you our esteemed clients is that Loca will be a welcome solution to your rental management needs, +not an additional problem. + +Thank You For Using Loca Today. diff --git a/documentation/README.md b/documentation/README.md index 604e46c..bcd7075 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -1,36 +1,34 @@ - - -# Loca - -Loca is a free and open-source web-based application that offers a veritable and user-friendly platform for owners and -managers of buildings, flats, offices, meeting rooms, car parks, letter boxes...to make accurate financial and business decisions. - -Loca is designed to help small and independent rental property owners, managers and agents to effectively manage their properties, keep accurate, secured and tenancy and property info /documents, in all downloadable formats at all times. - -The idea is to make easy the management of properties and occupants with feature such as - -• Collect and keep all information of your properties and tenants securely in one place. - -• Create downloadable rental contracts and leasing agreements from templates available in the system. - -• Follow all rental payments month by month - -• Generate rental debt recovery letters from Templates for recovery unpaid rents by defaulting tenants… and so much more. - - ## How to contribute - -If the issue you report is not yet reported and related to the Loca core code, please report it on GitHub. -Provide as much details as needed and include screenshots if possible.. - -Fork the repository, edit and submit a pull request to branch that has latest version in development. -Please be very clear on your commit messages and pull request, empty pull request messages are not accepted. - -Important! - -Issues that are not related to the core code (such as a third party extension or your server configuration) might be closed -without explanation. You need to contact extension developer, use the forum or find a third partner to resolve a custom code issue. - -## Making a suggestion - -We like improvements, but improvements are not bugs or issue. Please do not create an issue report if you think something needs -improving (such as features or change to code standards etc). +# Loca + +Loca is a free and open-source web-based application that offers a veritable and user-friendly platform for owners and +managers of buildings, flats, offices, meeting rooms, car parks, letter boxes...to make accurate financial and business decisions. + +Loca is designed to help small and independent rental property owners, managers and agents to effectively manage their properties, keep accurate, secured and tenancy and property info /documents, in all downloadable formats at all times. + +The idea is to make easy the management of properties and occupants with feature such as + +• Collect and keep all information of your properties and tenants securely in one place. + +• Create downloadable rental contracts and leasing agreements from templates available in the system. + +• Follow all rental payments month by month + +• Generate rental debt recovery letters from Templates for recovery unpaid rents by defaulting tenants… and so much more. + +## How to contribute + +If the issue you report is not yet reported and related to the Loca core code, please report it on GitHub. +Provide as much details as needed and include screenshots if possible.. + +Fork the repository, edit and submit a pull request to branch that has latest version in development. +Please be very clear on your commit messages and pull request, empty pull request messages are not accepted. + +Important! + +Issues that are not related to the core code (such as a third party extension or your server configuration) might be closed +without explanation. You need to contact extension developer, use the forum or find a third partner to resolve a custom code issue. + +## Making a suggestion + +We like improvements, but improvements are not bugs or issue. Please do not create an issue report if you think something needs +improving (such as features or change to code standards etc). diff --git a/frontend/js/accounting/middleware.js b/frontend/js/accounting/middleware.js index d8da0c6..4aab154 100644 --- a/frontend/js/accounting/middleware.js +++ b/frontend/js/accounting/middleware.js @@ -6,99 +6,115 @@ import ViewController from '../viewcontroller'; const LOCA = application.get('LOCA'); class AccountingMiddleware extends ViewController { + constructor() { + super({ + domViewId: '#view-accounting', + defaultMenuId: 'accounting-menu', + }); + this.year = LOCA.currentYear; + } - constructor() { - super({ - domViewId: '#view-accounting', - defaultMenuId: 'accounting-menu' - }); - this.year = LOCA.currentYear; - } - - onInitListener() { - $(document).on('click', '#view-accounting .accounting-period', function() { - const $yearPicker = $('#view-accounting .js-year-picker'); - if ($yearPicker.is(':visible')) { - $yearPicker.hide(); - } else { - $yearPicker.show(); - } - return false; - }); - } + onInitListener() { + $(document).on('click', '#view-accounting .accounting-period', function () { + const $yearPicker = $('#view-accounting .js-year-picker'); + if ($yearPicker.is(':visible')) { + $yearPicker.hide(); + } else { + $yearPicker.show(); + } + return false; + }); + } - onInitTemplate() { - // Handlebars templates - Handlebars.registerPartial('accounting-payments-row-template', $('#accounting-payments-row-template').html()); + onInitTemplate() { + // Handlebars templates + Handlebars.registerPartial( + 'accounting-payments-row-template', + $('#accounting-payments-row-template').html() + ); - this.templatePaymentsTable = Handlebars.compile($('#accounting-payments-template').html()); - this.templateEntriesExitsTable = Handlebars.compile($('#accounting-entries-exits-template').html()); - } + this.templatePaymentsTable = Handlebars.compile( + $('#accounting-payments-template').html() + ); + this.templateEntriesExitsTable = Handlebars.compile( + $('#accounting-entries-exits-template').html() + ); + } - onDataChanged(callback) { - const $yearPicker = $('#view-accounting .js-year-picker'); - $yearPicker.datepicker({ - language: LOCA.countryCode, - autoclose: true, - format: ' yyyy', - startView: 'years', - minViewMode: 'years' - }); + onDataChanged(callback) { + const $yearPicker = $('#view-accounting .js-year-picker'); + $yearPicker.datepicker({ + language: LOCA.countryCode, + autoclose: true, + format: ' yyyy', + startView: 'years', + minViewMode: 'years', + }); - const that = this; - $yearPicker.on('changeDate', function() { - const selection = moment($(this).datepicker('getDate')); - that.year = selection.get('year'); - $yearPicker.hide(); - $('#view-accounting .accounting-period').html(that.year).show(); - that.load(); - }); + const that = this; + $yearPicker.on('changeDate', function () { + const selection = moment($(this).datepicker('getDate')); + that.year = selection.get('year'); + $yearPicker.hide(); + $('#view-accounting .accounting-period').html(that.year).show(); + that.load(); + }); - $yearPicker.datepicker('setDate', moment('01/01/'+this.year, 'DD/MM/YYYY').toDate()); - $('#view-accounting .accounting-period').html(this.year); - this.load(callback); - } + $yearPicker.datepicker( + 'setDate', + moment('01/01/' + this.year, 'DD/MM/YYYY').toDate() + ); + $('#view-accounting .accounting-period').html(this.year); + this.load(callback); + } - onUserAction($action, actionId) { - if (actionId==='invoice-link') { - const month = $action.data('month'); - const year = $action.data('year'); - const occupantId = $action.data('occupantId'); - let url = `/print/invoice/occupants/${occupantId}/${year}`; - if (month) { - url += `/${month}`; - } - application.openPrintPreview(url); - } + onUserAction($action, actionId) { + if (actionId === 'invoice-link') { + const month = $action.data('month'); + const year = $action.data('year'); + const occupantId = $action.data('occupantId'); + let url = `/print/invoice/occupants/${occupantId}/${year}`; + if (month) { + url += `/${month}`; + } + application.openPrintPreview(url); } + } - load(callback) { - application.httpGet( - `/api/accounting/${this.year}`, - (req, res) => { - const data = JSON.parse(res.responseText); - data.payments.months = moment.months(); - $('#view-accounting #accounting-payments-table').html(this.templatePaymentsTable(data.payments)); - $('#view-accounting #accounting-entries-exits-table').html(this.templateEntriesExitsTable(data.entriesExists)); - $('#view-accounting #accounting-payments-fake-table').width($('#view-accounting #accounting-payments-per-year-table').outerWidth()); + load(callback) { + application.httpGet(`/api/accounting/${this.year}`, (req, res) => { + const data = JSON.parse(res.responseText); + data.payments.months = moment.months(); + $('#view-accounting #accounting-payments-table').html( + this.templatePaymentsTable(data.payments) + ); + $('#view-accounting #accounting-entries-exits-table').html( + this.templateEntriesExitsTable(data.entriesExists) + ); + $('#view-accounting #accounting-payments-fake-table').width( + $('#view-accounting #accounting-payments-per-year-table').outerWidth() + ); - // bind top horizontal scrollbar with table - const $topHScroll = $('#view-accounting #accounting-payments-table-top-hscroll'); - const $viewScroll = $('#view-accounting #accounting-payments-per-year-table').parent(); - $topHScroll.off('.topHscroll'); - $viewScroll.off('.topHscroll'); - $topHScroll.on('scroll.topHScroll', () => { - $viewScroll.scrollLeft($topHScroll.scrollLeft()); - }); - $viewScroll.on('scroll.topHScroll', () => { - $topHScroll.scrollLeft($viewScroll.scrollLeft()); - }); - if (callback) { - callback(); - } - } - ); - } + // bind top horizontal scrollbar with table + const $topHScroll = $( + '#view-accounting #accounting-payments-table-top-hscroll' + ); + const $viewScroll = $( + '#view-accounting #accounting-payments-per-year-table' + ).parent(); + $topHScroll.off('.topHscroll'); + $viewScroll.off('.topHscroll'); + $topHScroll.on('scroll.topHScroll', () => { + $viewScroll.scrollLeft($topHScroll.scrollLeft()); + }); + $viewScroll.on('scroll.topHScroll', () => { + $topHScroll.scrollLeft($viewScroll.scrollLeft()); + }); + if (callback) { + callback(); + } + }); + } } export default AccountingMiddleware; diff --git a/frontend/js/application.js b/frontend/js/application.js index 874784c..9347417 100644 --- a/frontend/js/application.js +++ b/frontend/js/application.js @@ -6,53 +6,67 @@ const application = frontexpress(); const now = new Date(); application.set('LOCA', { - currentMonth: now.getMonth() + 1, - currentYear: now.getFullYear(), - countryCode: 'en-US' + currentMonth: now.getMonth() + 1, + currentYear: now.getFullYear(), + countryCode: 'en-US', }); const httpPostPatchTransformer = { - data({data}) { - if (!data) { - return data; - } + data({ data }) { + if (!data) { + return data; + } - return $.param(data); - }, - headers({headers, data}) { - if (!data) { - return headers; - } - const updatedHeaders = headers || {}; - if (!updatedHeaders['Content-Type']) { - updatedHeaders['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; - } - return updatedHeaders; + return $.param(data); + }, + headers({ headers, data }) { + if (!data) { + return headers; } + const updatedHeaders = headers || {}; + if (!updatedHeaders['Content-Type']) { + updatedHeaders['Content-Type'] = + 'application/x-www-form-urlencoded; charset=UTF-8'; + } + return updatedHeaders; + }, }; application.set('http POST transformer', httpPostPatchTransformer); -application.set('http PATCH transformer',httpPostPatchTransformer); +application.set('http PATCH transformer', httpPostPatchTransformer); application.openPrintPreview = (url) => { - window.open(url, '_blank', 'location=no,menubar=yes,status=no,titlebar=yes,toolbar=yes,scrollbars=yes,resizable=yes,width=1000,height=700'); + window.open( + url, + '_blank', + 'location=no,menubar=yes,status=no,titlebar=yes,toolbar=yes,scrollbars=yes,resizable=yes,width=1000,height=700' + ); }; -application.sendEmail = (tenantIds, document, year, month, callback=()=>{}) => { - if (!tenantIds) { - callback(); +application.sendEmail = ( + tenantIds, + document, + year, + month, + callback = () => {} +) => { + if (!tenantIds) { + callback(); + } + application.httpPost( + { + uri: '/api/emails', + data: { + document, + tenantIds, + year, + month, + }, + }, + (req, res) => { + callback(JSON.parse(res.responseText)); } - application.httpPost({ - uri: '/api/emails', - data: { - document, - tenantIds, - year, - month - } - }, (req, res) => { - callback(JSON.parse(res.responseText)); - }); + ); }; export default application; diff --git a/frontend/js/baseview_middleware.js b/frontend/js/baseview_middleware.js index febaae1..4119916 100644 --- a/frontend/js/baseview_middleware.js +++ b/frontend/js/baseview_middleware.js @@ -2,11 +2,11 @@ import $ from 'jquery'; import frontexpress from 'frontexpress'; class BaseViewMiddleware extends frontexpress.Middleware { - entered() { - // hide footer and background image - $('body').removeClass('covered-body'); - $('body > .footer').hide(); - } + entered() { + // hide footer and background image + $('body').removeClass('covered-body'); + $('body > .footer').hide(); + } } -export default BaseViewMiddleware; \ No newline at end of file +export default BaseViewMiddleware; diff --git a/frontend/js/connection_middleware.js b/frontend/js/connection_middleware.js index 8c9b5a4..7282d42 100644 --- a/frontend/js/connection_middleware.js +++ b/frontend/js/connection_middleware.js @@ -3,54 +3,62 @@ import frontexpress from 'frontexpress'; import bootbox from 'bootbox'; class ConnectionMiddleware extends frontexpress.Middleware { - entered() { - $('#waitwindow').show(); - } + entered() { + $('#waitwindow').show(); + } - updated() { - $('#waitwindow').hide(); - } + updated() { + $('#waitwindow').hide(); + } - exited() { - $('#waitwindow').hide(); - } + exited() { + $('#waitwindow').hide(); + } - failed(request, response) { - const errors = []; - let needAuthentication = false; - if (response.status === 0) { - errors.push(i18next.t('Server access problem. Check your network connection')); - } else if (response.status == 401) { - needAuthentication = true; - errors.push(i18next.t('Your session has expired, Please reconnect')); - errors.push(i18next.t('[code: ]', {code: 401})); - } else if (response.status == 404) { - errors.push(i18next.t('Page not found on server')); - errors.push(i18next.t('[code: ]', {code: 404})); - } else if (response.status == 500) { - errors.push(i18next.t('Internal server error')); - errors.push(i18next.t('[code: ]', {code: 500})); - } else if (response.errorThrown === 'parsererror') { - errors.push(i18next.t('Problem during data decoding [JSON]')); - } else if (response.errorThrown === 'timeout') { - errors.push(i18next.t('Server is taking too long to reply')); - } else if (response.errorThrown === 'abort') { - errors.push(i18next.t('Request cancelled on server')); - } else { - errors.push(i18next.t('Unknown error')); - errors.push(response.statusText); - } + failed(request, response) { + const errors = []; + let needAuthentication = false; + if (response.status === 0) { + errors.push( + i18next.t('Server access problem. Check your network connection') + ); + } else if (response.status == 401) { + needAuthentication = true; + errors.push(i18next.t('Your session has expired, Please reconnect')); + errors.push(i18next.t('[code: ]', { code: 401 })); + } else if (response.status == 404) { + errors.push(i18next.t('Page not found on server')); + errors.push(i18next.t('[code: ]', { code: 404 })); + } else if (response.status == 500) { + errors.push(i18next.t('Internal server error')); + errors.push(i18next.t('[code: ]', { code: 500 })); + } else if (response.errorThrown === 'parsererror') { + errors.push(i18next.t('Problem during data decoding [JSON]')); + } else if (response.errorThrown === 'timeout') { + errors.push(i18next.t('Server is taking too long to reply')); + } else if (response.errorThrown === 'abort') { + errors.push(i18next.t('Request cancelled on server')); + } else { + errors.push(i18next.t('Unknown error')); + errors.push(response.statusText); + } - bootbox.hideAll(); - $('#waitwindow').hide(); - bootbox.alert({title: i18next.t('Uh-oh!'), message: errors}); + bootbox.hideAll(); + $('#waitwindow').hide(); + bootbox.alert({ title: i18next.t('Uh-oh!'), message: errors }); - if (needAuthentication) { - setTimeout(function(){ - window.location.replace(location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: '')+'/signin'); - }, 2000); - } + if (needAuthentication) { + setTimeout(function () { + window.location.replace( + location.protocol + + '//' + + location.hostname + + (location.port ? ':' + location.port : '') + + '/signin' + ); + }, 2000); } + } } -export default ConnectionMiddleware; \ No newline at end of file +export default ConnectionMiddleware; diff --git a/frontend/js/dashboard/middleware.js b/frontend/js/dashboard/middleware.js index cd8ed3a..322e591 100755 --- a/frontend/js/dashboard/middleware.js +++ b/frontend/js/dashboard/middleware.js @@ -7,146 +7,165 @@ import application from '../application'; import Helper from '../lib/helper'; class DashboardMiddleware extends BaseViewMiddleware { + // overriden + exited() { + super.exited(); + $('#view-dashboard .carousel').carousel('pause'); + } - // overriden - exited() { - super.exited(); - $('#view-dashboard .carousel').carousel('pause'); + // overriden + updated() { + super.updated(); + if (!this.notificationListTemplate) { + this.notificationListTemplate = Handlebars.compile( + $('#notification-list-template').html() + ); } - // overriden - updated() { - super.updated(); - if (!this.notificationListTemplate) { - this.notificationListTemplate = Handlebars.compile($('#notification-list-template').html()); - } + const currentMoment = moment(); + $('#view-dashboard #current-day').html(currentMoment.format('Do')); + $('#view-dashboard .current-month').html(currentMoment.format('MMMM YYYY')); - const currentMoment = moment(); - $('#view-dashboard #current-day').html(currentMoment.format('Do')); - $('#view-dashboard .current-month').html(currentMoment.format('MMMM YYYY')); - - this._loadRentsOverview(() => { - this._loadOccupantsOverview(() => { - this._loadPropertiesOverview(() => { - this._loadNotifications(() => { - $('#view-dashboard .carousel').each(function(index) { - const $carousel = $(this); - if (index % 2) { - setTimeout(() => { - $carousel.carousel({ - interval: 8000 - }); - }, 2500 * index); - } else { - $carousel.carousel({ - interval: 8000 - }); - } - }); - }); + this._loadRentsOverview(() => { + this._loadOccupantsOverview(() => { + this._loadPropertiesOverview(() => { + this._loadNotifications(() => { + $('#view-dashboard .carousel').each(function (index) { + const $carousel = $(this); + if (index % 2) { + setTimeout(() => { + $carousel.carousel({ + interval: 8000, + }); + }, 2500 * index); + } else { + $carousel.carousel({ + interval: 8000, }); + } }); + }); }); - } + }); + }); + } - _loadOccupantsOverview(callback) { - application.httpGet( - '/api/occupants/overview', - (req, res) => { - const occupantsOverview = JSON.parse(res.responseText); - const countAll = occupantsOverview.countAll; - const countActive = occupantsOverview.countActive; - const countInactive = occupantsOverview.countInactive; - $('#view-dashboard #count-all-occupants').html(countAll); - $('#view-dashboard #count-active-occupants').html(countActive); - $('#view-dashboard #count-inactive-occupants').html(countInactive); - $('#view-dashboard #count-all-occupants-label').html(i18next.t('Tenant', {count: countAll})); - $('#view-dashboard #count-active-occupants-label').html(i18next.t('Lease', {count: countActive})); - $('#view-dashboard #count-inactive-occupants-label').html(i18next.t('Terminated lease', {count: countActive})); - if (callback) { - callback(); - } - } - ); - } + _loadOccupantsOverview(callback) { + application.httpGet('/api/occupants/overview', (req, res) => { + const occupantsOverview = JSON.parse(res.responseText); + const countAll = occupantsOverview.countAll; + const countActive = occupantsOverview.countActive; + const countInactive = occupantsOverview.countInactive; + $('#view-dashboard #count-all-occupants').html(countAll); + $('#view-dashboard #count-active-occupants').html(countActive); + $('#view-dashboard #count-inactive-occupants').html(countInactive); + $('#view-dashboard #count-all-occupants-label').html( + i18next.t('Tenant', { count: countAll }) + ); + $('#view-dashboard #count-active-occupants-label').html( + i18next.t('Lease', { count: countActive }) + ); + $('#view-dashboard #count-inactive-occupants-label').html( + i18next.t('Terminated lease', { count: countActive }) + ); + if (callback) { + callback(); + } + }); + } - _loadPropertiesOverview(callback) { - application.httpGet( - '/api/properties/overview', - (req, res) => { - const propertiesOverview = JSON.parse(res.responseText); - const countAll = propertiesOverview.countAll; - const countFree = propertiesOverview.countFree; - const countBusy = propertiesOverview.countBusy; - $('#view-dashboard #count-all-properties').html(countAll); - $('#view-dashboard #count-active-properties').html(countBusy); - $('#view-dashboard #count-inactive-properties').html(countFree); - $('#view-dashboard #count-all-properties-label').html(i18next.t('Property', {count: countAll})); - $('#view-dashboard #count-active-properties-label').html(i18next.t('Leased', {count: countBusy})); - $('#view-dashboard #count-inactive-properties-label').html(i18next.t('Available', {count: countFree})); - if (callback) { - callback(); - } - } - ); - } + _loadPropertiesOverview(callback) { + application.httpGet('/api/properties/overview', (req, res) => { + const propertiesOverview = JSON.parse(res.responseText); + const countAll = propertiesOverview.countAll; + const countFree = propertiesOverview.countFree; + const countBusy = propertiesOverview.countBusy; + $('#view-dashboard #count-all-properties').html(countAll); + $('#view-dashboard #count-active-properties').html(countBusy); + $('#view-dashboard #count-inactive-properties').html(countFree); + $('#view-dashboard #count-all-properties-label').html( + i18next.t('Property', { count: countAll }) + ); + $('#view-dashboard #count-active-properties-label').html( + i18next.t('Leased', { count: countBusy }) + ); + $('#view-dashboard #count-inactive-properties-label').html( + i18next.t('Available', { count: countFree }) + ); + if (callback) { + callback(); + } + }); + } - _loadRentsOverview(callback) { - application.httpGet( - '/api/rents/overview', - (req, res) => { - const rentsOverview = JSON.parse(res.responseText); - const countAll = rentsOverview.countAll; - const countPaid = rentsOverview.countPaid; - const countPartiallyPaid = rentsOverview.countPartiallyPaid; - const countNotPaid = rentsOverview.countNotPaid; - const countPaidAndPartiallyPaid = countPaid + countPartiallyPaid; - const totalToPay = rentsOverview.totalToPay; - const totalNotPaid = rentsOverview.totalNotPaid; - const totalPaid = rentsOverview.totalPaid; - $('#view-dashboard #count-all-rents').html(countAll); - $('#view-dashboard #count-all-rents-label').html(i18next.t('Rent', {count: countAll})); - $('#view-dashboard #count-paid-rents').html(countPaidAndPartiallyPaid + ' ' + i18next.t('Rent', {count: countPaidAndPartiallyPaid})); - $('#view-dashboard #count-not-paid-rents').html(countNotPaid + ' ' + i18next.t('Rent', {count: countNotPaid})); - //$('#view-dashboard #count-partially-paid-rents').html(countPartiallyPaid); - $('#view-dashboard #count-js-total-topay-rents').html(Helper.formatMoney(totalToPay)); - $('#view-dashboard #count-total-notpaid-rents').html(Helper.formatMoney(totalNotPaid * (-1))); - $('#view-dashboard #count-total-paid-rents').html(Helper.formatMoney(totalPaid)); - if (callback) { - callback(); - } - } - ); - } + _loadRentsOverview(callback) { + application.httpGet('/api/rents/overview', (req, res) => { + const rentsOverview = JSON.parse(res.responseText); + const countAll = rentsOverview.countAll; + const countPaid = rentsOverview.countPaid; + const countPartiallyPaid = rentsOverview.countPartiallyPaid; + const countNotPaid = rentsOverview.countNotPaid; + const countPaidAndPartiallyPaid = countPaid + countPartiallyPaid; + const totalToPay = rentsOverview.totalToPay; + const totalNotPaid = rentsOverview.totalNotPaid; + const totalPaid = rentsOverview.totalPaid; + $('#view-dashboard #count-all-rents').html(countAll); + $('#view-dashboard #count-all-rents-label').html( + i18next.t('Rent', { count: countAll }) + ); + $('#view-dashboard #count-paid-rents').html( + countPaidAndPartiallyPaid + + ' ' + + i18next.t('Rent', { count: countPaidAndPartiallyPaid }) + ); + $('#view-dashboard #count-not-paid-rents').html( + countNotPaid + ' ' + i18next.t('Rent', { count: countNotPaid }) + ); + //$('#view-dashboard #count-partially-paid-rents').html(countPartiallyPaid); + $('#view-dashboard #count-js-total-topay-rents').html( + Helper.formatMoney(totalToPay) + ); + $('#view-dashboard #count-total-notpaid-rents').html( + Helper.formatMoney(totalNotPaid * -1) + ); + $('#view-dashboard #count-total-paid-rents').html( + Helper.formatMoney(totalPaid) + ); + if (callback) { + callback(); + } + }); + } - _loadNotifications(callback) { - application.httpGet( - '/api/notifications', - (req, res) => { - const notifications = JSON.parse(res.responseText); - let notificationsToDisplay; - const emptyNotifications = [{ - type: 'ok', - description: 'Rien à signaler' - }]; + _loadNotifications(callback) { + application.httpGet('/api/notifications', (req, res) => { + const notifications = JSON.parse(res.responseText); + let notificationsToDisplay; + const emptyNotifications = [ + { + type: 'ok', + description: 'Rien à signaler', + }, + ]; - if (notifications && notifications.length > 0) { - notificationsToDisplay = notifications.filter((notification) => { - return notification.expired; - }); - } - if (!notificationsToDisplay || notificationsToDisplay.length === 0) { - notificationsToDisplay = emptyNotifications; - } - $('#view-dashboard #carousel-notifications').html(this.notificationListTemplate({ - notifications: notificationsToDisplay - })); - if (callback) { - callback(); - } - } - ); - } + if (notifications && notifications.length > 0) { + notificationsToDisplay = notifications.filter((notification) => { + return notification.expired; + }); + } + if (!notificationsToDisplay || notificationsToDisplay.length === 0) { + notificationsToDisplay = emptyNotifications; + } + $('#view-dashboard #carousel-notifications').html( + this.notificationListTemplate({ + notifications: notificationsToDisplay, + }) + ); + if (callback) { + callback(); + } + }); + } } export default DashboardMiddleware; diff --git a/frontend/js/form.js b/frontend/js/form.js index 5e4e2dc..9e158a0 100644 --- a/frontend/js/form.js +++ b/frontend/js/form.js @@ -5,270 +5,290 @@ import ObjectFilter from './lib/objectfilter'; import application from './application'; class Form { - constructor(options = {}) { - const defaultOptions = { - domSelector: '', - httpMethod: null, - uri: null, - defaultData: {}, - manifest: {}, - alertOnFieldError: true - }; + constructor(options = {}) { + const defaultOptions = { + domSelector: '', + httpMethod: null, + uri: null, + defaultData: {}, + manifest: {}, + alertOnFieldError: true, + }; - this.options = { - ...defaultOptions, - ...options - }; - this.datepickersChangeHandler = () => { - $(`${this.options.domSelector} .datepicker`).datepicker('update'); - }; - } + this.options = { + ...defaultOptions, + ...options, + }; + this.datepickersChangeHandler = () => { + $(`${this.options.domSelector} .datepicker`).datepicker('update'); + }; + } + + beforeSetData() {} - beforeSetData() { } + afterSetData() {} - afterSetData() { } + onGetData(data) { + return data; + } - onGetData(data) { - return data; + onBind() {} + + onSubmit(response, callback) { + if (callback) { + callback(response); } + } - onBind() { } + // METHODS THAT MAKE THE JOB + showErrorMessage(message) { + this.$alertMsg.html(message); + this.$alert.show(); + } - onSubmit(response, callback) { - if (callback) { - callback(response); + setData(formData) { + const updateFormWithData = (data, keyPostfix) => { + if (!keyPostfix) { + keyPostfix = ''; + } + Sugar.Object.forEach(data, (value, key) => { + // var keyToFilter; + //var values; + if (!Array.isArray(value)) { + if (value === null) { + $(this.options.domSelector + ' #' + key + keyPostfix).val(''); + } else { + $(this.options.domSelector + ' #' + key + keyPostfix).val( + String(value) + ); + } + } else { + if (keyPostfix) { + throw new Error( + 'Two levels of attributes are not supported. Attribute to fix is ' + + key + ); + } + //values = value; + value.forEach((v, i) => { + // keyToFilter = this.options.defaultData[key]; + // if (keyToFilter && keyToFilter.length>0) { + // value = Object.select(values[i], Object.keys(keyToFilter[0])); + // } + // else { + // value = v; + // } + updateFormWithData(v, keyPostfix + '_' + i); + }); } + }); + }; + try { + this.validator.resetForm(); + } catch (e) { + console.error(e); } + this.$form[0].reset(); + this.$alert.hide(); + this.$form.find('.has-error').removeClass('has-error text-danger'); + this.$form.find('.js-form-row:not(.js-master-form-row)').remove(); + + this.beforeSetData(arguments); - // METHODS THAT MAKE THE JOB - showErrorMessage(message) { - this.$alertMsg.html(message); - this.$alert.show(); + if (!formData) { + formData = this.options.defaultData; } + const filteredData = ObjectFilter.filter( + this.options.defaultData, + formData + ); + updateFormWithData(filteredData); - setData(formData) { - const updateFormWithData = (data, keyPostfix) => { - if (!keyPostfix) { - keyPostfix = ''; - } - Sugar.Object.forEach(data, (value, key) => { - // var keyToFilter; - //var values; - if (!Array.isArray(value)) { - if (value === null) { - $(this.options.domSelector + ' #' + key + keyPostfix).val(''); - } - else { - $(this.options.domSelector + ' #' + key + keyPostfix).val(String(value)); - } - } - else { - if (keyPostfix) { - throw new Error('Two levels of attributes are not supported. Attribute to fix is ' + key); - } - //values = value; - value.forEach((v, i) => { - // keyToFilter = this.options.defaultData[key]; - // if (keyToFilter && keyToFilter.length>0) { - // value = Object.select(values[i], Object.keys(keyToFilter[0])); - // } - // else { - // value = v; - // } - updateFormWithData(v, keyPostfix + '_' + i); - }); - } - }); - }; - try { - this.validator.resetForm(); - } catch (e) { } - this.$form[0].reset(); - this.$alert.hide(); - this.$form.find('.has-error').removeClass('has-error text-danger'); - this.$form.find('.js-form-row:not(.js-master-form-row)').remove(); + $(`${this.options.domSelector} .datepicker`).datepicker('update'); - this.beforeSetData(arguments); + this.afterSetData(arguments); + } - if (!formData) { - formData = this.options.defaultData; - } - const filteredData = ObjectFilter.filter(this.options.defaultData, formData); - updateFormWithData(filteredData); + getData() { + var data = {}; + var values; + var key, value; - $(`${this.options.domSelector} .datepicker`).datepicker('update'); + this.$form.find('.js-form-rows').each(function () { + var $formRows = $(this); + var id = $formRows.attr('id'); + var $rows = $formRows.find('.js-form-row'); - this.afterSetData(arguments); + data[id] = []; + $rows.each(function () { + var $row = $(this); + var $elements = $row.find('input, select, textarea'); + var dataRow = {}; + $elements.each(function () { + var $element = $(this); + var keyRow = $element.attr('id').replace(/_\d+$/, ''); + dataRow[keyRow] = $element.val(); + }); + data[id].push(dataRow); + }); + }); + values = this.$form.serializeArray(); + for (var i = 0; i < values.length; ++i) { + if (values[i].name.match(/_\d+$/)) { + continue; + } + key = values[i].name; + value = values[i].value; + if (key in data) { + throw new Error( + 'key "' + key + '" already set to value >>' + data.key + '<<' + ); + } else { + data[key] = value || ''; + } } + return this.onGetData(data); + } - getData() { - var data = {}; - var values; - var key, value; + // validate() { + // //this.validator.resetForm(); + // this.$alert.hide(); + // return this.$form.valid(); + // } - this.$form.find('.js-form-rows').each(function () { - var $formRows = $(this); - var id = $formRows.attr('id'); - var $rows = $formRows.find('.js-form-row'); + submit(callback) { + var self = this; + if (this.$form.valid()) { + const data = self.getData(); - data[id] = []; - $rows.each(function () { - var $row = $(this); - var $elements = $row.find('input, select, textarea'); - var dataRow = {}; - $elements.each(function () { - var $element = $(this); - var keyRow = $element.attr('id').replace(/_\d+$/, ''); - dataRow[keyRow] = $element.val(); - }); - data[id].push(dataRow); - }); - }); - values = this.$form.serializeArray(); - for (var i = 0; i < values.length; ++i) { - if (values[i].name.match(/_\d+$/)) { - continue; - } - key = values[i].name; - value = values[i].value; - if (key in data) { - throw new Error('key "' + key + '" already set to value >>' + data.key + '<<'); + if (self.options.httpMethod === 'POST' || !data._id || !data._id.trim()) { + // if no _id add a new resource rather than updating it + delete data._id; + application.httpPost( + { uri: self.options.uri, data }, + (req, res) => { + const response = JSON.parse(res.responseText); + if (response.errors && response.errors.length > 0) { + self.showErrorMessage(response.errors.join('
')); + return; } - else { - data[key] = value || ''; + self.onSubmit(response, callback); + }, + (/*req, res*/) => { + self.showErrorMessage( + i18next.t("A technical issue has occurred (-_-')") + ); + } + ); + } else { + // complete the patch url with the id of the resource + const uri = `${self.options.uri}/${data._id}`; + application.httpPatch( + { uri, data }, + (req, res) => { + const response = JSON.parse(res.responseText); + if (response.errors && response.errors.length > 0) { + self.showErrorMessage(response.errors.join('
')); + return; } - } - return this.onGetData(data); + self.onSubmit(response, callback); + }, + (/*req, res*/) => { + self.showErrorMessage( + i18next.t("A technical issue has occurred (-_-')") + ); + } + ); + } } + } - // validate() { - // //this.validator.resetForm(); - // this.$alert.hide(); - // return this.$form.valid(); - // } - - submit(callback) { - var self = this; - if (this.$form.valid()) { - const data = self.getData(); + getPropertyFilters() { + return Sugar.Object.keys(this.options.defaultData); + } - if (self.options.httpMethod === 'POST' || !data._id || !data._id.trim()) { - // if no _id add a new resource rather than updating it - delete data._id; - application.httpPost( - { uri: self.options.uri, data }, - (req, res) => { - const response = JSON.parse(res.responseText); - if (response.errors && response.errors.length > 0) { - self.showErrorMessage(response.errors.join('
')); - return; - } - self.onSubmit(response, callback); - }, - (/*req, res*/) => { - self.showErrorMessage(i18next.t('A technical issue has occurred (-_-\')')); - } - ); - } else { - // complete the patch url with the id of the resource - const uri = `${self.options.uri}/${data._id}`; - application.httpPatch( - { uri, data }, - (req, res) => { - const response = JSON.parse(res.responseText); - if (response.errors && response.errors.length > 0) { - self.showErrorMessage(response.errors.join('
')); - return; - } - self.onSubmit(response, callback); - }, - (/*req, res*/) => { - self.showErrorMessage(i18next.t('A technical issue has occurred (-_-\')')); - } - ); - } - } + unbindForm() { + if (this.$form) { + $(`${this.options.domSelector} .datepicker`).datepicker('destroy'); + this.$form.off('.validate').removeData('validator'); } + } - getPropertyFilters() { - return Sugar.Object.keys(this.options.defaultData); - } + bindForm() { + const self = this; - unbindForm() { - if (this.$form) { - $(`${this.options.domSelector} .datepicker`).datepicker('destroy'); - this.$form.off('.validate').removeData('validator'); - } - } + this.onBind(arguments); - bindForm() { - const self = this; + this.$form = $(this.options.domSelector); + if (this.$form.length === 0) { + return this.$form; + } - this.onBind(arguments); + this.$alert = $(this.options.domSelector + ' .form-error').hide(); + this.$alertMsg = this.$alert.find('.js-form-error-message'); - this.$form = $(this.options.domSelector); - if (this.$form.length === 0) { - return this.$form; + this.validator = this.$form.validate({ + debug: true, + ignore: 'hidden', + rules: this.options.manifest, + highlight: function (element /*, errorClass, validClass*/) { + $(element).closest('.form-group').addClass('has-error text-danger'); + }, + success: function (element) { + $(element).closest('.form-group').removeClass('has-error text-danger'); + $(element).closest('label.error').remove(); + }, + showErrors: function (/*errorMap, errorList*/) { + var errorCount; + if (self.options.alertOnFieldError) { + errorCount = this.numberOfInvalids(); + if (errorCount) { + self.$alertMsg.html( + i18next.t( + 'The form is not valid. Please check the field with error', + { count: errorCount } + ) + ); + self.$alert.show(); + } } + this.defaultShowErrors(); + $(self.options.domSelector + ' label.error').addClass('control-label'); + }, + errorPlacement: function (error, element) { + error.appendTo($(element).closest('.form-group')); + }, + submitHandler: function (form) { + if (self.$form.attr('action')) { + form.submit(); + } + }, + }); - this.$alert = $(this.options.domSelector + ' .form-error').hide(); - this.$alertMsg = this.$alert.find('.js-form-error-message'); - - this.validator = this.$form.validate({ - debug: true, - ignore: 'hidden', - rules: this.options.manifest, - highlight: function (element/*, errorClass, validClass*/) { - $(element).closest('.form-group').addClass('has-error text-danger'); - }, - success: function (element) { - $(element).closest('.form-group').removeClass('has-error text-danger'); - $(element).closest('label.error').remove(); - }, - showErrors: function (/*errorMap, errorList*/) { - var errorCount; - if (self.options.alertOnFieldError) { - errorCount = this.numberOfInvalids(); - if (errorCount) { - self.$alertMsg.html(i18next.t('The form is not valid. Please check the field with error', { count: errorCount })); - self.$alert.show(); - } - } - this.defaultShowErrors(); - $(self.options.domSelector + ' label.error').addClass('control-label'); - }, - errorPlacement: function (error, element) { - error.appendTo($(element).closest('.form-group')); - }, - submitHandler: function (form) { - if (self.$form.attr('action')) { - form.submit(); - } - } - }); - - this.formHasBeenUpdated(); - return this.$form; - } + this.formHasBeenUpdated(); + return this.$form; + } - formHasBeenUpdated() { - $(`${this.options.domSelector} input, ${this.options.domSelector} select, ${this.options.domSelector} textarea`).each(function () { - const $input = $(this); - const id = $input.attr('id'); - if (!$input.attr('name')) { - $input.attr('name', id); - } - }); + formHasBeenUpdated() { + $( + `${this.options.domSelector} input, ${this.options.domSelector} select, ${this.options.domSelector} textarea` + ).each(function () { + const $input = $(this); + const id = $input.attr('id'); + if (!$input.attr('name')) { + $input.attr('name', id); + } + }); - const $datepickers = $(`${this.options.domSelector} .datepicker`); - $datepickers.off('change', this.datepickersChangeHandler); - $datepickers.datepicker('destroy'); + const $datepickers = $(`${this.options.domSelector} .datepicker`); + $datepickers.off('change', this.datepickersChangeHandler); + $datepickers.datepicker('destroy'); - $datepickers.datepicker({ - enableOnReadonly: false - }); - $datepickers.change(this.datepickersChangeHandler); - } + $datepickers.datepicker({ + enableOnReadonly: false, + }); + $datepickers.change(this.datepickersChangeHandler); + } } export default Form; diff --git a/frontend/js/formvalidators.js b/frontend/js/formvalidators.js index 62f6f2d..fb9cb34 100644 --- a/frontend/js/formvalidators.js +++ b/frontend/js/formvalidators.js @@ -3,83 +3,140 @@ import moment from 'moment'; import i18next from 'i18next'; export default () => { - $.validator.addMethod('mindate', function(value, element, params) { - let minDate; - let momentMin; - let momentValue; + $.validator.addMethod( + 'mindate', + function (value, element, params) { + let minDate; + let momentMin; + let momentValue; - minDate = params[0].domSelector?$(params[0].domSelector).val():params[0].minDate; - if (typeof minDate === 'function') { - minDate = minDate(); - } - if (moment.isMoment(minDate)) { - momentMin = minDate; - } else if (moment.isDate(minDate)) { - momentMin = moment(minDate); - } else { - momentMin = moment(minDate, 'L', true); - } + minDate = params[0].domSelector + ? $(params[0].domSelector).val() + : params[0].minDate; + if (typeof minDate === 'function') { + minDate = minDate(); + } + if (moment.isMoment(minDate)) { + momentMin = minDate; + } else if (moment.isDate(minDate)) { + momentMin = moment(minDate); + } else { + momentMin = moment(minDate, 'L', true); + } - momentValue = moment(value, 'L', true); + momentValue = moment(value, 'L', true); - params[1] = params[0].message?i18next.t(params[0].message):i18next.t('Please set a date after the', {date: momentMin.format('L')}); - return this.optional(element) || (momentValue.isValid() && momentMin.isValid() && (momentValue.isSame(momentMin) || momentValue.isAfter(momentMin))); - }, '{1}'); + params[1] = params[0].message + ? i18next.t(params[0].message) + : i18next.t('Please set a date after the', { + date: momentMin.format('L'), + }); + return ( + this.optional(element) || + (momentValue.isValid() && + momentMin.isValid() && + (momentValue.isSame(momentMin) || momentValue.isAfter(momentMin))) + ); + }, + '{1}' + ); - $.validator.addMethod('maxdate', function(value, element, params) { - let maxDate; - let momentMax; - let momentValue; + $.validator.addMethod( + 'maxdate', + function (value, element, params) { + let maxDate; + let momentMax; + let momentValue; - maxDate = params[0].domSelector?$(params[0].domSelector).val():params[0].maxDate; - if (typeof maxDate === 'function') { - maxDate = maxDate(); - } - if (moment.isMoment(maxDate)) { - momentMax = maxDate; - } else if (moment.isDate(maxDate)) { - momentMax = moment(maxDate); - } else { - momentMax = moment(maxDate, 'L', true); - } + maxDate = params[0].domSelector + ? $(params[0].domSelector).val() + : params[0].maxDate; + if (typeof maxDate === 'function') { + maxDate = maxDate(); + } + if (moment.isMoment(maxDate)) { + momentMax = maxDate; + } else if (moment.isDate(maxDate)) { + momentMax = moment(maxDate); + } else { + momentMax = moment(maxDate, 'L', true); + } - momentValue = moment(value, 'L', true); + momentValue = moment(value, 'L', true); - params[1] = params[0].message?i18next.t(params[0].message):i18next.t('Please set a date before the', {date: momentMax.format('L')}); - return this.optional(element) || (momentValue.isValid() && momentMax.isValid() && (momentValue.isSame(momentMax) || momentValue.isBefore(momentMax))); - }, '{1}'); + params[1] = params[0].message + ? i18next.t(params[0].message) + : i18next.t('Please set a date before the', { + date: momentMax.format('L'), + }); + return ( + this.optional(element) || + (momentValue.isValid() && + momentMax.isValid() && + (momentValue.isSame(momentMax) || momentValue.isBefore(momentMax))) + ); + }, + '{1}' + ); - $.validator.addMethod('maxcontractdate', function(value, element, params) { - var contract = $(params[0] + ' #' + params[1]).val(); - var beginDate = $(params[0] + ' #' + params[2]).val(); - var endDate; - var momentBegin, momentEnd; - var contractDuration; - var momentValue = moment(value, 'L', true); + $.validator.addMethod( + 'maxcontractdate', + function (value, element, params) { + var contract = $(params[0] + ' #' + params[1]).val(); + var beginDate = $(params[0] + ' #' + params[2]).val(); + var endDate; + var momentBegin, momentEnd; + var contractDuration; + var momentValue = moment(value, 'L', true); - momentBegin = moment(beginDate, 'L', true); - if (momentBegin.isValid()) { - if (contract === 'custom') { - contractDuration = moment.duration(2, 'years'); - momentEnd = moment(momentBegin).add(contractDuration).subtract(1, 'days'); - return momentValue.isValid() && momentValue.isAfter(momentBegin) && (momentValue.isSame(momentEnd) || momentValue.isBefore(momentEnd)); - } - contractDuration = moment.duration(9, 'years'); - momentEnd = moment(momentBegin).add(contractDuration).subtract(1, 'days'); - endDate = momentEnd.format('L'); - return endDate === value; + momentBegin = moment(beginDate, 'L', true); + if (momentBegin.isValid()) { + if (contract === 'custom') { + contractDuration = moment.duration(2, 'years'); + momentEnd = moment(momentBegin) + .add(contractDuration) + .subtract(1, 'days'); + return ( + momentValue.isValid() && + momentValue.isAfter(momentBegin) && + (momentValue.isSame(momentEnd) || momentValue.isBefore(momentEnd)) + ); } - return this.optional(element); - }, i18next.t('The end date of contract is not compatible with contract selected')); + contractDuration = moment.duration(9, 'years'); + momentEnd = moment(momentBegin) + .add(contractDuration) + .subtract(1, 'days'); + endDate = momentEnd.format('L'); + return endDate === value; + } + return this.optional(element); + }, + i18next.t( + 'The end date of contract is not compatible with contract selected' + ) + ); - $.validator.methods.date = function(value, element) { - return this.optional(element) || moment(value, 'L', true).isValid(); - }; + $.validator.methods.date = function (value, element) { + return this.optional(element) || moment(value, 'L', true).isValid(); + }; - $.validator.messages.date = i18next.t('The date is not valid (Sample date:)', {date: moment().format('L')}); + $.validator.messages.date = i18next.t( + 'The date is not valid (Sample date:)', + { date: moment().format('L') } + ); - $.validator.addMethod('phoneFR', function(phone_number, element) { - phone_number = phone_number.replace(/\(|\)|\s+|-/g, ''); - return this.optional(element) || phone_number.length > 9 && phone_number.match(/^(?:(?:(?:00\s?|\+)33\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/); - }, i18next.t('Please enter a valid phone number')); + $.validator.addMethod( + 'phoneFR', + function (phone_number, element) { + phone_number = phone_number.replace(/\(|\)|\s+|-/g, ''); + return ( + this.optional(element) || + (phone_number.length > 9 && + phone_number.match( + /^(?:(?:(?:00\s?|\+)33\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/ + )) + ); + }, + i18next.t('Please enter a valid phone number') + ); }; diff --git a/frontend/js/index.js b/frontend/js/index.js index 5e50cc2..d15e997 100644 --- a/frontend/js/index.js +++ b/frontend/js/index.js @@ -30,40 +30,40 @@ application.post(connectionMiddleware); application.get(new MenuMiddleware()); application.get(/^\/view\//, new ViewMiddleware()); [ - {id: 'website', Middleware: WebsiteMiddleware}, - {id: 'signup', Middleware: SignupMiddleware}, - {id: 'signin', Middleware: LoginMiddleware}, - {id: 'dashboard', Middleware: DashboardMiddleware}, - {id: 'rent', Middleware: RentMiddleware}, - {id: 'occupant', Middleware: OccupantMiddleware}, - {id: 'property', Middleware: PropertyMiddleware}, - {id: 'accounting', Middleware: AccountingMiddleware}, - {id: 'selectrealm', Middleware: SelectRealmMiddleware}, - {id: 'owner', Middleware: OwnerMiddleware}, -].forEach(page => { - const middleware = new page.Middleware(); - application.get(`/${page.id !== 'website' ? page.id : ''}`, middleware); - application.get(`/view/${page.id}`, middleware); + { id: 'website', Middleware: WebsiteMiddleware }, + { id: 'signup', Middleware: SignupMiddleware }, + { id: 'signin', Middleware: LoginMiddleware }, + { id: 'dashboard', Middleware: DashboardMiddleware }, + { id: 'rent', Middleware: RentMiddleware }, + { id: 'occupant', Middleware: OccupantMiddleware }, + { id: 'property', Middleware: PropertyMiddleware }, + { id: 'accounting', Middleware: AccountingMiddleware }, + { id: 'selectrealm', Middleware: SelectRealmMiddleware }, + { id: 'owner', Middleware: OwnerMiddleware }, +].forEach((page) => { + const middleware = new page.Middleware(); + application.get(`/${page.id !== 'website' ? page.id : ''}`, middleware); + application.get(`/view/${page.id}`, middleware); }); /////////////////////////////////////////////////////////////////////////////// // launch application /////////////////////////////////////////////////////////////////////////////// language(LOCA.countryCode, (countryCode) => { - LOCA.countryCode = countryCode; - application.listen(); + LOCA.countryCode = countryCode; + application.listen(); - // init form validators - FV(); + // init form validators + FV(); - // init menu - menu(); + // init menu + menu(); - // display reset data message - const $demoPopover = $('#demo-popover'); - if ($demoPopover.length) { - bootbox.alert({ - message: i18next.t('Site data is reset every 30 minutes') - }); - } + // display reset data message + const $demoPopover = $('#demo-popover'); + if ($demoPopover.length) { + bootbox.alert({ + message: i18next.t('Site data is reset every 30 minutes'), + }); + } }); diff --git a/frontend/js/language.js b/frontend/js/language.js index 2cc6ff6..202b44e 100644 --- a/frontend/js/language.js +++ b/frontend/js/language.js @@ -4,94 +4,107 @@ import moment from 'moment'; import i18next from 'i18next'; const LangsForJQueryValidate = { - 'pt': 'pt_PT' + pt: 'pt_PT', }; async function updateLanguageScript(id, src) { - let fileref = document.getElementById(id); + let fileref = document.getElementById(id); - if (fileref) { - document.getElementsByTagName('head')[0].removeChild(fileref); - } + if (fileref) { + document.getElementsByTagName('head')[0].removeChild(fileref); + } - await new Promise((resolve, reject) => { - try { - fileref = document.createElement('script'); - fileref.id = id; - fileref.type = 'text/javascript'; - fileref.async = true; - fileref.onload = res => { - if (res.type === 'error') { - reject(`an error has occurred when loading the localization file ${src}`); - } else { - resolve(res); - } - }; - fileref.onerror = () => { - reject(`an error has occurred when loading the localization file ${src}`); - }; - fileref.src = src; - document.getElementsByTagName('head')[0].appendChild(fileref); - } catch (error) { - reject(error); + await new Promise((resolve, reject) => { + try { + fileref = document.createElement('script'); + fileref.id = id; + fileref.type = 'text/javascript'; + fileref.async = true; + fileref.onload = (res) => { + if (res.type === 'error') { + reject( + `an error has occurred when loading the localization file ${src}` + ); + } else { + resolve(res); } - }); + }; + fileref.onerror = () => { + reject( + `an error has occurred when loading the localization file ${src}` + ); + }; + fileref.src = src; + document.getElementsByTagName('head')[0].appendChild(fileref); + } catch (error) { + reject(error); + } + }); } export default (defaultCountryCode, callback) => { - document.addEventListener('DOMContentLoaded', () => { - // Init locale - i18next - .use(window.i18nextBrowserLanguageDetector) - .use(window.i18nextLocalStorageCache) - .use(window.i18nextXHRBackend) - .use(window.i18nextSprintfPostProcessor) - .init({ - fallbackLng: 'en', - debug: false, - pluralSeparator: '_', - keySeparator: '::', - nsSeparator: ':::', - detection: { - order: [ /*'querystring', 'localStorage',*/ 'cookie', 'navigator'], - //lookupQuerystring: 'lng', - lookupCookie: 'locaI18next', - cookieDomain: 'loca', - // lookupLocalStorage: 'i18nextLng', - caches: [ /*'localStorage', */ 'cookie'] - }, - // cache: { - // enabled: false, - // prefix: 'i18next_res_', - // expirationTime: 7 * 24 * 60 * 60 * 1000 - // }, - backend: { - loadPath: '/public/locales/{{lng}}.json', - allowMultiLoading: false - } - }); + document.addEventListener('DOMContentLoaded', () => { + // Init locale + i18next + .use(window.i18nextBrowserLanguageDetector) + .use(window.i18nextLocalStorageCache) + .use(window.i18nextXHRBackend) + .use(window.i18nextSprintfPostProcessor) + .init({ + fallbackLng: 'en', + debug: false, + pluralSeparator: '_', + keySeparator: '::', + nsSeparator: ':::', + detection: { + order: [/*'querystring', 'localStorage',*/ 'cookie', 'navigator'], + //lookupQuerystring: 'lng', + lookupCookie: 'locaI18next', + cookieDomain: 'loca', + // lookupLocalStorage: 'i18nextLng', + caches: [/*'localStorage', */ 'cookie'], + }, + // cache: { + // enabled: false, + // prefix: 'i18next_res_', + // expirationTime: 7 * 24 * 60 * 60 * 1000 + // }, + backend: { + loadPath: '/public/locales/{{lng}}.json', + allowMultiLoading: false, + }, + }); - i18next.on('languageChanged', function(countryCode = defaultCountryCode) { - const splittedCountryCode = countryCode.split('-'); - const lang = splittedCountryCode[0].toLowerCase(); - const langForJQueryValidate = LangsForJQueryValidate[lang] || lang; - try { - Promise.all([ - updateLanguageScript('moment-language', `//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/locale/${lang}.js`), - updateLanguageScript('jquery-validate-language', `//ajax.aspnetcdn.com/ajax/jquery.validate/1.13.1/localization/messages_${langForJQueryValidate}.js`), - updateLanguageScript('bootstrap-datepicker-language', `/node_modules/bootstrap-datepicker/dist/locales/bootstrap-datepicker.${lang}.min.js`) - ]); - } catch (error) { - console.error(error); - } + i18next.on('languageChanged', function (countryCode = defaultCountryCode) { + const splittedCountryCode = countryCode.split('-'); + const lang = splittedCountryCode[0].toLowerCase(); + const langForJQueryValidate = LangsForJQueryValidate[lang] || lang; + try { + Promise.all([ + updateLanguageScript( + 'moment-language', + `//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/locale/${lang}.js` + ), + updateLanguageScript( + 'jquery-validate-language', + `//ajax.aspnetcdn.com/ajax/jquery.validate/1.13.1/localization/messages_${langForJQueryValidate}.js` + ), + updateLanguageScript( + 'bootstrap-datepicker-language', + `/node_modules/bootstrap-datepicker/dist/locales/bootstrap-datepicker.${lang}.min.js` + ), + ]); + } catch (error) { + console.error(error); + } - moment.locale(lang); - const dateFormat = moment.localeData().longDateFormat('L').toLowerCase(); - $.fn.datepicker.defaults.language = countryCode; - $.fn.datepicker.defaults.format = dateFormat; - if (callback) { - callback(lang); - } - }); + moment.locale(lang); + const dateFormat = moment.localeData().longDateFormat('L').toLowerCase(); + $.fn.datepicker.defaults.language = countryCode; + $.fn.datepicker.defaults.format = dateFormat; + if (callback) { + callback(lang); + } }); + }); }; diff --git a/frontend/js/lib/anilayout.js b/frontend/js/lib/anilayout.js index cbc0cde..282f92b 100755 --- a/frontend/js/lib/anilayout.js +++ b/frontend/js/lib/anilayout.js @@ -3,66 +3,74 @@ import $ from 'jquery'; const TRANSITION_DURATION_MENU = 200; class Anilayout { - isMenuVisible(dataId) { - dataId = dataId.startsWith('#')?dataId.slice(1, dataId.length):dataId; - return $('.js-side-menu[data-id="' + dataId + '"]').hasClass('active'); - } + isMenuVisible(dataId) { + dataId = dataId.startsWith('#') ? dataId.slice(1, dataId.length) : dataId; + return $('.js-side-menu[data-id="' + dataId + '"]').hasClass('active'); + } - showMenu(dataId, callback) { - var $cardToSelect; + showMenu(dataId, callback) { + var $cardToSelect; - function callbackEx() { - if (callback) { - callback(); - } - } + function callbackEx() { + if (callback) { + callback(); + } + } - dataId = dataId.startsWith('#')?dataId.slice(1, dataId.length):dataId; - $cardToSelect = $('.js-side-menu[data-id="' + dataId + '"]:hidden'); + dataId = dataId.startsWith('#') ? dataId.slice(1, dataId.length) : dataId; + $cardToSelect = $('.js-side-menu[data-id="' + dataId + '"]:hidden'); - if ($cardToSelect.length > 0) { - this.hideMenu(function () { - $cardToSelect.trigger('before-show-card'); - $cardToSelect.addClass('active').velocity('transition.bounceRightIn', {duration: TRANSITION_DURATION_MENU, complete: function () { - callbackEx(); - $cardToSelect.trigger('after-show-card'); - }}); - }); - } else { + if ($cardToSelect.length > 0) { + this.hideMenu(function () { + $cardToSelect.trigger('before-show-card'); + $cardToSelect.addClass('active').velocity('transition.bounceRightIn', { + duration: TRANSITION_DURATION_MENU, + complete: function () { callbackEx(); - } + $cardToSelect.trigger('after-show-card'); + }, + }); + }); + } else { + callbackEx(); } + } - hideMenu(callback) { - var $activeCard = $('.js-side-menu.active').not(':hidden'); - - function callbackEx() { - if (callback) { - callback(); - } - } + hideMenu(callback) { + var $activeCard = $('.js-side-menu.active').not(':hidden'); - if ($activeCard.length > 0) { - $activeCard.trigger('before-hide-card'); - $activeCard.removeClass('active'); - $activeCard.velocity('transition.bounceRightOut', {duration: TRANSITION_DURATION_MENU, complete: function() { - callbackEx(); - $activeCard.trigger('after-hide-card'); - }}); - } else { - callbackEx(); - } + function callbackEx() { + if (callback) { + callback(); + } } - showSheet(dataId) { - dataId = dataId.startsWith('#')?dataId.slice(1, dataId.length):dataId; - this.hideSheet(); - $('.js-sheet[data-id="'+dataId+'"]').addClass('active').show(); + if ($activeCard.length > 0) { + $activeCard.trigger('before-hide-card'); + $activeCard.removeClass('active'); + $activeCard.velocity('transition.bounceRightOut', { + duration: TRANSITION_DURATION_MENU, + complete: function () { + callbackEx(); + $activeCard.trigger('after-hide-card'); + }, + }); + } else { + callbackEx(); } + } - hideSheet() { - $('.js-sheet.active').removeClass('active').hide(); - } + showSheet(dataId) { + dataId = dataId.startsWith('#') ? dataId.slice(1, dataId.length) : dataId; + this.hideSheet(); + $('.js-sheet[data-id="' + dataId + '"]') + .addClass('active') + .show(); + } + + hideSheet() { + $('.js-sheet.active').removeClass('active').hide(); + } } export default new Anilayout(); diff --git a/frontend/js/lib/anilist.js b/frontend/js/lib/anilist.js index 5fdd6bf..c024eb2 100755 --- a/frontend/js/lib/anilist.js +++ b/frontend/js/lib/anilist.js @@ -5,465 +5,487 @@ import Helper from './helper'; const EVENT_TYPE_SELECTION_CHANGED = 'list.selection.changed'; -class Anilist{ - // ----------------------------------------------------------------------- - // PRIVATE ATTRIBUTES - // ----------------------------------------------------------------------- - // var TRANSITION_ROW_DURATION = 100; - // var TRANSITION_ROW_STAGGER_DURATION = 50; - // var TRANSITION_MAXROW = 10; - - // ----------------------------------------------------------------------- - // CONSTRUCTOR - // ----------------------------------------------------------------------- - constructor(listId, rowTemplateId, contentTemplateId) { - var self = this; - - // Use minivents - Events(this); - - // attributes - this.listId = listId.startsWith('#')?listId.slice(1, listId.length):listId; - this.rowTemplateId = (rowTemplateId && rowTemplateId.startsWith('#'))?rowTemplateId.slice(1, rowTemplateId.length):rowTemplateId; - this.contentTemplateId = (contentTemplateId && contentTemplateId.startsWith('#'))?contentTemplateId.slice(1, contentTemplateId.length):contentTemplateId; - this.filterText = ''; - - // row management - $(document).on('click', '#' + this.listId + ' .js-list-row', function() { - self.select($(this)); - return false; - }); +class Anilist { + // ----------------------------------------------------------------------- + // PRIVATE ATTRIBUTES + // ----------------------------------------------------------------------- + // var TRANSITION_ROW_DURATION = 100; + // var TRANSITION_ROW_STAGGER_DURATION = 50; + // var TRANSITION_MAXROW = 10; + + // ----------------------------------------------------------------------- + // CONSTRUCTOR + // ----------------------------------------------------------------------- + constructor(listId, rowTemplateId, contentTemplateId) { + var self = this; + + // Use minivents + Events(this); + + // attributes + this.listId = listId.startsWith('#') + ? listId.slice(1, listId.length) + : listId; + this.rowTemplateId = + rowTemplateId && rowTemplateId.startsWith('#') + ? rowTemplateId.slice(1, rowTemplateId.length) + : rowTemplateId; + this.contentTemplateId = + contentTemplateId && contentTemplateId.startsWith('#') + ? contentTemplateId.slice(1, contentTemplateId.length) + : contentTemplateId; + this.filterText = ''; + + // row management + $(document).on('click', '#' + this.listId + ' .js-list-row', function () { + self.select($(this)); + return false; + }); + } + + // ----------------------------------------------------------------------- + // PRIVATE METHODS + // ----------------------------------------------------------------------- + _cloneObject(obj) { + var copy; + + // Handle the 3 simple types, and null or undefined + if (null === obj || 'object' !== typeof obj) { + return obj; } - // ----------------------------------------------------------------------- - // PRIVATE METHODS - // ----------------------------------------------------------------------- - _cloneObject(obj) { - var copy; - - // Handle the 3 simple types, and null or undefined - if (null === obj || 'object' !== typeof obj) { - return obj; - } - - // Handle Date - if (obj instanceof Date) { - copy = new Date(); - copy.setTime(obj.getTime()); - return copy; - } - - // Handle Array - if (obj instanceof Array) { - copy = []; - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = this._cloneObject(obj[i]); - } - return copy; - } - - // Handle Object - if (obj instanceof Object) { - copy = {}; - for (var attr in obj) { - if (Object.prototype.hasOwnProperty.call(obj, attr)) { - copy[attr] = this._cloneObject(obj[attr]); - } - } - return copy; - } - - throw new Error('Unable to copy obj! Its type isn\'t supported.'); + // Handle Date + if (obj instanceof Date) { + copy = new Date(); + copy.setTime(obj.getTime()); + return copy; } - _ref(obj, str) { - str = str.split('.'); - for (var i = 0; i < str.length; i++) { - obj = obj[str[i]]; - } - return obj; + // Handle Array + if (obj instanceof Array) { + copy = []; + for (var i = 0, len = obj.length; i < len; i++) { + copy[i] = this._cloneObject(obj[i]); + } + return copy; } - _set(obj, str, val) { - str = str.split('.'); - while (str.length > 1) { - obj = obj[str.shift()]; + // Handle Object + if (obj instanceof Object) { + copy = {}; + for (var attr in obj) { + if (Object.prototype.hasOwnProperty.call(obj, attr)) { + copy[attr] = this._cloneObject(obj[attr]); } - obj[str.shift()] = val; + } + return copy; } - _animateValue($element, start, end, duration) { - var nbLoop = 20; - var range = end - start; - var current = start; - var stepTime = duration / nbLoop; - var increment = range / nbLoop; - var timer; + throw new Error("Unable to copy obj! Its type isn't supported."); + } - if (range === 0) { - return; - } - timer = setInterval(function() { - current += increment; - if ((range>0 && current >= end) || - (range<0 && current <= end)) { - // TODO: manage number format - $element.html(Helper.formatMoney(end)); - clearInterval(timer); - } - else { - // TODO: manage number format - $element.html(Helper.formatMoney(current)); - } - }, stepTime); + _ref(obj, str) { + str = str.split('.'); + for (var i = 0; i < str.length; i++) { + obj = obj[str[i]]; } + return obj; + } - // ----------------------------------------------------------------------- - // PUBLIC METHODS - // ----------------------------------------------------------------------- - bindDom() { - this.$list = $('#'+this.listId); - this.$rowsContainer = this.$list.find('.js-list-content'); - - // Handlebars templates - var rowTemplate = $('#' + this.rowTemplateId); - if (rowTemplate.length >0) { - this.sourceTemplateRow = rowTemplate.html(); - Handlebars.registerPartial(this.rowTemplateId, this.sourceTemplateRow); - this.templateRowsContainer = Handlebars.compile($('#' + this.contentTemplateId).html()); - } + _set(obj, str, val) { + str = str.split('.'); + while (str.length > 1) { + obj = obj[str.shift()]; } - - // ----------------------------------------------------------------------- - // ROW MANAGEMENT - // ----------------------------------------------------------------------- - unselectAll() { - this.$list.find('.js-list-row').removeClass('active'); - this.emit(EVENT_TYPE_SELECTION_CHANGED); + obj[str.shift()] = val; + } + + _animateValue($element, start, end, duration) { + var nbLoop = 20; + var range = end - start; + var current = start; + var stepTime = duration / nbLoop; + var increment = range / nbLoop; + var timer; + + if (range === 0) { + return; } - - unselect(id) { - var selection; - $('#'+id).removeClass('active'); - selection = this.getSelectedData(); - this.emit(EVENT_TYPE_SELECTION_CHANGED, selection.length>0?selection:null); + timer = setInterval(function () { + current += increment; + if ((range > 0 && current >= end) || (range < 0 && current <= end)) { + // TODO: manage number format + $element.html(Helper.formatMoney(end)); + clearInterval(timer); + } else { + // TODO: manage number format + $element.html(Helper.formatMoney(current)); + } + }, stepTime); + } + + // ----------------------------------------------------------------------- + // PUBLIC METHODS + // ----------------------------------------------------------------------- + bindDom() { + this.$list = $('#' + this.listId); + this.$rowsContainer = this.$list.find('.js-list-content'); + + // Handlebars templates + var rowTemplate = $('#' + this.rowTemplateId); + if (rowTemplate.length > 0) { + this.sourceTemplateRow = rowTemplate.html(); + Handlebars.registerPartial(this.rowTemplateId, this.sourceTemplateRow); + this.templateRowsContainer = Handlebars.compile( + $('#' + this.contentTemplateId).html() + ); } - - select($selectedRow, dontUnselectIfSelected) { - var $entireSelectedRows; - if ($selectedRow.hasClass('fixed')) { - return; - } - if (!dontUnselectIfSelected && $selectedRow.hasClass('active')) { - $selectedRow.removeClass('active'); - } - else { - $selectedRow.addClass('active'); - } - - $entireSelectedRows = this.getSelection(); - if ($entireSelectedRows.length !== 0) { - this.emit(EVENT_TYPE_SELECTION_CHANGED, this.getSelectedData()); - } - else { - this.emit(EVENT_TYPE_SELECTION_CHANGED); - } + } + + // ----------------------------------------------------------------------- + // ROW MANAGEMENT + // ----------------------------------------------------------------------- + unselectAll() { + this.$list.find('.js-list-row').removeClass('active'); + this.emit(EVENT_TYPE_SELECTION_CHANGED); + } + + unselect(id) { + var selection; + $('#' + id).removeClass('active'); + selection = this.getSelectedData(); + this.emit( + EVENT_TYPE_SELECTION_CHANGED, + selection.length > 0 ? selection : null + ); + } + + select($selectedRow, dontUnselectIfSelected) { + var $entireSelectedRows; + if ($selectedRow.hasClass('fixed')) { + return; } - - getSelection() { - return this.$list.find('.js-list-row.active'); + if (!dontUnselectIfSelected && $selectedRow.hasClass('active')) { + $selectedRow.removeClass('active'); + } else { + $selectedRow.addClass('active'); } - update(newDataRow) { - var self = this, - templateRow, - $htmlRow, - newDataRowWithoutOdometerValues = this._cloneObject(newDataRow), - oldDataRow, - $oldRow, - $newRow, - dataRowIndex, - active, - fixed; - - // Find old data - for (var i=0; i { - $newRow.find('[data-toggle=tooltip]').tooltip(); - }); - // this.show($newRow); - - // Play odometers - $newRow.find('.odometer').each(function() { - var $odometer = $(this); - var key = $odometer.data('key'); - self._animateValue($odometer, self._ref(oldDataRow, key), self._ref(newDataRow, key), 1000); - }); - - // update data - this.dataRows.rows[dataRowIndex] = newDataRow; + $entireSelectedRows = this.getSelection(); + if ($entireSelectedRows.length !== 0) { + this.emit(EVENT_TYPE_SELECTION_CHANGED, this.getSelectedData()); + } else { + this.emit(EVENT_TYPE_SELECTION_CHANGED); } - - remove(dataRow/*, noAnimation*/) { - var $rowToRemove = this.$list.find('#'+dataRow._id+'.js-list-row'); - - // Find data in array and remove it - for (var i=0; i { - $newRow.find('[data-toggle=tooltip]').tooltip(); - }); - // if (!noAnimation) { - // $newRow.velocity('transition.swoopIn'); - // } - // else { - this.show($newRow); - // } + $oldRow = this.$list.find('#' + newDataRow._id + '.js-list-row'); + $oldRow.find('.odometer').each(function () { + var key = $(this).data('key'); + self._set( + newDataRowWithoutOdometerValues, + key, + self._ref(oldDataRow, key) + ); + }); + + templateRow = Handlebars.compile(this.sourceTemplateRow); + $htmlRow = $(templateRow(newDataRowWithoutOdometerValues)); + + active = $oldRow.hasClass('active'); + if (active) { + $htmlRow.addClass('active'); } - - init(dataRows, noAnimation, callback) { - var htmlRows, $htmlRows; - - // Add initial rows - if (!dataRows) { - this.dataRows = {rows:[]}; - } - else { - this.dataRows = dataRows; - } - if (this.templateRowsContainer) { - htmlRows = this.templateRowsContainer(this.dataRows); - $htmlRows = $(htmlRows).hide(); - this.$rowsContainer.html($htmlRows); - - // Tooltip management - $(() => { - this.$rowsContainer.find('[data-toggle=tooltip]').tooltip(); - }); - - this.filter(this.filterText, false, callback); - } - else if (callback) { - callback(); - } + fixed = $oldRow.hasClass('fixed'); + if (fixed) { + $htmlRow.addClass('fixed'); } - setFilterText(text) { - this.filterText = text; + $oldRow.replaceWith($htmlRow); + + $newRow = this.$list.find('#' + newDataRow._id + '.js-list-row'); + $(() => { + $newRow.find('[data-toggle=tooltip]').tooltip(); + }); + // this.show($newRow); + + // Play odometers + $newRow.find('.odometer').each(function () { + var $odometer = $(this); + var key = $odometer.data('key'); + self._animateValue( + $odometer, + self._ref(oldDataRow, key), + self._ref(newDataRow, key), + 1000 + ); + }); + + // update data + this.dataRows.rows[dataRowIndex] = newDataRow; + } + + remove(dataRow /*, noAnimation*/) { + var $rowToRemove = this.$list.find('#' + dataRow._id + '.js-list-row'); + + // Find data in array and remove it + for (var i = 0; i < this.dataRows.rows.length; ++i) { + if (this.dataRows.rows[i]._id === dataRow._id) { + this.dataRows.rows.splice(i, 1); + break; + } } - filter(text, noAnimation, callback) { - // var that = this; - let $rowsToShow; - - this.filterText = text; - - // remove filter on all rows - const $allRows = this.$list.find('.js-list-row'); - $allRows.removeClass('list-element-filtered'); - - // hide rows that not match filter - if (this.filterText) { - const filterValues = this.filterText.split(','); - $allRows.each(function() { - const $row = $(this); - const rowFilterValues = $row.data('filter-values').split(','); - if (rowFilterValues.some(value => filterValues.indexOf(value) !== -1)) { - $row.addClass('list-element-filtered'); - } - }); - - const $rowsToHide = $allRows.not('.list-element-filtered').not(':hidden'); - $rowsToShow = this.$list.find('.js-list-row.list-element-filtered:hidden'); - - $rowsToHide.hide(); - $rowsToShow.show(); - if (callback) { - callback(); - } - } - else { - $rowsToShow = this.$list.find('.js-list-row:hidden'); - $rowsToShow.show(); - if (callback) { - callback(); - } - } + // Animate remove + // if (!noAnimation) { + // $rowToRemove.velocity('transition.swoopOut', {complete: function() { + // // Remove row from DOM + // $rowToRemove.remove(); + // }}); + // } + // else { + $rowToRemove.remove(); + // } + } + + add(dataRow /*, noAnimation*/) { + var templateRow = Handlebars.compile(this.sourceTemplateRow); + var htmlRow = templateRow(dataRow); + var $newRow; + + // Add data in array + this.dataRows.rows.push(dataRow); + + // Add row in DOM + this.$rowsContainer.append(htmlRow); + $newRow = this.$list.find('#' + dataRow._id + '.js-list-row'); + + // Animate add + $(() => { + $newRow.find('[data-toggle=tooltip]').tooltip(); + }); + // if (!noAnimation) { + // $newRow.velocity('transition.swoopIn'); + // } + // else { + this.show($newRow); + // } + } + + init(dataRows, noAnimation, callback) { + var htmlRows, $htmlRows; + + // Add initial rows + if (!dataRows) { + this.dataRows = { rows: [] }; + } else { + this.dataRows = dataRows; } - - hide($rows) { - $rows.hide(); + if (this.templateRowsContainer) { + htmlRows = this.templateRowsContainer(this.dataRows); + $htmlRows = $(htmlRows).hide(); + this.$rowsContainer.html($htmlRows); + + // Tooltip management + $(() => { + this.$rowsContainer.find('[data-toggle=tooltip]').tooltip(); + }); + + this.filter(this.filterText, false, callback); + } else if (callback) { + callback(); } - - show($rows) { - var $rowsToShow = $rows; - if (this.filterText) { - $rowsToShow = $rows.find('.js-list-value').filter(':contains("'+this.filterText+'")').closest('.js-list-row:hidden'); + } + + setFilterText(text) { + this.filterText = text; + } + + filter(text, noAnimation, callback) { + // var that = this; + let $rowsToShow; + + this.filterText = text; + + // remove filter on all rows + const $allRows = this.$list.find('.js-list-row'); + $allRows.removeClass('list-element-filtered'); + + // hide rows that not match filter + if (this.filterText) { + const filterValues = this.filterText.split(','); + $allRows.each(function () { + const $row = $(this); + const rowFilterValues = $row.data('filter-values').split(','); + if ( + rowFilterValues.some((value) => filterValues.indexOf(value) !== -1) + ) { + $row.addClass('list-element-filtered'); } - $rowsToShow.show(); + }); + + const $rowsToHide = $allRows.not('.list-element-filtered').not(':hidden'); + $rowsToShow = this.$list.find( + '.js-list-row.list-element-filtered:hidden' + ); + + $rowsToHide.hide(); + $rowsToShow.show(); + if (callback) { + callback(); + } + } else { + $rowsToShow = this.$list.find('.js-list-row:hidden'); + $rowsToShow.show(); + if (callback) { + callback(); + } } - - hideRows($rows, callback) { - // var $rowsWithTransition, $rowsWithoutTransition; - - if ($rows.length === 0) { - if (callback) { - callback(); - } - return; - } - - // if ($rows.length > TRANSITION_MAXROW) { - // $rowsWithTransition = $rows.slice(0, TRANSITION_MAXROW); - // $rowsWithoutTransition = $rows.slice(TRANSITION_MAXROW, $rows.length); - - // this.hide($rowsWithoutTransition); - // $rowsWithTransition.velocity('transition.bounceRightOut', {duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ - // if (callback) { - // callback(); - // } - // }}); - // } - // else { - // $rows.velocity('transition.bounceRightOut', {duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ - // if (callback) { - // callback(); - // } - // }}); - // } - $rows.hide(); - if (callback) { - callback(); - } + } + + hide($rows) { + $rows.hide(); + } + + show($rows) { + var $rowsToShow = $rows; + if (this.filterText) { + $rowsToShow = $rows + .find('.js-list-value') + .filter(':contains("' + this.filterText + '")') + .closest('.js-list-row:hidden'); } + $rowsToShow.show(); + } - showRows($rows, callback) { - // var $rowsWithTransition, $rowsWithoutTransition, - // self = this; - - if ($rows.length === 0) { - if (callback) { - callback(); - } - return; - } + hideRows($rows, callback) { + // var $rowsWithTransition, $rowsWithoutTransition; - // if ($rows.length > TRANSITION_MAXROW) { - // $rowsWithTransition = $rows.slice(0, TRANSITION_MAXROW); - // $rowsWithoutTransition = $rows.slice(TRANSITION_MAXROW, $rows.length); - - // $rowsWithTransition.velocity('transition.bounceRightIn', {display: 'table', duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ - // self.show($rowsWithoutTransition); - // if (callback) { - // callback(); - // } - // }}); - // } - // else { - // if ($rows.length>0) { - // $rows.velocity('transition.bounceRightIn', {display: 'table', duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ - // if (callback) { - // callback(); - // } - // }}); - // } - // else { - // if (callback) { - // callback(); - // } - // } - // } - $rows.show(); - if (callback) { - callback(); - } + if ($rows.length === 0) { + if (callback) { + callback(); + } + return; } - showAllRows(callback) { - this.filter(this.filterText, false, callback); + // if ($rows.length > TRANSITION_MAXROW) { + // $rowsWithTransition = $rows.slice(0, TRANSITION_MAXROW); + // $rowsWithoutTransition = $rows.slice(TRANSITION_MAXROW, $rows.length); + + // this.hide($rowsWithoutTransition); + // $rowsWithTransition.velocity('transition.bounceRightOut', {duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ + // if (callback) { + // callback(); + // } + // }}); + // } + // else { + // $rows.velocity('transition.bounceRightOut', {duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ + // if (callback) { + // callback(); + // } + // }}); + // } + $rows.hide(); + if (callback) { + callback(); } + } + + showRows($rows, callback) { + // var $rowsWithTransition, $rowsWithoutTransition, + // self = this; - hideAllRows(callback) { - this.hideRows(this.$list.find('.js-list-row').not(':hidden'), callback); + if ($rows.length === 0) { + if (callback) { + callback(); + } + return; } - // ----------------------------------------------------------------------- - // DATA ACCESS - // ----------------------------------------------------------------------- - getSelectedData() { - var self = this; - var data = []; - - //TODO: to optimize - this.getSelection().each(function() { - var id = $(this).attr('id'); - for (var i = 0; i < self.dataRows.rows.length; i++) { - if (self.dataRows.rows[i]._id === id) { - data.push(self._cloneObject(self.dataRows.rows[i])); - break; - } - } - }); - return data; + // if ($rows.length > TRANSITION_MAXROW) { + // $rowsWithTransition = $rows.slice(0, TRANSITION_MAXROW); + // $rowsWithoutTransition = $rows.slice(TRANSITION_MAXROW, $rows.length); + + // $rowsWithTransition.velocity('transition.bounceRightIn', {display: 'table', duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ + // self.show($rowsWithoutTransition); + // if (callback) { + // callback(); + // } + // }}); + // } + // else { + // if ($rows.length>0) { + // $rows.velocity('transition.bounceRightIn', {display: 'table', duration: TRANSITION_ROW_DURATION, stagger: TRANSITION_ROW_STAGGER_DURATION, complete: function(){ + // if (callback) { + // callback(); + // } + // }}); + // } + // else { + // if (callback) { + // callback(); + // } + // } + // } + $rows.show(); + if (callback) { + callback(); } + } + + showAllRows(callback) { + this.filter(this.filterText, false, callback); + } + + hideAllRows(callback) { + this.hideRows(this.$list.find('.js-list-row').not(':hidden'), callback); + } + + // ----------------------------------------------------------------------- + // DATA ACCESS + // ----------------------------------------------------------------------- + getSelectedData() { + var self = this; + var data = []; + + //TODO: to optimize + this.getSelection().each(function () { + var id = $(this).attr('id'); + for (var i = 0; i < self.dataRows.rows.length; i++) { + if (self.dataRows.rows[i]._id === id) { + data.push(self._cloneObject(self.dataRows.rows[i])); + break; + } + } + }); + return data; + } } export default Anilist; diff --git a/frontend/js/lib/helper.js b/frontend/js/lib/helper.js index 41757a4..6c2d517 100644 --- a/frontend/js/lib/helper.js +++ b/frontend/js/lib/helper.js @@ -3,331 +3,364 @@ import Handlebars from 'handlebars'; import moment from 'moment'; //import Intl from 'intl'; - class Helper { - // Method helpers - static get _Intl() { - return { - NumberFormat: new Intl.NumberFormat(i18next.language, { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 }), - NumberFormatPercent: new Intl.NumberFormat(i18next.language, { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 }), - NumberFormatCurrency: new Intl.NumberFormat(i18next.language, { style: 'currency', currency: i18next.t('__currency_code') }) - }; - } + // Method helpers + static get _Intl() { + return { + NumberFormat: new Intl.NumberFormat(i18next.language, { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + NumberFormatPercent: new Intl.NumberFormat(i18next.language, { + style: 'percent', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + NumberFormatCurrency: new Intl.NumberFormat(i18next.language, { + style: 'currency', + currency: i18next.t('__currency_code'), + }), + }; + } - static _textToNumber(text) { - let value = parseFloat(text); - if (isNaN(value)) { - value = 0; - } - return value; + static _textToNumber(text) { + let value = parseFloat(text); + if (isNaN(value)) { + value = 0; } + return value; + } - static formatSurface(text, hideUnit, emptyForZero) { - const value = Helper._textToNumber(text); - - if (value === 0 && emptyForZero) { - return ''; - } + static formatSurface(text, hideUnit, emptyForZero) { + const value = Helper._textToNumber(text); - if (hideUnit) { - return Helper._Intl.NumberFormat.format(value); - } - - return `${Helper._Intl.NumberFormat.format(value)} m2`; + if (value === 0 && emptyForZero) { + return ''; } - static formatNumber(text) { - return Helper._Intl.NumberFormat.format(text); + if (hideUnit) { + return Helper._Intl.NumberFormat.format(value); } - static formatMoney(text, hideCurrency, emptyForZero) { - const value = Helper._textToNumber(text); + return `${Helper._Intl.NumberFormat.format(value)} m2`; + } - if (value === 0 && emptyForZero) { - return ''; - } + static formatNumber(text) { + return Helper._Intl.NumberFormat.format(text); + } - if (hideCurrency) { - return Helper._Intl.NumberFormat.format(value); - } + static formatMoney(text, hideCurrency, emptyForZero) { + const value = Helper._textToNumber(text); - return Helper._Intl.NumberFormatCurrency.format(value); + if (value === 0 && emptyForZero) { + return ''; } - static formatPercent(text, hidePercent, emptyForZero) { - const value = Helper._textToNumber(text); + if (hideCurrency) { + return Helper._Intl.NumberFormat.format(value); + } - if (value === 0 && emptyForZero) { - return ''; - } + return Helper._Intl.NumberFormatCurrency.format(value); + } - if (hidePercent) { - return Helper._Intl.NumberFormat.format(value); - } + static formatPercent(text, hidePercent, emptyForZero) { + const value = Helper._textToNumber(text); - return Helper._Intl.NumberFormatPercent.format(value); + if (value === 0 && emptyForZero) { + return ''; } - static formatMonth(text) { - return moment.months()[parseInt(text, 10)-1]; + if (hidePercent) { + return Helper._Intl.NumberFormat.format(value); } - static formatMonthYear(month, year) { - return moment.months()[parseInt(month, 10)-1] + ' ' + year; - } + return Helper._Intl.NumberFormatPercent.format(value); + } - static formatDate(text) { - let date = moment(text, 'DD/MM/YYYY'); - if (!date.isValid()) { - date = moment(text); - } - return date.format('L'); + static formatMonth(text) { + return moment.months()[parseInt(text, 10) - 1]; + } + + static formatMonthYear(month, year) { + return moment.months()[parseInt(month, 10) - 1] + ' ' + year; + } + + static formatDate(text) { + let date = moment(text, 'DD/MM/YYYY'); + if (!date.isValid()) { + date = moment(text); } + return date.format('L'); + } - static formatDateText(text) { - let date = moment(text, 'DD/MM/YYYY'); - if (!date.isValid()) { - date = moment(text); - } - return date.format('LL'); + static formatDateText(text) { + let date = moment(text, 'DD/MM/YYYY'); + if (!date.isValid()) { + date = moment(text); } + return date.format('LL'); + } - static formatDateTime(text) { - let dateTime = moment(text); - if (!dateTime.isValid()) { - dateTime = moment(text, 'DD/MM/YYYY HH:MM'); - } - return dateTime.format('L LTS'); + static formatDateTime(text) { + let dateTime = moment(text); + if (!dateTime.isValid()) { + dateTime = moment(text, 'DD/MM/YYYY HH:MM'); } + return dateTime.format('L LTS'); + } } //Handlebars helpers -Handlebars.registerHelper('ifNeg', function(value, options) { - if(value<0) { - return options.fn(this); - } - return options.inverse(this); +Handlebars.registerHelper('ifNeg', function (value, options) { + if (value < 0) { + return options.fn(this); + } + return options.inverse(this); }); -Handlebars.registerHelper('i18next', function(params) { - var attr, - options, - text; - - if (params.hash && params.hash.key) { - for(attr in params.hash) { - if (attr !== 'key') { - if (!options) { - options = {}; - } - if (attr.toLowerCase() === 'date') { - options[attr] = Helper.formatDate(params.hash[attr]); - } - else if (attr.toLowerCase() === 'datetime') { - options[attr] = Helper.formatDateTime(params.hash[attr]); - } - else if (attr.toLowerCase() === 'amount') { - options[attr] = Helper.formatMoney(params.hash[attr]); - } - else { - options[attr] = params.hash[attr]; - } - } - } - if (options) { - text = i18next.t(params.hash.key, options); +Handlebars.registerHelper('i18next', function (params) { + var attr, options, text; + + if (params.hash && params.hash.key) { + for (attr in params.hash) { + if (attr !== 'key') { + if (!options) { + options = {}; } - else { - text = i18next.t(params.hash.key); + if (attr.toLowerCase() === 'date') { + options[attr] = Helper.formatDate(params.hash[attr]); + } else if (attr.toLowerCase() === 'datetime') { + options[attr] = Helper.formatDateTime(params.hash[attr]); + } else if (attr.toLowerCase() === 'amount') { + options[attr] = Helper.formatMoney(params.hash[attr]); + } else { + options[attr] = params.hash[attr]; } - return new Handlebars.SafeString(text); + } + } + if (options) { + text = i18next.t(params.hash.key, options); + } else { + text = i18next.t(params.hash.key); } - return new Handlebars.SafeString('???'); + return new Handlebars.SafeString(text); + } + return new Handlebars.SafeString('???'); }); -Handlebars.registerHelper('indexPlusOne', function() { - return new Handlebars.SafeString(Number(arguments[0].data.index)+1); //index not zero based +Handlebars.registerHelper('indexPlusOne', function () { + return new Handlebars.SafeString(Number(arguments[0].data.index) + 1); //index not zero based }); -Handlebars.registerHelper('formatSurface', function(text, options) { - text = Handlebars.Utils.escapeExpression(text); - text = Helper.formatSurface(text, options.hash.hideUnit, options.hash.emptyForZero); - return new Handlebars.SafeString(text); +Handlebars.registerHelper('formatSurface', function (text, options) { + text = Handlebars.Utils.escapeExpression(text); + text = Helper.formatSurface( + text, + options.hash.hideUnit, + options.hash.emptyForZero + ); + return new Handlebars.SafeString(text); }); -Handlebars.registerHelper('formatMoney', function(text, options) { +Handlebars.registerHelper('formatMoney', function (text, options) { + text = Handlebars.Utils.escapeExpression(text); - text = Handlebars.Utils.escapeExpression(text); + let html = ''; + let classes = 'price-amount'; + const amount = Helper.formatNumber(text); + const amountWithCurrencySymbol = Helper.formatMoney(text); - let html = ''; - let classes = 'price-amount'; - const amount = Helper.formatNumber(text); - const amountWithCurrencySymbol = Helper.formatMoney(text); - - if (!options) { - html = `${amountWithCurrencySymbol}`; + if (!options) { + html = `${amountWithCurrencySymbol}`; + } else { + if (parseFloat(text) === 0 && options.hash.emptyForZero) { + return ''; } - else { - if (parseFloat(text) === 0 && (options.hash.emptyForZero)) { - return ''; - } - let key = ''; - if (options.hash.withOdometer) { - classes += ' odometer'; - key = options.hash.withOdometer; - } - - html = `${options.hash.hideCurrency ? amount : amountWithCurrencySymbol}`; + let key = ''; + if (options.hash.withOdometer) { + classes += ' odometer'; + key = options.hash.withOdometer; } - return new Handlebars.SafeString(html); + html = `${ + options.hash.hideCurrency ? amount : amountWithCurrencySymbol + }`; + } + + return new Handlebars.SafeString(html); }); -Handlebars.registerHelper('formatPercent', function(text, options) { - text = Handlebars.Utils.escapeExpression(text); - text = Helper.formatPercent(text, options.hash.hidePercent, options.hash.emptyForZero); - return new Handlebars.SafeString(text); +Handlebars.registerHelper('formatPercent', function (text, options) { + text = Handlebars.Utils.escapeExpression(text); + text = Helper.formatPercent( + text, + options.hash.hidePercent, + options.hash.emptyForZero + ); + return new Handlebars.SafeString(text); }); -Handlebars.registerHelper('formatDate', function(text/*, options*/) { - text = Handlebars.Utils.escapeExpression(text); - text = Helper.formatDate(text); - return new Handlebars.SafeString(text); +Handlebars.registerHelper('formatDate', function (text /*, options*/) { + text = Handlebars.Utils.escapeExpression(text); + text = Helper.formatDate(text); + return new Handlebars.SafeString(text); }); -Handlebars.registerHelper('formatDateTime', function(text/*, options*/) { - text = Handlebars.Utils.escapeExpression(text); - text = Helper.formatDateTime(text); - return new Handlebars.SafeString(text); +Handlebars.registerHelper('formatDateTime', function (text /*, options*/) { + text = Handlebars.Utils.escapeExpression(text); + text = Helper.formatDateTime(text); + return new Handlebars.SafeString(text); }); -Handlebars.registerHelper('formatMonth', function(text/*, options*/) { - text = Handlebars.Utils.escapeExpression(text); - text = Helper.formatMonth(text); - return new Handlebars.SafeString(text); +Handlebars.registerHelper('formatMonth', function (text /*, options*/) { + text = Handlebars.Utils.escapeExpression(text); + text = Helper.formatMonth(text); + return new Handlebars.SafeString(text); }); -Handlebars.registerHelper('formatMonthYear', function(params) { - if (params.hash && params.hash.month && params.hash.year) { - return new Handlebars.SafeString( - Helper.formatMonthYear(params.hash.month, params.hash.year) - ); - } - return new Handlebars.SafeString('???'); +Handlebars.registerHelper('formatMonthYear', function (params) { + if (params.hash && params.hash.month && params.hash.year) { + return new Handlebars.SafeString( + Helper.formatMonthYear(params.hash.month, params.hash.year) + ); + } + return new Handlebars.SafeString('???'); }); -Handlebars.registerHelper('breaklines', function(text) { - text = Handlebars.Utils.escapeExpression(text); - text = text.replace(/(\r\n|\n|\r)/gm, '
'); - return new Handlebars.SafeString(text); +Handlebars.registerHelper('breaklines', function (text) { + text = Handlebars.Utils.escapeExpression(text); + text = text.replace(/(\r\n|\n|\r)/gm, '
'); + return new Handlebars.SafeString(text); }); -Handlebars.registerHelper('commentVisible', function(text) { - if (!text || text.length ===0) { - return new Handlebars.SafeString('display: none;'); - } - return ''; +Handlebars.registerHelper('commentVisible', function (text) { + if (!text || text.length === 0) { + return new Handlebars.SafeString('display: none;'); + } + return ''; }); -Handlebars.registerHelper('paymentType', function(paymentType) { - if (this.type) { - paymentType = this.type; - } - if (paymentType === 'cheque') { - return new Handlebars.SafeString(i18next.t('cheque')); - } - if (paymentType === 'cash') { - return new Handlebars.SafeString(i18next.t('cash')); - } - if (paymentType === 'levy') { - return new Handlebars.SafeString(i18next.t('levy')); - } - if (paymentType === 'transfer') { - return new Handlebars.SafeString(i18next.t('transfer')); - } - return new Handlebars.SafeString(i18next.t('unknown')); +Handlebars.registerHelper('paymentType', function (paymentType) { + if (this.type) { + paymentType = this.type; + } + if (paymentType === 'cheque') { + return new Handlebars.SafeString(i18next.t('cheque')); + } + if (paymentType === 'cash') { + return new Handlebars.SafeString(i18next.t('cash')); + } + if (paymentType === 'levy') { + return new Handlebars.SafeString(i18next.t('levy')); + } + if (paymentType === 'transfer') { + return new Handlebars.SafeString(i18next.t('transfer')); + } + return new Handlebars.SafeString(i18next.t('unknown')); }); -Handlebars.registerHelper('cssClassPaymentStatus', function() { - var html = ''; - if (this.status === 'paid') { - html = 'text-success'; - } - else if (this.status === 'notpaid') { - html = 'text-danger'; - } - else if (this.status === 'partialypaid') { - html = 'text-warning'; - } - return new Handlebars.SafeString(html); +Handlebars.registerHelper('cssClassPaymentStatus', function () { + var html = ''; + if (this.status === 'paid') { + html = 'text-success'; + } else if (this.status === 'notpaid') { + html = 'text-danger'; + } else if (this.status === 'partialypaid') { + html = 'text-warning'; + } + return new Handlebars.SafeString(html); }); -Handlebars.registerHelper('paymentStatus', function() { - var html = ''; - if (this.status === 'paid') { - html = i18next.t('Paid'); - } - else if (this.status === 'notpaid') { - html = i18next.t('Not paid'); - } - else if (this.status === 'partialypaid') { - html = i18next.t('Partially paid'); - } - return new Handlebars.SafeString(html); +Handlebars.registerHelper('paymentStatus', function () { + var html = ''; + if (this.status === 'paid') { + html = i18next.t('Paid'); + } else if (this.status === 'notpaid') { + html = i18next.t('Not paid'); + } else if (this.status === 'partialypaid') { + html = i18next.t('Partially paid'); + } + return new Handlebars.SafeString(html); }); -Handlebars.registerHelper('paymentBadgeStatus', function() { - var html = ''; - if (this.status === 'paid') { - html = ' '+moment.monthsShort()[parseInt(this.month, 10)-1].toUpperCase()+''; - } - else if (this.status === 'partialypaid') { - html = ' '+moment.monthsShort()[parseInt(this.month, 10)-1].toUpperCase()+''; - } - else if (this.status === 'notpaid') { - html = ' '+moment.monthsShort()[parseInt(this.month, 10)-1].toUpperCase()+''; - } - return new Handlebars.SafeString(html); +Handlebars.registerHelper('paymentBadgeStatus', function () { + var html = ''; + if (this.status === 'paid') { + html = + ' ' + + moment.monthsShort()[parseInt(this.month, 10) - 1].toUpperCase() + + ''; + } else if (this.status === 'partialypaid') { + html = + ' ' + + moment.monthsShort()[parseInt(this.month, 10) - 1].toUpperCase() + + ''; + } else if (this.status === 'notpaid') { + html = + ' ' + + moment.monthsShort()[parseInt(this.month, 10) - 1].toUpperCase() + + ''; + } + return new Handlebars.SafeString(html); }); -Handlebars.registerHelper('Image', function(imageId, options) { - var cssClass = ''; - var id; - if (this.type) { - id = this.type; - } - else { - id = imageId; - } - if (imageId && imageId.hash && imageId.hash.cssClass) { - cssClass = imageId.hash.cssClass; - } - else if (options && options.hash && options.hash.cssClass) { - cssClass = options.hash.cssClass; - } - if (id === 'office') { - return new Handlebars.SafeString(''); - } - if (id === 'parking') { - return new Handlebars.SafeString(''); - } - if (id === 'letterbox') { - return new Handlebars.SafeString(''); - } - if (id === 'expiredDocument') { - return new Handlebars.SafeString(''); - } - if (id === 'ok') { - return new Handlebars.SafeString(''); - } - if (id === 'warning') { - return new Handlebars.SafeString(''); - } - - return new Handlebars.SafeString(''); +Handlebars.registerHelper('Image', function (imageId, options) { + var cssClass = ''; + var id; + if (this.type) { + id = this.type; + } else { + id = imageId; + } + if (imageId && imageId.hash && imageId.hash.cssClass) { + cssClass = imageId.hash.cssClass; + } else if (options && options.hash && options.hash.cssClass) { + cssClass = options.hash.cssClass; + } + if (id === 'office') { + return new Handlebars.SafeString( + '' + ); + } + if (id === 'parking') { + return new Handlebars.SafeString( + '' + ); + } + if (id === 'letterbox') { + return new Handlebars.SafeString( + '' + ); + } + if (id === 'expiredDocument') { + return new Handlebars.SafeString( + '' + ); + } + if (id === 'ok') { + return new Handlebars.SafeString( + '' + ); + } + if (id === 'warning') { + return new Handlebars.SafeString( + '' + ); + } + + return new Handlebars.SafeString( + '' + ); }); -Handlebars.registerHelper('propertyName', function(propertyType) { - if (this.type) { - propertyType = this.type; - } - - if (propertyType === 'office') { - return new Handlebars.SafeString(i18next.t('Room')); - } - if (propertyType === 'parking') { - return new Handlebars.SafeString(i18next.t('Car park')); - } - if (propertyType === 'letterbox') { - return new Handlebars.SafeString(i18next.t('Letterbox')); - } - - return new Handlebars.SafeString(i18next.t('unknown')); +Handlebars.registerHelper('propertyName', function (propertyType) { + if (this.type) { + propertyType = this.type; + } + + if (propertyType === 'office') { + return new Handlebars.SafeString(i18next.t('Room')); + } + if (propertyType === 'parking') { + return new Handlebars.SafeString(i18next.t('Car park')); + } + if (propertyType === 'letterbox') { + return new Handlebars.SafeString(i18next.t('Letterbox')); + } + + return new Handlebars.SafeString(i18next.t('unknown')); }); export default Helper; diff --git a/frontend/js/lib/objectfilter.js b/frontend/js/lib/objectfilter.js index 082a17d..ff5395b 100755 --- a/frontend/js/lib/objectfilter.js +++ b/frontend/js/lib/objectfilter.js @@ -1,31 +1,28 @@ class ObjectFilter { + static filter(schema, data) { + var self = this; + var filteredData = {}; + var key; + var value; + var childSchema; - static filter(schema, data) { - var self = this; - var filteredData = {}; - var key; - var value; - var childSchema; - - for (key in schema) { - value = data[key]; - if (value !== undefined) { - childSchema = schema[key]; - if (Array.isArray(childSchema)) { - if (Array.isArray(value)) { - filteredData[key] = []; - value.forEach(function(data/*, index*/) { - filteredData[key].push(self.filter(childSchema[0], data)); - }); - } - } - else { - filteredData[key] = value; - } - } + for (key in schema) { + value = data[key]; + if (value !== undefined) { + childSchema = schema[key]; + if (Array.isArray(childSchema)) { + if (Array.isArray(value)) { + filteredData[key] = []; + value.forEach(function (data /*, index*/) { + filteredData[key].push(self.filter(childSchema[0], data)); + }); + } + } else { + filteredData[key] = value; } - return filteredData; + } } - + return filteredData; + } } export default ObjectFilter; diff --git a/frontend/js/login/loginform.js b/frontend/js/login/loginform.js index 4d16af6..7459a1c 100755 --- a/frontend/js/login/loginform.js +++ b/frontend/js/login/loginform.js @@ -1,35 +1,35 @@ import Form from '../form'; class LoginForm extends Form { - constructor() { - super({ - domSelector: '#login-form', - httpMethod: 'POST', - uri: '/signin', - manifest: { - 'username': { - required: true, - email: true - }, - 'password': { - required: true - } - }, - alertOnFieldError: false - }); - } + constructor() { + super({ + domSelector: '#login-form', + httpMethod: 'POST', + uri: '/signin', + manifest: { + username: { + required: true, + email: true, + }, + password: { + required: true, + }, + }, + alertOnFieldError: false, + }); + } - onGetData(data) { - if (data.username) { - data.email = data.username; - delete data.username; - } - if (data.password) { - data.secretword = data.password; - delete data.password; - } - return data; + onGetData(data) { + if (data.username) { + data.email = data.username; + delete data.username; + } + if (data.password) { + data.secretword = data.password; + delete data.password; } + return data; + } } export default LoginForm; diff --git a/frontend/js/login/middleware.js b/frontend/js/login/middleware.js index d59d0be..c3bb87d 100644 --- a/frontend/js/login/middleware.js +++ b/frontend/js/login/middleware.js @@ -4,51 +4,53 @@ import LoginForm from './loginform'; import frontexpress from 'frontexpress'; class LoginMiddleware extends frontexpress.Middleware { - constructor() { - super(); - this.form = new LoginForm(); - } - - // overriden - entered() { - $('body').addClass('covered-body'); - $('body > .footer').show(); - } - - updated(req, res) { - super.updated(req, res); - - this.form.bindForm(); - $('#login-send').click(() => { - this.form.submit((response) => { - let message; - - if (response.status === 'success') { - $('#login-form').submit(); // Add this to allow browsers to store username and password. Also I do redirect to home page server side look at /loggedin - return; - } - - if (response.status === 'login-user-not-found') { - message = i18next.t('Unknown user'); - } else if (response.status === 'login-invalid-password') { - message = i18next.t('Bad password'); - } else if (response.status === 'login-realm-not-found') { - message = i18next.t('This user does not manage any real estate accounts'); - } else if (response.status === 'missing-field') { - message = i18next.t('Please fill missing fields'); - } else { - message = i18next.t('A technical issue has occurred (-_-\')'); - } - this.form.showErrorMessage(message); - }); - return false; - }); - } - - // overriden - exited() { - this.form.unbindForm(); - } + constructor() { + super(); + this.form = new LoginForm(); + } + + // overriden + entered() { + $('body').addClass('covered-body'); + $('body > .footer').show(); + } + + updated(req, res) { + super.updated(req, res); + + this.form.bindForm(); + $('#login-send').click(() => { + this.form.submit((response) => { + let message; + + if (response.status === 'success') { + $('#login-form').submit(); // Add this to allow browsers to store username and password. Also I do redirect to home page server side look at /loggedin + return; + } + + if (response.status === 'login-user-not-found') { + message = i18next.t('Unknown user'); + } else if (response.status === 'login-invalid-password') { + message = i18next.t('Bad password'); + } else if (response.status === 'login-realm-not-found') { + message = i18next.t( + 'This user does not manage any real estate accounts' + ); + } else if (response.status === 'missing-field') { + message = i18next.t('Please fill missing fields'); + } else { + message = i18next.t("A technical issue has occurred (-_-')"); + } + this.form.showErrorMessage(message); + }); + return false; + }); + } + + // overriden + exited() { + this.form.unbindForm(); + } } -export default LoginMiddleware; \ No newline at end of file +export default LoginMiddleware; diff --git a/frontend/js/menu.js b/frontend/js/menu.js index 0a8d7d3..401eeca 100644 --- a/frontend/js/menu.js +++ b/frontend/js/menu.js @@ -2,62 +2,70 @@ import $ from 'jquery'; import application from './application'; function uriFromId(id) { - if (id === 'website') { - return {nav: `/view/${id}`, history: '/'}; - } - return {nav: `/view/${id}`, history:`/${id}`}; + if (id === 'website') { + return { nav: `/view/${id}`, history: '/' }; + } + return { nav: `/view/${id}`, history: `/${id}` }; } function resizeRightMenus() { - const parentSize = $('#right-menu-pane').width(); - $('.js-side-menu').each(function() { - const $activeMenu = $(this); - const affixPadding = $activeMenu.innerWidth() - $activeMenu.width(); - $activeMenu.width(parentSize - affixPadding); - }); + const parentSize = $('#right-menu-pane').width(); + $('.js-side-menu').each(function () { + const $activeMenu = $(this); + const affixPadding = $activeMenu.innerWidth() - $activeMenu.width(); + $activeMenu.width(parentSize - affixPadding); + }); } export default () => { - $(document).on('click', '.js-nav-action', function() { - const viewId = $(this).data('id'); - const uri = uriFromId(viewId); - application.httpGet({ - uri: uri.nav, - history: { - state: {viewId}, - title: viewId, - uri: uri.history - } - }); - $( '.dropdown.open .dropdown-toggle').dropdown('toggle'); - return false; - }); - $(document).on('click', '.navbar-collapse.collapse.in a:not(.dropdown-toggle)', function() { - $(this).closest('.navbar-collapse').collapse('hide'); - }); - $(document).on('click', '.navbar-collapse.collapse.in button:not(.navbar-toggle)', function() { - $(this).closest('.navbar-collapse').collapse('hide'); + $(document).on('click', '.js-nav-action', function () { + const viewId = $(this).data('id'); + const uri = uriFromId(viewId); + application.httpGet({ + uri: uri.nav, + history: { + state: { viewId }, + title: viewId, + uri: uri.history, + }, }); + $('.dropdown.open .dropdown-toggle').dropdown('toggle'); + return false; + }); + $(document).on( + 'click', + '.navbar-collapse.collapse.in a:not(.dropdown-toggle)', + function () { + $(this).closest('.navbar-collapse').collapse('hide'); + } + ); + $(document).on( + 'click', + '.navbar-collapse.collapse.in button:not(.navbar-toggle)', + function () { + $(this).closest('.navbar-collapse').collapse('hide'); + } + ); - // affix management for js-side-menu - $(document).on('before-show-card', '.js-side-menu', function() { - resizeRightMenus(); - $(this).affix({offset: { top: 0 }}); - }); + // affix management for js-side-menu + $(document).on('before-show-card', '.js-side-menu', function () { + resizeRightMenus(); + $(this).affix({ offset: { top: 0 } }); + }); - $(document).on('after-show-card', '.js-side-menu', function() { - $(this).affix('checkPosition'); - }); + $(document).on('after-show-card', '.js-side-menu', function () { + $(this).affix('checkPosition'); + }); - $(document).on('before-hide-card', '.js-side-menu', function() { - $(window).off('.affix'); - $(this).removeData('bs.affix').removeClass('affix affix-top affix-bottom'); - }); + $(document).on('before-hide-card', '.js-side-menu', function () { + $(window).off('.affix'); + $(this).removeData('bs.affix').removeClass('affix affix-top affix-bottom'); + }); - $(document).on('affix.bs.affix', '.js-side-menu', function() { - const $menu = $(this); - $menu.width($menu.width()); - }); + $(document).on('affix.bs.affix', '.js-side-menu', function () { + const $menu = $(this); + $menu.width($menu.width()); + }); - $(window).resize(() => resizeRightMenus()); + $(window).resize(() => resizeRightMenus()); }; diff --git a/frontend/js/menu_middleware.js b/frontend/js/menu_middleware.js index 3890b49..33ff506 100644 --- a/frontend/js/menu_middleware.js +++ b/frontend/js/menu_middleware.js @@ -1,20 +1,20 @@ import frontexpress from 'frontexpress'; class MenuMiddleware extends frontexpress.Middleware { - entered(req) { - const menus = document.querySelectorAll('li > .js-nav-action'); - for (let i = 0; i < menus.length; i++) { - const menu = menus[i]; - const parentMenu = menu.parentNode; - const re = new RegExp(`^/view/${menu.dataset.id}|^/${menu.dataset.id}`); - if (req.uri.match(re)) { - parentMenu.className = 'active'; - menu.focus(); - } else { - parentMenu.className = ''; - } - } + entered(req) { + const menus = document.querySelectorAll('li > .js-nav-action'); + for (let i = 0; i < menus.length; i++) { + const menu = menus[i]; + const parentMenu = menu.parentNode; + const re = new RegExp(`^/view/${menu.dataset.id}|^/${menu.dataset.id}`); + if (req.uri.match(re)) { + parentMenu.className = 'active'; + menu.focus(); + } else { + parentMenu.className = ''; + } } + } } -export default MenuMiddleware; \ No newline at end of file +export default MenuMiddleware; diff --git a/frontend/js/occupant/contractdocumentsform.js b/frontend/js/occupant/contractdocumentsform.js index ebb0d38..3c2e61f 100755 --- a/frontend/js/occupant/contractdocumentsform.js +++ b/frontend/js/occupant/contractdocumentsform.js @@ -6,110 +6,123 @@ import Form from '../form'; const domSelector = '#contract-documents-form'; class ContractDocumentsForm extends Form { - constructor() { - super({ - domSelector, - uri: '', - manifest: { - 'name_0': { - minlength: 2 - }, - 'expirationDate_0': { - required: { - depends: () => $(domSelector + ' #name_0').val().trim() !== '' - }, - date: true - } - }, - defaultData: { - _id: '', - occupantId: '', - documents: [{name:'', expirationDate:''}] - } - }); - } + constructor() { + super({ + domSelector, + uri: '', + manifest: { + name_0: { + minlength: 2, + }, + expirationDate_0: { + required: { + depends: () => + $(domSelector + ' #name_0') + .val() + .trim() !== '', + }, + date: true, + }, + }, + defaultData: { + _id: '', + occupantId: '', + documents: [{ name: '', expirationDate: '' }], + }, + }); + } - beforeSetData(args) { - const occupant = args[0]; + beforeSetData(args) { + const occupant = args[0]; - this.documentRowCount=0; + this.documentRowCount = 0; - if (occupant.documents) { - occupant.documents.forEach((doc, index) => { - if (doc.expirationDate) { - doc.expirationDate = moment(doc.expirationDate).format('L'); //db formtat to display one - } - if (index !==0) { // Except first one row still exists - this.addDocumentRow(); - } - }); + if (occupant.documents) { + occupant.documents.forEach((doc, index) => { + if (doc.expirationDate) { + doc.expirationDate = moment(doc.expirationDate).format('L'); //db formtat to display one + } + if (index !== 0) { + // Except first one row still exists + this.addDocumentRow(); } + }); } + } - afterSetData(args) { - const occupant = args[0]; + afterSetData(args) { + const occupant = args[0]; - $(domSelector + ' #occupantNameLabel').html(i18next.t('\'s documents', {name:occupant.name})); - } + $(domSelector + ' #occupantNameLabel').html( + i18next.t("'s documents", { name: occupant.name }) + ); + } - onGetData(data) { - if (data.documents) { - data.documents.forEach((doc) => { - if (doc.expirationDate) { - doc.expirationDate = moment(doc.expirationDate, 'L').toDate(); //display format to db one - } - }); + onGetData(data) { + if (data.documents) { + data.documents.forEach((doc) => { + if (doc.expirationDate) { + doc.expirationDate = moment(doc.expirationDate, 'L').toDate(); //display format to db one } - return data; + }); } + return data; + } - onBind() { - // Dynamic property rows - $(domSelector + ' #btn-add-document').click(() => { - this.addDocumentRow(); - return false; - }); + onBind() { + // Dynamic property rows + $(domSelector + ' #btn-add-document').click(() => { + this.addDocumentRow(); + return false; + }); - // Remove dynamic rows - $(domSelector + ' .js-btn-form-remove-row').click(function() { - const $row = $(this).parents('.js-form-row'); - if (!$row.hasClass('js-master-form-row')) { - $row.remove(); - } - else { - $(domSelector + ' #name_0').val(''); - $(domSelector + ' #expirationDate_0').val(''); - } - return false; - }); - $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide(); - } + // Remove dynamic rows + $(domSelector + ' .js-btn-form-remove-row').click(function () { + const $row = $(this).parents('.js-form-row'); + if (!$row.hasClass('js-master-form-row')) { + $row.remove(); + } else { + $(domSelector + ' #name_0').val(''); + $(domSelector + ' #expirationDate_0').val(''); + } + return false; + }); + $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide(); + } - addDocumentRow() { - // Create new property row - this.documentRowCount++; - const $newRow = $(domSelector + ' #documents .js-master-form-row').clone(true).removeClass('js-master-form-row'); - $('.has-error', $newRow).removeClass('has-error'); - $('label.error', $newRow).remove(); - const itemDocumentName = 'name_'+this.documentRowCount; - const itemExpirtationDateName = 'expirationDate_'+this.documentRowCount; - $('#name_0',$newRow).attr('id', itemDocumentName).attr('name', itemDocumentName).val(''); - $('#expirationDate_0',$newRow).attr('id', itemExpirtationDateName).attr('name', itemExpirtationDateName).val(''); - $('.js-btn-form-remove-row',$newRow).show(); - // Add new property row in DOM - $(domSelector + ' #documents').append($newRow); + addDocumentRow() { + // Create new property row + this.documentRowCount++; + const $newRow = $(domSelector + ' #documents .js-master-form-row') + .clone(true) + .removeClass('js-master-form-row'); + $('.has-error', $newRow).removeClass('has-error'); + $('label.error', $newRow).remove(); + const itemDocumentName = 'name_' + this.documentRowCount; + const itemExpirtationDateName = 'expirationDate_' + this.documentRowCount; + $('#name_0', $newRow) + .attr('id', itemDocumentName) + .attr('name', itemDocumentName) + .val(''); + $('#expirationDate_0', $newRow) + .attr('id', itemExpirtationDateName) + .attr('name', itemExpirtationDateName) + .val(''); + $('.js-btn-form-remove-row', $newRow).show(); + // Add new property row in DOM + $(domSelector + ' #documents').append($newRow); - //Add jquery validation rules for new added fields - $('#'+itemDocumentName, $newRow).rules('add', { - required:true, - minlength: 2 - }); + //Add jquery validation rules for new added fields + $('#' + itemDocumentName, $newRow).rules('add', { + required: true, + minlength: 2, + }); - $('#'+itemExpirtationDateName, $newRow).rules('add', { - required: true, - date: true - }); - } + $('#' + itemExpirtationDateName, $newRow).rules('add', { + required: true, + date: true, + }); + } } export default ContractDocumentsForm; diff --git a/frontend/js/occupant/middleware.js b/frontend/js/occupant/middleware.js index 9b8ffce..f401235 100755 --- a/frontend/js/occupant/middleware.js +++ b/frontend/js/occupant/middleware.js @@ -11,270 +11,312 @@ import ContractDocumentsForm from './contractdocumentsform'; const LOCA = application.get('LOCA'); class OccupantMiddelware extends ViewController { + constructor() { + super({ + domViewId: '#view-occupant', + domListId: '#occupants', + defaultMenuId: 'occupants-menu', + listSelectionLabel: 'Selected tenant', + listSelectionMenuId: '#occupants-selection-menu', + urls: { + overview: '/api/occupants/overview', + items: '/api/occupants', + }, + }); + this.form = new OccupantForm(); + this.documentsForm = new ContractDocumentsForm(); + } - constructor() { - super({ - domViewId: '#view-occupant', - domListId: '#occupants', - defaultMenuId: 'occupants-menu', - listSelectionLabel: 'Selected tenant', - listSelectionMenuId: '#occupants-selection-menu', - urls: { - overview: '/api/occupants/overview', - items: '/api/occupants' - } - }); - this.form = new OccupantForm(); - this.documentsForm = new ContractDocumentsForm(); - } - - // callback - onInitTemplate() { - // Handlebars templates - Handlebars.registerPartial('history-rent-row-template', $('#history-rent-row-template').html()); - this.templateHistoryRents = Handlebars.compile($('#history-rents-template').html()); + // callback + onInitTemplate() { + // Handlebars templates + Handlebars.registerPartial( + 'history-rent-row-template', + $('#history-rent-row-template').html() + ); + this.templateHistoryRents = Handlebars.compile( + $('#history-rents-template').html() + ); - Handlebars.registerPartial('occupant-invoice-links-template', $('#occupant-invoice-links-template').html()); - this.templateInvoices = Handlebars.compile($('#occupant-invoices-template').html()); + Handlebars.registerPartial( + 'occupant-invoice-links-template', + $('#occupant-invoice-links-template').html() + ); + this.templateInvoices = Handlebars.compile( + $('#occupant-invoices-template').html() + ); - const $occupantsSelected = $('#view-occupant-selected-list-template'); - if ($occupantsSelected.length >0) { - this.templateSelectedRow = Handlebars.compile($occupantsSelected.html()); - } + const $occupantsSelected = $('#view-occupant-selected-list-template'); + if ($occupantsSelected.length > 0) { + this.templateSelectedRow = Handlebars.compile($occupantsSelected.html()); } + } - _loadPropertyList(callback) { - application.httpGet( - '/api/properties', - (req, res) => { - if (callback) { - const properties = JSON.parse(res.responseText); - callback(properties); - } - } - ); - } + _loadPropertyList(callback) { + application.httpGet('/api/properties', (req, res) => { + if (callback) { + const properties = JSON.parse(res.responseText); + callback(properties); + } + }); + } - onUserAction($action, actionId) { - const selection = this.list.getSelectedData(); + onUserAction($action, actionId) { + const selection = this.list.getSelectedData(); - if (actionId==='list-action-view-occupant') { - this._loadPropertyList((properties) => { - this.form.setData(selection[0], properties); - $('#occupant-form select').attr('readonly', true).attr('disabled', true).addClass('uneditable-input'); - $('#occupant-form input').attr('readonly', true).addClass('uneditable-input'); - $('#occupant-form .btn').addClass('hidden'); - this.openForm('occupant-form', 'occupant-view-menu'); - }); - } - else if (actionId==='list-action-edit-occupant') { - $('#occupant-form select').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#occupant-form input').attr('readonly', false).removeClass('uneditable-input'); - $('#occupant-form .btn').removeClass('hidden'); - this.showMenu('occupant-edit-menu'); - } - else if (actionId==='list-action-add-occupant') { - this.list.unselectAll(); - this._loadPropertyList((properties) => { - this.form.setData(null, properties); - $('#occupant-form select').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#occupant-form input').attr('readonly', false).removeClass('uneditable-input'); - $('#occupant-form .btn').removeClass('hidden'); - this.openForm('occupant-form', 'occupant-edit-menu'); - }); - } - else if (actionId==='list-action-remove-occupant') { - bootbox.confirm(i18next.t('Are you sure to remove this tenant?'), (result) => { - if (!result) { - return; - } - const selectionIds = []; - for (let index=0; index < selection.length; ++index) { - selectionIds.push(selection[index]._id); - } - application.httpDelete( - `/api/occupants/${selectionIds.join()}`, - (req, res) => { - const response = JSON.parse(res.responseText); - if (!response.errors || response.errors.length===0) { - this.list.unselectAll(); - this.loadList(() => { - this.closeForm(); - }); - } else { - bootbox.alert({message: response.errors}); - } - } - ); - }); - } - else if (actionId==='list-action-save-form') { - this.form.submit((data) => { - this.closeForm(() => { - this.loadList(() => { - this.list.select($('.js-list-row#'+data._id), true); - }); + if (actionId === 'list-action-view-occupant') { + this._loadPropertyList((properties) => { + this.form.setData(selection[0], properties); + $('#occupant-form select') + .attr('readonly', true) + .attr('disabled', true) + .addClass('uneditable-input'); + $('#occupant-form input') + .attr('readonly', true) + .addClass('uneditable-input'); + $('#occupant-form .btn').addClass('hidden'); + this.openForm('occupant-form', 'occupant-view-menu'); + }); + } else if (actionId === 'list-action-edit-occupant') { + $('#occupant-form select') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#occupant-form input') + .attr('readonly', false) + .removeClass('uneditable-input'); + $('#occupant-form .btn').removeClass('hidden'); + this.showMenu('occupant-edit-menu'); + } else if (actionId === 'list-action-add-occupant') { + this.list.unselectAll(); + this._loadPropertyList((properties) => { + this.form.setData(null, properties); + $('#occupant-form select') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#occupant-form input') + .attr('readonly', false) + .removeClass('uneditable-input'); + $('#occupant-form .btn').removeClass('hidden'); + this.openForm('occupant-form', 'occupant-edit-menu'); + }); + } else if (actionId === 'list-action-remove-occupant') { + bootbox.confirm( + i18next.t('Are you sure to remove this tenant?'), + (result) => { + if (!result) { + return; + } + const selectionIds = []; + for (let index = 0; index < selection.length; ++index) { + selectionIds.push(selection[index]._id); + } + application.httpDelete( + `/api/occupants/${selectionIds.join()}`, + (req, res) => { + const response = JSON.parse(res.responseText); + if (!response.errors || response.errors.length === 0) { + this.list.unselectAll(); + this.loadList(() => { + this.closeForm(); }); - }); + } else { + bootbox.alert({ message: response.errors }); + } + } + ); } - else if (actionId==='list-action-invoices') { - $('#occupant-invoices').html(''); - this.openForm('invoices'); - application.httpGet( - `/api/rents/occupant/${selection[0]._id}`, - (req, res) => { - const rentsHistory = JSON.parse(res.responseText); - const current = moment(); - let count = 0; - const rents = rentsHistory.rents.reverse().filter((rent) => { - if (moment(`${rent.year}-${rent.month}-01`).isSameOrBefore(current)) { - count++; - return count <= 48; // view last 48 months - } - return false; - }).reduce((result, rent) => { - let foundYears = result.years.filter(year => year.year === rent.year); - if (!foundYears || foundYears.length===0) { - const yearObject = { - occupantId: selection[0]._id, - year: rent.year, - months:[] - }; - result.years.push(yearObject); - foundYears = [yearObject]; - } - foundYears[0].months.push({ - occupantId: selection[0]._id, - year: Number(rent.year), - month: Number(rent.month), - totalToPay: rent.totalToPay, - payment: rent.payment - }); - return result; - }, {years:[]}); - $('#occupant-invoices').html(this.templateInvoices(rents)); + ); + } else if (actionId === 'list-action-save-form') { + this.form.submit((data) => { + this.closeForm(() => { + this.loadList(() => { + this.list.select($('.js-list-row#' + data._id), true); + }); + }); + }); + } else if (actionId === 'list-action-invoices') { + $('#occupant-invoices').html(''); + this.openForm('invoices'); + application.httpGet( + `/api/rents/occupant/${selection[0]._id}`, + (req, res) => { + const rentsHistory = JSON.parse(res.responseText); + const current = moment(); + let count = 0; + const rents = rentsHistory.rents + .reverse() + .filter((rent) => { + if ( + moment(`${rent.year}-${rent.month}-01`).isSameOrBefore(current) + ) { + count++; + return count <= 48; // view last 48 months + } + return false; + }) + .reduce( + (result, rent) => { + let foundYears = result.years.filter( + (year) => year.year === rent.year + ); + if (!foundYears || foundYears.length === 0) { + const yearObject = { + occupantId: selection[0]._id, + year: rent.year, + months: [], + }; + result.years.push(yearObject); + foundYears = [yearObject]; } + foundYears[0].months.push({ + occupantId: selection[0]._id, + year: Number(rent.year), + month: Number(rent.month), + totalToPay: rent.totalToPay, + payment: rent.payment, + }); + return result; + }, + { years: [] } ); + $('#occupant-invoices').html(this.templateInvoices(rents)); } - else if (actionId==='invoice-link') { - const month = $action.data('month'); - const year = $action.data('year'); - const occupantId = $action.data('occupantId'); - let url = `/print/invoice/occupants/${occupantId}/${year}`; - if (month) { - url += `/${month}`; - } - application.openPrintPreview(url); - } - else if (actionId==='list-action-manage-documents') { - this.documentsForm.setData(selection[0]); - this.openForm('contract-documents-form'); - } - else if (actionId==='list-action-save-contract-documents') { - this.documentsForm.submit((data) => { - this.closeForm(() => { - if (data._id) { - application.httpGet( - '/api/occupants/overview', - (req, res) => { - const occupantsOverview = JSON.parse(res.responseText); - const countAll = occupantsOverview.countAll; - const countActive = occupantsOverview.countActive; - const countInactive = occupantsOverview.countInactive; - $('#view-occupant .js-all-filter-label').html('('+countAll+')'); - $('#view-occupant .all-active-filter-label').html('('+countActive+')'); - $('#view-occupant .all-inactive-filter-label').html('('+countInactive+')'); + ); + } else if (actionId === 'invoice-link') { + const month = $action.data('month'); + const year = $action.data('year'); + const occupantId = $action.data('occupantId'); + let url = `/print/invoice/occupants/${occupantId}/${year}`; + if (month) { + url += `/${month}`; + } + application.openPrintPreview(url); + } else if (actionId === 'list-action-manage-documents') { + this.documentsForm.setData(selection[0]); + this.openForm('contract-documents-form'); + } else if (actionId === 'list-action-save-contract-documents') { + this.documentsForm.submit((data) => { + this.closeForm(() => { + if (data._id) { + application.httpGet('/api/occupants/overview', (req, res) => { + const occupantsOverview = JSON.parse(res.responseText); + const countAll = occupantsOverview.countAll; + const countActive = occupantsOverview.countActive; + const countInactive = occupantsOverview.countInactive; + $('#view-occupant .js-all-filter-label').html( + '(' + countAll + ')' + ); + $('#view-occupant .all-active-filter-label').html( + '(' + countActive + ')' + ); + $('#view-occupant .all-inactive-filter-label').html( + '(' + countInactive + ')' + ); - this.list.update(data); - this.list.showAllRows(); - } - ); - } - }); + this.list.update(data); + this.list.showAllRows(); }); - } - else if (actionId==='list-action-rents-history') { - $('#history-rents-table').html(''); - this.openForm('rents-history', null, () => { - application.httpGet( - `/api/rents/occupant/${selection[0]._id}`, - (req, res) => { - const rentsHistory = JSON.parse(res.responseText); - $('#history-rents-table').html(this.templateHistoryRents(rentsHistory)); - this.scrollToElement('#history-rents-table .active'); - } - ); - }); - } - else if (actionId==='list-action-print') { - this.openForm('print-doc-selector'); - } - } - - onInitListener() { - $(document).on('click', '#view-occupant #printofficechecklist', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/checklist/occupants/${selection}`); - return false; + } }); + }); + } else if (actionId === 'list-action-rents-history') { + $('#history-rents-table').html(''); + this.openForm('rents-history', null, () => { + application.httpGet( + `/api/rents/occupant/${selection[0]._id}`, + (req, res) => { + const rentsHistory = JSON.parse(res.responseText); + $('#history-rents-table').html( + this.templateHistoryRents(rentsHistory) + ); + this.scrollToElement('#history-rents-table .active'); + } + ); + }); + } else if (actionId === 'list-action-print') { + this.openForm('print-doc-selector'); + } + } - $(document).on('click', '#view-occupant #printcontract', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/contract/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + onInitListener() { + $(document).on('click', '#view-occupant #printofficechecklist', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview(`/print/checklist/occupants/${selection}`); + return false; + }); - $(document).on('click', '#view-occupant #printcustomcontract', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/customcontract/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-occupant #printcontract', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/contract/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-occupant #printdomcontract', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/domcontract/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-occupant #printcustomcontract', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/customcontract/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-occupant #printguarantycertificate', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/guarantycertificate/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-occupant #printdomcontract', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/domcontract/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-occupant #printguarantypayback', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/guarantypaybackcertificate/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-occupant #printguarantycertificate', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/guarantycertificate/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-occupant #printguarantyrequest', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/guarantyrequest/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-occupant #printguarantypayback', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/guarantypaybackcertificate/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-occupant #printinsurancerequest', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/insurance/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-occupant #printguarantyrequest', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/guarantyrequest/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - } + $(document).on('click', '#view-occupant #printinsurancerequest', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/insurance/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); + } - onDataChanged(callback) { - this.form.bindForm(); - this.documentsForm.bindForm(); - if (callback) { - callback(); - } + onDataChanged(callback) { + this.form.bindForm(); + this.documentsForm.bindForm(); + if (callback) { + callback(); } + } - // overriden - exited() { - this.form.unbindForm(); - this.documentsForm.unbindForm(); - } + // overriden + exited() { + this.form.unbindForm(); + this.documentsForm.unbindForm(); + } } export default OccupantMiddelware; diff --git a/frontend/js/occupant/occupantform.js b/frontend/js/occupant/occupantform.js index 4429a12..c73acce 100755 --- a/frontend/js/occupant/occupantform.js +++ b/frontend/js/occupant/occupantform.js @@ -8,483 +8,572 @@ import Helper from '../lib/helper'; const domSelector = '#occupant-form'; class OccupantForm extends Form { - - constructor() { - super({ - domSelector, - uri: '/api/occupants', - manifest: { - 'isCompany': 'required', - 'manager': { - minlength: 2, - required: true - }, - 'company': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'legalForm': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'siret': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'capital': { - number: true, - min: 0, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'street1': { - required: true, - minlength: 2 - }, - 'zipCode': { - required: true, - minlength: 2 - }, - 'city': { - required: true, - minlength: 2 - }, - 'reference': { - required: true - }, - 'contract': 'required', - 'beginDate': { - required: true, - date: true - }, - 'endDate': { - required: true, - date: true, - maxcontractdate: [domSelector, 'contract', 'beginDate'] - }, - 'terminationDate': { - date: true, - mindate: [{domSelector: domSelector + ' #beginDate'}] - }, - 'guarantyPayback': { - number: true, - min: 0 - }, - 'guaranty': { - number: true, - min: 0 - }, - 'isVat': 'required', - 'vatRatio': { - number: true, - min: 0, - max: 100, - required: { - depends: () => $(domSelector + ' #isVat option:selected').val()==='true' - } - }, - 'discount': { - number: true, - min: 0 - }, - 'propertyId_0': { - required: true - } - }, - defaultData: { - _id: '', - isCompany: false, - company: '', - legalForm: '', - siret: '', - capital: '', - manager: '', - name: '', - street1: '', - street2: '', - zipCode: '', - city: '', - contacts: [{contact:'', email: '', phone: ''}], - contract: '369', - beginDate: '', - endDate: '', - terminationDate: '', - guarantyPayback: '', - properties: [{propertyId:''}], - guaranty: '', - reference: '', - isVat: false, - vatRatio: '', - discount: '' - } + constructor() { + super({ + domSelector, + uri: '/api/occupants', + manifest: { + isCompany: 'required', + manager: { + minlength: 2, + required: true, + }, + company: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + legalForm: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + siret: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + capital: { + number: true, + min: 0, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + street1: { + required: true, + minlength: 2, + }, + zipCode: { + required: true, + minlength: 2, + }, + city: { + required: true, + minlength: 2, + }, + reference: { + required: true, + }, + contract: 'required', + beginDate: { + required: true, + date: true, + }, + endDate: { + required: true, + date: true, + maxcontractdate: [domSelector, 'contract', 'beginDate'], + }, + terminationDate: { + date: true, + mindate: [{ domSelector: domSelector + ' #beginDate' }], + }, + guarantyPayback: { + number: true, + min: 0, + }, + guaranty: { + number: true, + min: 0, + }, + isVat: 'required', + vatRatio: { + number: true, + min: 0, + max: 100, + required: { + depends: () => + $(domSelector + ' #isVat option:selected').val() === 'true', + }, + }, + discount: { + number: true, + min: 0, + }, + propertyId_0: { + required: true, + }, + }, + defaultData: { + _id: '', + isCompany: false, + company: '', + legalForm: '', + siret: '', + capital: '', + manager: '', + name: '', + street1: '', + street2: '', + zipCode: '', + city: '', + contacts: [{ contact: '', email: '', phone: '' }], + contract: '369', + beginDate: '', + endDate: '', + terminationDate: '', + guarantyPayback: '', + properties: [{ propertyId: '' }], + guaranty: '', + reference: '', + isVat: false, + vatRatio: '', + discount: '', + }, + }); + } + + beforeSetData(args) { + var index, property; + var propertyOptions = ''; + var occupant = args[0]; + + this.propertyRowCount = 0; + this.contactRowCount = 0; + + if (occupant) { + if (occupant.beginDate) { + occupant.beginDate = moment(occupant.beginDate, 'DD/MM/YYYY').format( + 'L' + ); //display format date + } + + if (occupant.endDate) { + occupant.endDate = moment(occupant.endDate, 'DD/MM/YYYY').format('L'); //display format date + } + + if (occupant.terminationDate) { + occupant.terminationDate = moment( + occupant.terminationDate, + 'DD/MM/YYYY' + ).format('L'); //display format date + } + + if (occupant.properties) { + occupant.properties.forEach((property, index) => { + if (index !== 0) { + this.addPropertyRow(); + } }); + } + if (occupant.contacts) { + occupant.contacts.forEach((contact, index) => { + if (index !== 0) { + this.addContactRow(); + } + }); + } + if (!occupant.isVat) { + occupant.vatRatio = 0; + } else { + occupant.vatRatio = occupant.vatRatio * 100; + } } - beforeSetData(args) { - var index, property; - var propertyOptions = ''; - var occupant = args[0]; - - this.propertyRowCount=0; - this.contactRowCount=0; - - if (occupant) { - if (occupant.beginDate) { - occupant.beginDate = moment(occupant.beginDate, 'DD/MM/YYYY').format('L'); //display format date - } - - if (occupant.endDate) { - occupant.endDate = moment(occupant.endDate, 'DD/MM/YYYY').format('L'); //display format date - } - - if (occupant.terminationDate) { - occupant.terminationDate = moment(occupant.terminationDate, 'DD/MM/YYYY').format('L'); //display format date - } - - if (occupant.properties) { - occupant.properties.forEach((property, index) => { - if (index !==0) { - this.addPropertyRow(); - } - }); - } - if (occupant.contacts) { - occupant.contacts.forEach((contact, index) => { - if (index !==0) { - this.addContactRow(); - } - }); - } - if (!occupant.isVat) { - occupant.vatRatio = 0; - } - else { - occupant.vatRatio = occupant.vatRatio * 100; - } - } - - // init property combos - this.properties = args[1]; - for (index=0; index { - this.addContactRow(); - this.formHasBeenUpdated(); - return false; - }); - - // Dynamic property rows - $(domSelector + ' #btn-add-property').off('click').click(() => { - this.addPropertyRow(); - this._propertyChanged(); - this._computeRent(); - this.formHasBeenUpdated(); - return false; - }); - - // Remove dynamic rows - $(domSelector + ' .js-btn-form-remove-row').off('click').click(function() { - var $row = $(this).parents('.js-form-row'); - var selectPropertyId = $row.find('select.available-properties').attr('id'); - if (selectPropertyId) { - $('#occupant-form select.available-properties option[data-selectedby='+selectPropertyId+']').attr('data-selectedby', '').attr('disabled', false); - } - $row.remove(); - that._computeRent(); - that.formHasBeenUpdated(); - return false; - }); - - $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide(); + if (data.endDate) { + data.endDate = moment(data.endDate, 'L').format('DD/MM/YYYY'); //display format to db one } - //---------------------------------------- - // Helpers - //---------------------------------------- - _getPropertyById(propertyId) { - if (this.properties) { - for (var index=0; index { + this.addContactRow(); + this.formHasBeenUpdated(); + return false; + }); + + // Dynamic property rows + $(domSelector + ' #btn-add-property') + .off('click') + .click(() => { + this.addPropertyRow(); + this._propertyChanged(); + this._computeRent(); + this.formHasBeenUpdated(); + return false; + }); + + // Remove dynamic rows + $(domSelector + ' .js-btn-form-remove-row') + .off('click') + .click(function () { + var $row = $(this).parents('.js-form-row'); + var selectPropertyId = $row + .find('select.available-properties') + .attr('id'); + if (selectPropertyId) { + $( + '#occupant-form select.available-properties option[data-selectedby=' + + selectPropertyId + + ']' + ) + .attr('data-selectedby', '') + .attr('disabled', false); } - else { - $('#occupant-form .js-company-fields').hide(); - $('#occupant-form .private-fields').show(); - $('#occupant-form #manager-label').html(i18next.t('First and last name')); + $row.remove(); + that._computeRent(); + that.formHasBeenUpdated(); + return false; + }); + + $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide(); + } + + //---------------------------------------- + // Helpers + //---------------------------------------- + _getPropertyById(propertyId) { + if (this.properties) { + for (var index = 0; index < this.properties.length; ++index) { + if (propertyId === this.properties[index]._id) { + return this.properties[index]; } + } } - - _contractChanged(/*$select*/) { - // var selection = $select.find(':selected').val(); - // if (selection === 'custom') { - // $('#occupant-form #endDate').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - // } - // else { - // $('#occupant-form #endDate').attr('readonly', true).attr('disabled', true).addClass('uneditable-input'); - // } + return null; + } + + _companyChanged($select) { + var selection = $select.find(':selected').val(); + if (selection === 'true') { + $('#occupant-form .private-fields').hide(); + $('#occupant-form .js-company-fields').show(); + $('#occupant-form #manager-label').html( + i18next.t('Effective manager (first and last name)') + ); + } else { + $('#occupant-form .js-company-fields').hide(); + $('#occupant-form .private-fields').show(); + $('#occupant-form #manager-label').html(i18next.t('First and last name')); } - - _contractBeginDateChanged($element) { - var beginDate = $element.val(); - var contract = $('#occupant-form #contract').val(); - var contractDuration = moment.duration(9, 'years'); - var momentBegin = moment(beginDate, 'L', true); - var momentEnd; - - if (momentBegin.isValid() && contract !== 'custom') { - momentEnd = moment(momentBegin).add(contractDuration).subtract(1, 'days'); - $('#occupant-form #endDate').val(momentEnd.format('L')); - } + } + + _contractChanged(/*$select*/) { + // var selection = $select.find(':selected').val(); + // if (selection === 'custom') { + // $('#occupant-form #endDate').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); + // } + // else { + // $('#occupant-form #endDate').attr('readonly', true).attr('disabled', true).addClass('uneditable-input'); + // } + } + + _contractBeginDateChanged($element) { + var beginDate = $element.val(); + var contract = $('#occupant-form #contract').val(); + var contractDuration = moment.duration(9, 'years'); + var momentBegin = moment(beginDate, 'L', true); + var momentEnd; + + if (momentBegin.isValid() && contract !== 'custom') { + momentEnd = moment(momentBegin).add(contractDuration).subtract(1, 'days'); + $('#occupant-form #endDate').val(momentEnd.format('L')); } - - _vatChanged($select) { - var selection = $select.find(':selected').val(); - if (selection === 'true') { - $('#occupant-form .js-occupant-form-vatratio').show(); - $('.occupant-form-vat-row').show(); - } - else { - $('#occupant-form .js-occupant-form-vatratio').hide(); - $('.occupant-form-vat-row').hide(); + } + + _vatChanged($select) { + var selection = $select.find(':selected').val(); + if (selection === 'true') { + $('#occupant-form .js-occupant-form-vatratio').show(); + $('.occupant-form-vat-row').show(); + } else { + $('#occupant-form .js-occupant-form-vatratio').hide(); + $('.occupant-form-vat-row').hide(); + } + } + + _propertyChanged() { + $('#occupant-form select.available-properties').each(function () { + var $select = $(this); + var selectId = $select.attr('id'); + var value = $select.find('option:selected').val(); + var $others = $('#occupant-form select.available-properties').not( + $select + ); + + $others.each(function () { + $(this) + .find('[data-selectedby=' + selectId + ']') + .attr('data-selectedby', '') + .attr('disabled', false); + $(this) + .find('option[value=' + value + ']') + .attr('disabled', true) + .attr('data-selectedby', selectId); + }); + }); + } + + _computeRent() { + var data = this.getData(); + var propertyId; + var property; + var rentWithExpenses = 0; + var vat = 0; + var rentWithVat = 0; + var rentWithDiscount = 0; + + if (data.properties) { + for (var i = 0; i < data.properties.length; ++i) { + propertyId = data.properties[i].propertyId; + if (propertyId) { + property = this._getPropertyById(propertyId); + rentWithExpenses += property.priceWithExpense; } + } } - _propertyChanged() { - $('#occupant-form select.available-properties').each(function() { - var $select = $(this); - var selectId = $select.attr('id'); - var value = $select.find('option:selected').val(); - var $others = $('#occupant-form select.available-properties').not($select); - - $others.each(function() { - $(this).find('[data-selectedby='+selectId+']').attr('data-selectedby', '').attr('disabled', false); - $(this).find('option[value='+value+']').attr('disabled', true).attr('data-selectedby', selectId); - }); - }); + rentWithDiscount = rentWithExpenses; + if (rentWithExpenses > 0 && data.discount) { + rentWithDiscount -= data.discount; } - _computeRent() { - var data = this.getData(); - var propertyId; - var property; - var rentWithExpenses = 0; - var vat = 0; - var rentWithVat = 0; - var rentWithDiscount = 0; - - if (data.properties) { - for (var i=0; i0 && data.discount) { - rentWithDiscount -= data.discount; - } - - if (rentWithDiscount<0) { - rentWithDiscount = 0; - } - - if (data.isVat && data.vatRatio) { - vat = rentWithDiscount * data.vatRatio; - } - - rentWithVat = rentWithDiscount + vat; + if (rentWithDiscount < 0) { + rentWithDiscount = 0; + } - $('.js-occupant-form-summary-guaranty').html(Helper.formatMoney(data.guaranty, false, false)); - $('.js-occupant-form-summary-rentwithexpenses').html(Helper.formatMoney(rentWithExpenses, false, false)); - $('.js-occupant-form-summary-discount').html(Helper.formatMoney(data.discount, false, false)); - $('.js-occupant-form-summary-vat').html(Helper.formatMoney(vat, false, false)); - $('.js-occupant-form-summary-total-rentwithexpenses').html(Helper.formatMoney(rentWithDiscount, false, false)); - $('.js-occupant-form-summary-total-rentwithexpensesandvat').html(Helper.formatMoney(rentWithVat, false, false)); + if (data.isVat && data.vatRatio) { + vat = rentWithDiscount * data.vatRatio; } + + rentWithVat = rentWithDiscount + vat; + + $('.js-occupant-form-summary-guaranty').html( + Helper.formatMoney(data.guaranty, false, false) + ); + $('.js-occupant-form-summary-rentwithexpenses').html( + Helper.formatMoney(rentWithExpenses, false, false) + ); + $('.js-occupant-form-summary-discount').html( + Helper.formatMoney(data.discount, false, false) + ); + $('.js-occupant-form-summary-vat').html( + Helper.formatMoney(vat, false, false) + ); + $('.js-occupant-form-summary-total-rentwithexpenses').html( + Helper.formatMoney(rentWithDiscount, false, false) + ); + $('.js-occupant-form-summary-total-rentwithexpensesandvat').html( + Helper.formatMoney(rentWithVat, false, false) + ); + } } export default OccupantForm; diff --git a/frontend/js/owner/middleware.js b/frontend/js/owner/middleware.js index 666b049..7cce6ce 100755 --- a/frontend/js/owner/middleware.js +++ b/frontend/js/owner/middleware.js @@ -5,69 +5,73 @@ import anilayout from '../lib/anilayout'; import OwnerForm from './ownerform'; class OwnerMiddleware extends ViewController { + constructor() { + super({ + domViewId: '#view-owner', + }); + this.form = new OwnerForm(); + this.edited = false; + } - constructor() { - super({ - domViewId: '#view-owner' - }); - this.form = new OwnerForm(); - this.edited = false; - } - - onDataChanged() { - var data; - this.form.bindForm(); - application.httpGet( - '/api/owner', - (req, res) => { - const owner = JSON.parse(res.responseText); - this.form.setData(owner); - data = this.form.getData(); - if (!this.edited) { - if (data._id && data._id !== '') { - this.closeForm(); - } - else { - this.openForm(); - } - } - else { - this.openForm(); - } - } - ); - } - - openForm() { - this.edited = true; - $('#owner-form select').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#owner-form input').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - anilayout.showMenu('owner-form-menu'); - } + onDataChanged() { + var data; + this.form.bindForm(); + application.httpGet('/api/owner', (req, res) => { + const owner = JSON.parse(res.responseText); + this.form.setData(owner); + data = this.form.getData(); + if (!this.edited) { + if (data._id && data._id !== '') { + this.closeForm(); + } else { + this.openForm(); + } + } else { + this.openForm(); + } + }); + } - closeForm() { - this.edited = false; - $('#owner-form select').attr('readonly', true).attr('disabled', true).addClass('uneditable-input'); - $('#owner-form input').attr('readonly', true).attr('disabled', true).addClass('uneditable-input'); - anilayout.showMenu('owner-menu'); - } + openForm() { + this.edited = true; + $('#owner-form select') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#owner-form input') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + anilayout.showMenu('owner-form-menu'); + } - onUserAction($action, actionId) { - if (actionId === 'edit-owner') { - this.openForm(); - } - else if (actionId === 'save-form') { - this.form.submit((/*errors*/) => { - this.closeForm(); - }); - } - } + closeForm() { + this.edited = false; + $('#owner-form select') + .attr('readonly', true) + .attr('disabled', true) + .addClass('uneditable-input'); + $('#owner-form input') + .attr('readonly', true) + .attr('disabled', true) + .addClass('uneditable-input'); + anilayout.showMenu('owner-menu'); + } - // overriden - exited() { - this.form.unbindForm(); + onUserAction($action, actionId) { + if (actionId === 'edit-owner') { + this.openForm(); + } else if (actionId === 'save-form') { + this.form.submit((/*errors*/) => { + this.closeForm(); + }); } + } + // overriden + exited() { + this.form.unbindForm(); + } } export default OwnerMiddleware; diff --git a/frontend/js/owner/ownerform.js b/frontend/js/owner/ownerform.js index a35414e..c534408 100644 --- a/frontend/js/owner/ownerform.js +++ b/frontend/js/owner/ownerform.js @@ -5,157 +5,167 @@ import Form from '../form'; const domSelector = '#owner-form'; class OwnerForm extends Form { + constructor() { + super({ + domSelector, + httpMethod: 'PATCH', + uri: '/api/owner', + manifest: { + isCompany: { + required: true, + }, + manager: { + required: true, + minlength: 2, + }, + company: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + legalForm: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + siret: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + dos: { + minlength: 2, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + capital: { + number: true, + min: 0, + required: { + depends: () => + $(domSelector + ' #isCompany option:selected').val() === + 'company', + }, + }, + street1: { + required: true, + minlength: 2, + }, + zipCode: { + required: true, + minlength: 2, + }, + city: { + required: true, + minlength: 2, + }, + state: { + required: false, + minlength: 2, + }, + country: { + required: true, + minlength: 2, + }, + contact: { + required: true, + minlength: 2, + }, + phone1: { + phoneFR: true, + }, + phone2: { + phoneFR: true, + }, + email: { + required: true, + email: true, + }, + bank: { + minlength: 2, + }, + rib: { + minlength: 2, + }, + }, + defaultData: { + _id: '', + isCompany: false, + company: '', + legalForm: '', + siret: '', + dos: '', + capital: '', + vatNumber: '', + manager: '', + street1: '', + street2: '', + zipCode: '', + city: '', + state: '', + country: '', + contact: '', + phone1: '', + phone2: '', + email: '', + bank: '', + rib: '', + }, + }); + } - constructor() { - super({ - domSelector, - httpMethod: 'PATCH', - uri: '/api/owner', - manifest: { - 'isCompany': { - required: true - }, - 'manager': { - required: true, - minlength: 2 - }, - 'company': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'legalForm': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'siret': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'dos': { - minlength: 2, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'capital': { - number: true, - min: 0, - required: { - depends: () => $(domSelector + ' #isCompany option:selected').val()==='company' - } - }, - 'street1': { - required: true, - minlength: 2 - }, - 'zipCode': { - required: true, - minlength: 2 - }, - 'city': { - required: true, - minlength: 2 - }, - 'state': { - required: false, - minlength: 2 - }, - 'country': { - required: true, - minlength: 2 - }, - 'contact': { - required: true, - minlength: 2 - }, - 'phone1': { - phoneFR: true - }, - 'phone2': { - phoneFR: true - }, - 'email': { - required: true, - email: true - }, - 'bank': { - minlength: 2 - }, - 'rib': { - minlength: 2 - } - }, - defaultData: { - _id: '', - isCompany: false, - company: '', - legalForm: '', - siret: '', - dos: '', - capital: '', - vatNumber: '', - manager: '', - street1: '', - street2: '', - zipCode: '', - city: '', - state: '', - country: '', - contact: '', - phone1: '', - phone2: '', - email: '', - bank: '', - rib: '' - } - }); + onGetData(data) { + if (!data.isCompany) { + data.company = null; + data.legalForm = null; + data.siret = null; + data.capital = null; + data.vatNumber = null; } - onGetData(data) { - if (!data.isCompany) { - data.company = null; - data.legalForm = null; - data.siret = null; - data.capital = null; - data.vatNumber = null; - } + return data; + } - return data; - } - - afterSetData(/*args*/) { - this._companyChanged($(domSelector + ' #isCompany')); - } + afterSetData(/*args*/) { + this._companyChanged($(domSelector + ' #isCompany')); + } - onBind() { - const that = this; - $(domSelector + ' #isCompany').change(function() { - that._companyChanged($(this)); - }); - } + onBind() { + const that = this; + $(domSelector + ' #isCompany').change(function () { + that._companyChanged($(this)); + }); + } - //---------------------------------------- - // Helpers - //---------------------------------------- - _companyChanged($select) { - const selection = $select.find(':selected').val(); - if (selection === 'true') { - $(domSelector + ' .private-fields').hide(); - $(domSelector + ' .js-company-fields').show(); - $(domSelector + ' #manager-label').html(i18next.t('Effective manager (first and last name)')); - } - else { - $(domSelector + ' .js-company-fields').hide(); - $(domSelector + ' .private-fields').show(); - $(domSelector + ' #manager-label').html(i18next.t('First and last name')); - } + //---------------------------------------- + // Helpers + //---------------------------------------- + _companyChanged($select) { + const selection = $select.find(':selected').val(); + if (selection === 'true') { + $(domSelector + ' .private-fields').hide(); + $(domSelector + ' .js-company-fields').show(); + $(domSelector + ' #manager-label').html( + i18next.t('Effective manager (first and last name)') + ); + } else { + $(domSelector + ' .js-company-fields').hide(); + $(domSelector + ' .private-fields').show(); + $(domSelector + ' #manager-label').html(i18next.t('First and last name')); } + } } export default OwnerForm; diff --git a/frontend/js/print.js b/frontend/js/print.js index eea9a86..04042d1 100644 --- a/frontend/js/print.js +++ b/frontend/js/print.js @@ -6,25 +6,28 @@ import language from './language'; const LOCA = application.get('LOCA'); language(LOCA.countryCode, (countryCode) => { - LOCA.countryCode = countryCode; - $('#printbutton').click(function() { - let error = false; - $('[contenteditable]').each(function() { - if ($(this).html().replace(/ /g, '').replace(' ', '').length === 0) { - $(this).addClass('error'); - error = true; - } - else { - $(this).removeClass('error'); - } - }); - if (error) { - window.alert(i18next.t('Fill the empty fields before printing the document')); - } - else { - window.print(); - } + LOCA.countryCode = countryCode; + $('#printbutton').click(function () { + let error = false; + $('[contenteditable]').each(function () { + if ( + $(this) + .html() + .replace(/ /g, '') + .replace(' ', '').length === 0 + ) { + $(this).addClass('error'); + error = true; + } else { + $(this).removeClass('error'); + } }); + if (error) { + window.alert( + i18next.t('Fill the empty fields before printing the document') + ); + } else { + window.print(); + } + }); }); - - diff --git a/frontend/js/property/middleware.js b/frontend/js/property/middleware.js index 6fa3fa9..0ed5d18 100644 --- a/frontend/js/property/middleware.js +++ b/frontend/js/property/middleware.js @@ -7,91 +7,90 @@ import ViewController from '../viewcontroller'; import PropertyForm from './propertyform'; class PropertyMiddleware extends ViewController { - // PropertyCtrl extends Controller - constructor() { - super({ - domViewId: '#view-property', - domListId: '#properties', - defaultMenuId: 'properties-menu', - listSelectionLabel: 'Selected property', - listSelectionMenuId: 'properties-selection-menu', - urls: { - overview: '/api/properties/overview', - items: '/api/properties' - } - }); - this.form = new PropertyForm(); - } + // PropertyCtrl extends Controller + constructor() { + super({ + domViewId: '#view-property', + domListId: '#properties', + defaultMenuId: 'properties-menu', + listSelectionLabel: 'Selected property', + listSelectionMenuId: 'properties-selection-menu', + urls: { + overview: '/api/properties/overview', + items: '/api/properties', + }, + }); + this.form = new PropertyForm(); + } - onInitTemplate() { - // Handlebars templates - var $propertiesSelected = $('#view-property-selected-list-template'); - if ($propertiesSelected.length >0) { - this.templateSelectedRow = Handlebars.compile($propertiesSelected.html()); - } + onInitTemplate() { + // Handlebars templates + var $propertiesSelected = $('#view-property-selected-list-template'); + if ($propertiesSelected.length > 0) { + this.templateSelectedRow = Handlebars.compile($propertiesSelected.html()); } + } - onUserAction($action, actionId) { - var selection = []; - var selectionIds; - + onUserAction($action, actionId) { + var selection = []; + var selectionIds; - selection = this.list.getSelectedData(); + selection = this.list.getSelectedData(); - if (actionId==='list-action-edit-property') { - this.form.setData(selection[0]); - this.openForm('property-form'); - } - else if (actionId==='list-action-add-property') { - this.list.unselectAll(); - this.form.setData(null); - this.openForm('property-form'); - } - else if (actionId==='list-action-remove-property') { - bootbox.confirm(i18next.t('Are you sure to remove this property'), (result) => { - if (!result) { - return; - } - selectionIds = []; - for (var index=0; index < selection.length; ++index) { - selectionIds.push(selection[index]._id); - } - application.httpDelete( - `/api/properties/${selectionIds.join()}`, - (req, res) => { - const response = JSON.parse(res.responseText); - if (!response.errors || response.errors.length===0) { - this.list.unselectAll(); - this.loadList(() => { - this.closeForm(); - }); - } - } - ); - }); - } - else if (actionId==='list-action-save-form') { - this.form.submit((data) => { - this.closeForm(() => { - this.loadList(() => { - this.list.select($('.js-list-row#'+data._id), true); - }); + if (actionId === 'list-action-edit-property') { + this.form.setData(selection[0]); + this.openForm('property-form'); + } else if (actionId === 'list-action-add-property') { + this.list.unselectAll(); + this.form.setData(null); + this.openForm('property-form'); + } else if (actionId === 'list-action-remove-property') { + bootbox.confirm( + i18next.t('Are you sure to remove this property'), + (result) => { + if (!result) { + return; + } + selectionIds = []; + for (var index = 0; index < selection.length; ++index) { + selectionIds.push(selection[index]._id); + } + application.httpDelete( + `/api/properties/${selectionIds.join()}`, + (req, res) => { + const response = JSON.parse(res.responseText); + if (!response.errors || response.errors.length === 0) { + this.list.unselectAll(); + this.loadList(() => { + this.closeForm(); }); - }); + } + } + ); } + ); + } else if (actionId === 'list-action-save-form') { + this.form.submit((data) => { + this.closeForm(() => { + this.loadList(() => { + this.list.select($('.js-list-row#' + data._id), true); + }); + }); + }); } + } - onDataChanged(callback) { - this.form.bindForm(); - if (callback) { - callback(); - } + onDataChanged(callback) { + this.form.bindForm(); + if (callback) { + callback(); } + } - // overriden - exited() { - this.form.unbindForm(); - } + // overriden + exited() { + this.form.unbindForm(); + } } export default PropertyMiddleware; diff --git a/frontend/js/property/propertyform.js b/frontend/js/property/propertyform.js index c40081c..961a3d6 100644 --- a/frontend/js/property/propertyform.js +++ b/frontend/js/property/propertyform.js @@ -6,123 +6,127 @@ import Helper from '../lib/helper'; const domSelector = '#property-form'; class PropertyForm extends Form { - - constructor() { - super({ - domSelector, - uri: '/api/properties', - manifest: { - 'type': { - required: true - }, - 'name': { - required: true, - minlength: 2 - }, - 'surface': { - number: true, - min: 0 - }, - 'phone': { - phoneFR: true - }, - 'price': { - required: true, - number: true, - min: 0 - }, - 'expense': { - required: { - depends: () => { - const type = $(domSelector + ' #type').val(); - return (type === 'office'); - } - }, - number: true, - min: 0 - } + constructor() { + super({ + domSelector, + uri: '/api/properties', + manifest: { + type: { + required: true, + }, + name: { + required: true, + minlength: 2, + }, + surface: { + number: true, + min: 0, + }, + phone: { + phoneFR: true, + }, + price: { + required: true, + number: true, + min: 0, + }, + expense: { + required: { + depends: () => { + const type = $(domSelector + ' #type').val(); + return type === 'office'; }, - defaultData: { - _id: '', - type: 'office', - name: '', - description: '', - surface: '', - phone: '', - building: '', - level: '', - location: '', - price: '', - expense: '' - } - }); - } + }, + number: true, + min: 0, + }, + }, + defaultData: { + _id: '', + type: 'office', + name: '', + description: '', + surface: '', + phone: '', + building: '', + level: '', + location: '', + price: '', + expense: '', + }, + }); + } - beforeSetData(args) { - const property = args[0]; - if (property) { - if (!property.phone) { - property.phone = ''; - } - if (!property.surface) { - property.surface = ''; - } - if (!property.expense) { - property.expense = ''; - } - } + beforeSetData(args) { + const property = args[0]; + if (property) { + if (!property.phone) { + property.phone = ''; + } + if (!property.surface) { + property.surface = ''; + } + if (!property.expense) { + property.expense = ''; + } } + } - afterSetData(args) { - const property = args[0]; + afterSetData(args) { + const property = args[0]; - if (property && property._id) { - $(domSelector + ' #propertyNameLabel').html(property.name); - $('.js-user-action[data-id="list-action-remove-property"]').show(); - } - else { - $(domSelector + ' #propertyNameLabel').html(i18next.t('Property to rent')); - $('.js-user-action[data-id="list-action-remove-property"]').hide(); - } - - this._typeChanged($(domSelector + ' #type')); - this._computeRent(); + if (property && property._id) { + $(domSelector + ' #propertyNameLabel').html(property.name); + $('.js-user-action[data-id="list-action-remove-property"]').show(); + } else { + $(domSelector + ' #propertyNameLabel').html( + i18next.t('Property to rent') + ); + $('.js-user-action[data-id="list-action-remove-property"]').hide(); } - onBind() { - const that = this; + this._typeChanged($(domSelector + ' #type')); + this._computeRent(); + } - $(domSelector + ' #type').change(function () { - that._typeChanged($(this)); - }); + onBind() { + const that = this; - $(domSelector + ' #price').keyup(() => { - this._computeRent(); - }); - } + $(domSelector + ' #type').change(function () { + that._typeChanged($(this)); + }); - //---------------------------------------- - // Helpers - //---------------------------------------- - _typeChanged($select) { - const selection = $select.find(':selected').val(); - if (selection !== 'office') { - $('.property-no-expense').hide(); - } - else { - $('.property-no-expense').show(); - } - } - - _computeRent() { - const data = this.getData(); - const rentWithExpenses = Number(data.price) + Number(data.expense); + $(domSelector + ' #price').keyup(() => { + this._computeRent(); + }); + } - $('#property-form-summary-rent').html(Helper.formatMoney(data.price, false, false)); - $('#property-form-summary-expense').html(Helper.formatMoney(data.expense, false, false)); - $('#property-form-summary-totla-rentwithexpenses').html(Helper.formatMoney(rentWithExpenses, false, false)); + //---------------------------------------- + // Helpers + //---------------------------------------- + _typeChanged($select) { + const selection = $select.find(':selected').val(); + if (selection !== 'office') { + $('.property-no-expense').hide(); + } else { + $('.property-no-expense').show(); } + } + + _computeRent() { + const data = this.getData(); + const rentWithExpenses = Number(data.price) + Number(data.expense); + $('#property-form-summary-rent').html( + Helper.formatMoney(data.price, false, false) + ); + $('#property-form-summary-expense').html( + Helper.formatMoney(data.expense, false, false) + ); + $('#property-form-summary-totla-rentwithexpenses').html( + Helper.formatMoney(rentWithExpenses, false, false) + ); + } } export default PropertyForm; diff --git a/frontend/js/rent/middleware.js b/frontend/js/rent/middleware.js index 89c1e18..c732146 100644 --- a/frontend/js/rent/middleware.js +++ b/frontend/js/rent/middleware.js @@ -11,300 +11,401 @@ import PaymentForm from './paymentform'; const LOCA = application.get('LOCA'); class RentMiddleware extends ViewController { + constructor() { + super({ + domViewId: '#view-rent', + domListId: '#rents', + defaultMenuId: 'rents-menu', + listSelectionLabel: 'Selected rent', + listSelectionMenuId: 'rents-selection-menu', + urls: { + overview: '/api/rents/overview', + items: '/api/rents', + }, + }); + this.form = new PaymentForm(); + } - constructor() { - super({ - domViewId: '#view-rent', - domListId: '#rents', - defaultMenuId: 'rents-menu', - listSelectionLabel: 'Selected rent', - listSelectionMenuId: 'rents-selection-menu', - urls: { - overview: '/api/rents/overview', - items: '/api/rents' - } - }); - this.form = new PaymentForm(); - } - - // overriden - loadList(callback) { - application.httpGet( - `/api/rents/${LOCA.currentYear}/${LOCA.currentMonth}`, - (req, res) => { - const { rents, overview } = JSON.parse(res.responseText); - const countAll = overview.countAll; - const countPaid = overview.countPaid; - const countPartiallyPaid = overview.countPartiallyPaid; - const countNotPaid = overview.countNotPaid; - const totalToPay = overview.totalToPay; - const totalNotPaid = overview.totalNotPaid; - const totalPaid = overview.totalPaid; - $('.js-all-filter-label').html('(' + countAll + ')'); - $('.js-paid-filter-label').html('(' + (countPaid + countPartiallyPaid) + ')'); - $('.js-not-paid-filter-label').html('(' + countNotPaid + ')'); - //$('.partially-js-paid-filter-label').html(countPartiallyPaid); - $('.js-total-topay').html(Helper.formatMoney(totalToPay)); - $('.js-total-notpaid').html(Helper.formatMoney(totalNotPaid)); - $('.js-total-paid').html(Helper.formatMoney(totalPaid)); + // overriden + loadList(callback) { + application.httpGet( + `/api/rents/${LOCA.currentYear}/${LOCA.currentMonth}`, + (req, res) => { + const { rents, overview } = JSON.parse(res.responseText); + const countAll = overview.countAll; + const countPaid = overview.countPaid; + const countPartiallyPaid = overview.countPartiallyPaid; + const countNotPaid = overview.countNotPaid; + const totalToPay = overview.totalToPay; + const totalNotPaid = overview.totalNotPaid; + const totalPaid = overview.totalPaid; + $('.js-all-filter-label').html('(' + countAll + ')'); + $('.js-paid-filter-label').html( + '(' + (countPaid + countPartiallyPaid) + ')' + ); + $('.js-not-paid-filter-label').html('(' + countNotPaid + ')'); + //$('.partially-js-paid-filter-label').html(countPartiallyPaid); + $('.js-total-topay').html(Helper.formatMoney(totalToPay)); + $('.js-total-notpaid').html(Helper.formatMoney(totalNotPaid)); + $('.js-total-paid').html(Helper.formatMoney(totalPaid)); - $('#view-rent .js-filterbar .js-user-action').removeClass('active'); - if (this.filterValue) { - $('#view-rent .js-filterbar .js-user-action[data-value="' + this.filterValue + '"]').addClass('active'); - } else { - $('#view-rent .js-filterbar .js-default-filter.js-user-action').addClass('active'); - } + $('#view-rent .js-filterbar .js-user-action').removeClass('active'); + if (this.filterValue) { + $( + '#view-rent .js-filterbar .js-user-action[data-value="' + + this.filterValue + + '"]' + ).addClass('active'); + } else { + $( + '#view-rent .js-filterbar .js-default-filter.js-user-action' + ).addClass('active'); + } - this.list.init({ rows: rents }); - if (callback) { - callback(); - } - } - ); - } + this.list.init({ rows: rents }); + if (callback) { + callback(); + } + } + ); + } - // callback - onInitTemplate() { - // Handlebars templates - Handlebars.registerPartial('history-rent-row-template', $('#history-rent-row-template').html()); - Handlebars.registerPartial('view-rent-payment-badges-template', $('#view-rent-payment-badges-template').html()); - Handlebars.registerPartial('view-rent-email-status-badges-template', $('#view-rent-email-status-badges-template').html()); + // callback + onInitTemplate() { + // Handlebars templates + Handlebars.registerPartial( + 'history-rent-row-template', + $('#history-rent-row-template').html() + ); + Handlebars.registerPartial( + 'view-rent-payment-badges-template', + $('#view-rent-payment-badges-template').html() + ); + Handlebars.registerPartial( + 'view-rent-email-status-badges-template', + $('#view-rent-email-status-badges-template').html() + ); - this.templateHistoryRents = Handlebars.compile($('#history-rents-template').html()); + this.templateHistoryRents = Handlebars.compile( + $('#history-rents-template').html() + ); - const $rentsSelected = $('#view-rent-selected-list-template'); - if ($rentsSelected.length > 0) { - this.templateSelectedRow = Handlebars.compile($rentsSelected.html()); - } - const $emailStatus = $('#email-status-template'); - if ($emailStatus.length > 0) { - this.emailStatus = Handlebars.compile($emailStatus.html()); - } + const $rentsSelected = $('#view-rent-selected-list-template'); + if ($rentsSelected.length > 0) { + this.templateSelectedRow = Handlebars.compile($rentsSelected.html()); + } + const $emailStatus = $('#email-status-template'); + if ($emailStatus.length > 0) { + this.emailStatus = Handlebars.compile($emailStatus.html()); } + } - // callback - onInitListener() { - $(document).on('click', '#view-rent #emailinvoice', () => { - const tenantIds = this.getSelectedIds(); - bootbox.confirm(i18next.t('Are you sure to send invoices by email?'), (result) => { - if (!result) { - return; - } - application.sendEmail(tenantIds, 'invoice', LOCA.currentYear, LOCA.currentMonth, status => { - bootbox.alert(this.emailStatus({ results: status }), () => { - this.closeForm(() => { - this.loadList(); - }); - }); + // callback + onInitListener() { + $(document).on('click', '#view-rent #emailinvoice', () => { + const tenantIds = this.getSelectedIds(); + bootbox.confirm( + i18next.t('Are you sure to send invoices by email?'), + (result) => { + if (!result) { + return; + } + application.sendEmail( + tenantIds, + 'invoice', + LOCA.currentYear, + LOCA.currentMonth, + (status) => { + bootbox.alert(this.emailStatus({ results: status }), () => { + this.closeForm(() => { + this.loadList(); }); - }); - return false; - }); + }); + } + ); + } + ); + return false; + }); - $(document).on('click', '#view-rent #emailrentcall', () => { - const tenantIds = this.getSelectedIds(); - bootbox.confirm(i18next.t('Are you sure to send rent notices by email?'), (result) => { - if (!result) { - return; - } - application.sendEmail(tenantIds, 'rentcall', LOCA.currentYear, LOCA.currentMonth, results => { - bootbox.alert(this.emailStatus({ results }), () => { - this.closeForm(() => { - this.loadList(); - }); - }); + $(document).on('click', '#view-rent #emailrentcall', () => { + const tenantIds = this.getSelectedIds(); + bootbox.confirm( + i18next.t('Are you sure to send rent notices by email?'), + (result) => { + if (!result) { + return; + } + application.sendEmail( + tenantIds, + 'rentcall', + LOCA.currentYear, + LOCA.currentMonth, + (results) => { + bootbox.alert(this.emailStatus({ results }), () => { + this.closeForm(() => { + this.loadList(); }); - }); - return false; - }); + }); + } + ); + } + ); + return false; + }); - $(document).on('click', '#view-rent #emailrentcallreminder', () => { - const tenantIds = this.getSelectedIds(); - bootbox.confirm(i18next.t('Are you sure to send rent notices by email?'), (result) => { - if (!result) { - return; - } - application.sendEmail(tenantIds, 'rentcall_reminder', LOCA.currentYear, LOCA.currentMonth, status => { - bootbox.alert(this.emailStatus({ results: status }), () => { - this.closeForm(() => { - this.loadList(); - }); - }); + $(document).on('click', '#view-rent #emailrentcallreminder', () => { + const tenantIds = this.getSelectedIds(); + bootbox.confirm( + i18next.t('Are you sure to send rent notices by email?'), + (result) => { + if (!result) { + return; + } + application.sendEmail( + tenantIds, + 'rentcall_reminder', + LOCA.currentYear, + LOCA.currentMonth, + (status) => { + bootbox.alert(this.emailStatus({ results: status }), () => { + this.closeForm(() => { + this.loadList(); }); - }); - return false; - }); + }); + } + ); + } + ); + return false; + }); - $(document).on('click', '#view-rent #emaillastrentcallreminder', () => { - const tenantIds = this.getSelectedIds(); - bootbox.confirm(i18next.t('Are you sure to send rent notices by email?'), (result) => { - if (!result) { - return; - } - application.sendEmail(tenantIds, 'rentcall_last_reminder', LOCA.currentYear, LOCA.currentMonth, status => { - bootbox.alert(this.emailStatus({ results: status }), () => { - this.closeForm(() => { - this.loadList(); - }); - }); + $(document).on('click', '#view-rent #emaillastrentcallreminder', () => { + const tenantIds = this.getSelectedIds(); + bootbox.confirm( + i18next.t('Are you sure to send rent notices by email?'), + (result) => { + if (!result) { + return; + } + application.sendEmail( + tenantIds, + 'rentcall_last_reminder', + LOCA.currentYear, + LOCA.currentMonth, + (status) => { + bootbox.alert(this.emailStatus({ results: status }), () => { + this.closeForm(() => { + this.loadList(); }); - }); - return false; - }); + }); + } + ); + } + ); + return false; + }); - $(document).on('click', '#view-rent #printinvoices', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/invoice/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-rent #printinvoices', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/invoice/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-rent #rentcall', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/rentcall/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-rent #rentcall', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/rentcall/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-rent #recovery1', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/recovery1/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-rent #recovery1', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/recovery1/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-rent #recovery2', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/recovery2/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-rent #recovery2', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/recovery2/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-rent #recovery3', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/recovery3/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-rent #recovery3', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/recovery3/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-rent #paymentorder', () => { - const selection = this.getSelectedIds(); - application.openPrintPreview(`/print/paymentorder/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}`); - return false; - }); + $(document).on('click', '#view-rent #paymentorder', () => { + const selection = this.getSelectedIds(); + application.openPrintPreview( + `/print/paymentorder/occupants/${selection}/${LOCA.currentYear}/${LOCA.currentMonth}` + ); + return false; + }); - $(document).on('click', '#view-rent .js-rent-period', function () { - const $monthPicker = $('#view-rent .js-month-picker'); - if ($monthPicker.is(':visible')) { - $monthPicker.hide(); - } else { - $monthPicker.show(); - } - return false; - }); - } + $(document).on('click', '#view-rent .js-rent-period', function () { + const $monthPicker = $('#view-rent .js-month-picker'); + if ($monthPicker.is(':visible')) { + $monthPicker.hide(); + } else { + $monthPicker.show(); + } + return false; + }); + } - // callback - onUserAction($action, actionId) { - const selection = this.list.getSelectedData(); + // callback + onUserAction($action, actionId) { + const selection = this.list.getSelectedData(); - if (actionId === 'list-action-pay-rent') { - const rent = selection[0]; - this.form.bindForm(); - this.form.setData(rent); - if (rent.occupant.terminated) { - $('#rent-payment-form select').attr('readonly', true).attr('disabled', true).addClass('uneditable-input'); - $('#rent-payment-form input').attr('readonly', true).addClass('uneditable-input'); - $('#rent-payment-form textarea').attr('readonly', true).addClass('uneditable-input'); - this.openForm('pay-rent-form', 'pay-rent-view-menu'); - } else { - this.openForm('pay-rent-form', 'pay-rent-edit-menu'); - } - } - else if (actionId === 'list-action-edit-pay-rent') { - $('#rent-payment-form select').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#rent-payment-form input').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#rent-payment-form textarea').attr('readonly', false).removeClass('uneditable-input'); - this.showMenu('pay-rent-edit-menu'); - } - else if (actionId === 'list-action-edit-occupant') { - $('#occupant-form select').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#occupant-form input').attr('readonly', false).attr('disabled', false).removeClass('uneditable-input'); - $('#occupant-form .btn').removeClass('hidden'); - this.showMenu('pay-rent-edit-menu'); - } - else if (actionId === 'list-action-rents-history') { - $('#history-rents-table').html(''); - this.openForm('rents-history', null, () => { - application.httpGet( - `/api/rents/occupant/${selection[0]._id}`, - (req, res) => { - const rentsHistory = JSON.parse(res.responseText); - $('#history-rents-table').html(this.templateHistoryRents(rentsHistory)); - this.scrollToElement('#history-rents-table .active'); - } - ); - }); - } - else if (actionId === 'list-action-print') { - this.openForm('print-doc-selector'); - } - else if (actionId === 'list-action-email') { - this.openForm('email-doc-selector'); - } - else if (actionId === 'list-action-save-form') { - this.form.submit((data) => { - this.closeForm(() => { - application.httpGet( - `/api/rents/overview/${LOCA.currentYear}/${LOCA.currentMonth}`, - (req, res) => { - const overview = JSON.parse(res.responseText); - const countAll = overview.countAll; - const countPaid = overview.countPaid; - const countPartiallyPaid = overview.countPartiallyPaid; - const countNotPaid = overview.countNotPaid; - const totalToPay = overview.totalToPay; - const totalNotPaid = overview.totalNotPaid; - const totalPaid = overview.totalPaid; - $('.js-all-filter-label').html('(' + countAll + ')'); - $('.js-paid-filter-label').html('(' + (countPaid + countPartiallyPaid) + ')'); - $('.js-not-paid-filter-label').html('(' + countNotPaid + ')'); - //$('.partially-js-paid-filter-label').html(countPartiallyPaid); - $('.js-total-topay').html(Helper.formatMoney(totalToPay)); - $('.js-total-notpaid').html(Helper.formatMoney(totalNotPaid)); - $('.js-total-paid').html(Helper.formatMoney(totalPaid)); + if (actionId === 'list-action-pay-rent') { + const rent = selection[0]; + this.form.bindForm(); + this.form.setData(rent); + if (rent.occupant.terminated) { + $('#rent-payment-form select') + .attr('readonly', true) + .attr('disabled', true) + .addClass('uneditable-input'); + $('#rent-payment-form input') + .attr('readonly', true) + .addClass('uneditable-input'); + $('#rent-payment-form textarea') + .attr('readonly', true) + .addClass('uneditable-input'); + this.openForm('pay-rent-form', 'pay-rent-view-menu'); + } else { + this.openForm('pay-rent-form', 'pay-rent-edit-menu'); + } + } else if (actionId === 'list-action-edit-pay-rent') { + $('#rent-payment-form select') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#rent-payment-form input') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#rent-payment-form textarea') + .attr('readonly', false) + .removeClass('uneditable-input'); + this.showMenu('pay-rent-edit-menu'); + } else if (actionId === 'list-action-edit-occupant') { + $('#occupant-form select') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#occupant-form input') + .attr('readonly', false) + .attr('disabled', false) + .removeClass('uneditable-input'); + $('#occupant-form .btn').removeClass('hidden'); + this.showMenu('pay-rent-edit-menu'); + } else if (actionId === 'list-action-rents-history') { + $('#history-rents-table').html(''); + this.openForm('rents-history', null, () => { + application.httpGet( + `/api/rents/occupant/${selection[0]._id}`, + (req, res) => { + const rentsHistory = JSON.parse(res.responseText); + $('#history-rents-table').html( + this.templateHistoryRents(rentsHistory) + ); + this.scrollToElement('#history-rents-table .active'); + } + ); + }); + } else if (actionId === 'list-action-print') { + this.openForm('print-doc-selector'); + } else if (actionId === 'list-action-email') { + this.openForm('email-doc-selector'); + } else if (actionId === 'list-action-save-form') { + this.form.submit((data) => { + this.closeForm(() => { + application.httpGet( + `/api/rents/overview/${LOCA.currentYear}/${LOCA.currentMonth}`, + (req, res) => { + const overview = JSON.parse(res.responseText); + const countAll = overview.countAll; + const countPaid = overview.countPaid; + const countPartiallyPaid = overview.countPartiallyPaid; + const countNotPaid = overview.countNotPaid; + const totalToPay = overview.totalToPay; + const totalNotPaid = overview.totalNotPaid; + const totalPaid = overview.totalPaid; + $('.js-all-filter-label').html('(' + countAll + ')'); + $('.js-paid-filter-label').html( + '(' + (countPaid + countPartiallyPaid) + ')' + ); + $('.js-not-paid-filter-label').html('(' + countNotPaid + ')'); + //$('.partially-js-paid-filter-label').html(countPartiallyPaid); + $('.js-total-topay').html(Helper.formatMoney(totalToPay)); + $('.js-total-notpaid').html(Helper.formatMoney(totalNotPaid)); + $('.js-total-paid').html(Helper.formatMoney(totalPaid)); - this.list.update(data); - this.list.showAllRows(); - }); - }); - }); - } + this.list.update(data); + this.list.showAllRows(); + } + ); + }); + }); } + } - // callback - onDataChanged() { - const self = this; - const $monthPicker = $('#view-rent .js-month-picker'); - $monthPicker.datepicker({ - language: LOCA.countryCode, - autoclose: true, - format: 'MM yyyy', - startView: 1, - minViewMode: 1 - }); + // callback + onDataChanged() { + const self = this; + const $monthPicker = $('#view-rent .js-month-picker'); + $monthPicker.datepicker({ + language: LOCA.countryCode, + autoclose: true, + format: 'MM yyyy', + startView: 1, + minViewMode: 1, + }); - $monthPicker.datepicker('setDate', moment('01/' + LOCA.currentMonth + '/' + LOCA.currentYear, 'DD/MM/YYYY').toDate()); - $('#view-rent .js-rent-period').html(Helper.formatMonthYear(LOCA.currentMonth, LOCA.currentYear).toUpperCase()); - $monthPicker.on('changeDate', function () { - const selection = moment($(this).datepicker('getDate')); - LOCA.currentYear = selection.get('year'); - LOCA.currentMonth = selection.get('month') + 1; - $monthPicker.hide(); - $('#view-rent .js-rent-period').html(Helper.formatMonthYear(LOCA.currentMonth, LOCA.currentYear).toUpperCase()).show(); - self.loadList(); - }); - } + $monthPicker.datepicker( + 'setDate', + moment( + '01/' + LOCA.currentMonth + '/' + LOCA.currentYear, + 'DD/MM/YYYY' + ).toDate() + ); + $('#view-rent .js-rent-period').html( + Helper.formatMonthYear(LOCA.currentMonth, LOCA.currentYear).toUpperCase() + ); + $monthPicker.on('changeDate', function () { + const selection = moment($(this).datepicker('getDate')); + LOCA.currentYear = selection.get('year'); + LOCA.currentMonth = selection.get('month') + 1; + $monthPicker.hide(); + $('#view-rent .js-rent-period') + .html( + Helper.formatMonthYear( + LOCA.currentMonth, + LOCA.currentYear + ).toUpperCase() + ) + .show(); + self.loadList(); + }); + } - // overriden - exited() { - this.form.unbindForm(); - } + // overriden + exited() { + this.form.unbindForm(); + } } export default RentMiddleware; diff --git a/frontend/js/rent/paymentform.js b/frontend/js/rent/paymentform.js index b1ffb45..ba67cb3 100644 --- a/frontend/js/rent/paymentform.js +++ b/frontend/js/rent/paymentform.js @@ -7,246 +7,322 @@ import Helper from '../lib/helper'; const LOCA = application.get('LOCA'); const domSelector = '#rent-payment-form'; -const minDate = () => moment({ day: 0, month: Number(LOCA.currentMonth) - 1, year: Number(LOCA.currentYear) }).startOf('month'); -const maxDate = () => moment({ day: 0, month: Number(LOCA.currentMonth) - 1, year: Number(LOCA.currentYear) }).endOf('month'); +const minDate = () => + moment({ + day: 0, + month: Number(LOCA.currentMonth) - 1, + year: Number(LOCA.currentYear), + }).startOf('month'); +const maxDate = () => + moment({ + day: 0, + month: Number(LOCA.currentMonth) - 1, + year: Number(LOCA.currentYear), + }).endOf('month'); const period = () => moment.months()[Number(LOCA.currentMonth) - 1]; class PaymentForm extends Form { - - constructor() { - super({ - domSelector, - uri: '/api/rents', - manifest: { - 'paymentAmount_0': { - number: true, - min: 0 - }, - 'paymentDate_0': { - required: { - depends: (/*element*/) => { - const amount = Number($(domSelector + ' #paymentAmount_0').val()); - return amount > 0; - } - }, - date: true, - mindate: [{ - minDate, - message: i18next.t('Only the payment of rent period are authorized. Please enter a date between', { period: period(), minDate: minDate().format('L'), maxDate: maxDate().format('L') }) - }], - maxdate: [{ - maxDate, - message: i18next.t('Only the payment of rent period are authorized. Please enter a date between', { period: period(), minDate: minDate().format('L'), maxDate: maxDate().format('L') }) - }] - }, - 'paymentType_0': { - required: { - depends: (/*element*/) => { - const amount = Number($(domSelector + ' #paymentAmount_0').val()); - return amount > 0; - } - } - }, - 'paymentReference_0': { - required: { - depends: (/*element*/) => { - const amount = Number($(domSelector + ' #paymentAmount_0').val()); - const ref = $(domSelector + ' #paymentType_0').val(); - return amount > 0 && ref && ref !== 'cash'; - } - } - }, - 'extracharge': { - number: true, - min: 0 - }, - 'noteextracharge': { - minlength: 2, - required: { - depends: (/*element*/) => { - const amount = Number($(domSelector + ' #extracharge').val()); - return amount > 0; - } - } - }, - 'promo': { - number: true, - min: 0 - }, - 'notepromo': { - minlength: 2, - required: { - depends: (/*element*/) => { - const amount = Number($(domSelector + ' #promo').val()); - return amount > 0; - } - } + constructor() { + super({ + domSelector, + uri: '/api/rents', + manifest: { + paymentAmount_0: { + number: true, + min: 0, + }, + paymentDate_0: { + required: { + depends: (/*element*/) => { + const amount = Number($(domSelector + ' #paymentAmount_0').val()); + return amount > 0; + }, + }, + date: true, + mindate: [ + { + minDate, + message: i18next.t( + 'Only the payment of rent period are authorized. Please enter a date between', + { + period: period(), + minDate: minDate().format('L'), + maxDate: maxDate().format('L'), } + ), }, - defaultData: { - _id: '', - month: '', - year: '', - payments: [{ - paymentAmount: '', - paymentType: '', - paymentReference: '', - paymentDate: '', - }], - description: '', - extracharge: '', - noteextracharge: '', - promo: '', - notepromo: '' - } - }); - } + ], + maxdate: [ + { + maxDate, + message: i18next.t( + 'Only the payment of rent period are authorized. Please enter a date between', + { + period: period(), + minDate: minDate().format('L'), + maxDate: maxDate().format('L'), + } + ), + }, + ], + }, + paymentType_0: { + required: { + depends: (/*element*/) => { + const amount = Number($(domSelector + ' #paymentAmount_0').val()); + return amount > 0; + }, + }, + }, + paymentReference_0: { + required: { + depends: (/*element*/) => { + const amount = Number($(domSelector + ' #paymentAmount_0').val()); + const ref = $(domSelector + ' #paymentType_0').val(); + return amount > 0 && ref && ref !== 'cash'; + }, + }, + }, + extracharge: { + number: true, + min: 0, + }, + noteextracharge: { + minlength: 2, + required: { + depends: (/*element*/) => { + const amount = Number($(domSelector + ' #extracharge').val()); + return amount > 0; + }, + }, + }, + promo: { + number: true, + min: 0, + }, + notepromo: { + minlength: 2, + required: { + depends: (/*element*/) => { + const amount = Number($(domSelector + ' #promo').val()); + return amount > 0; + }, + }, + }, + }, + defaultData: { + _id: '', + month: '', + year: '', + payments: [ + { + paymentAmount: '', + paymentType: '', + paymentReference: '', + paymentDate: '', + }, + ], + description: '', + extracharge: '', + noteextracharge: '', + promo: '', + notepromo: '', + }, + }); + } - beforeSetData(args) { - const rent = args[0]; - const { occupant } = rent; + beforeSetData(args) { + const rent = args[0]; + const { occupant } = rent; - this.paymentRowCount = 0; + this.paymentRowCount = 0; - if (occupant.terminated) { - $(`${domSelector} .js-lease-state`).removeClass('hidden'); - $(`${domSelector} .js-contract-termination-date`).html(Helper.formatDate(occupant.terminationDate)); - const { mindate, maxdate, ...paymentDate } = this.validator.settings.rules.paymentDate; - this.validator.settings.rules.paymentDate = paymentDate; - } else { - $(`${domSelector} .js-lease-state`).addClass('hidden'); - } + if (occupant.terminated) { + $(`${domSelector} .js-lease-state`).removeClass('hidden'); + $(`${domSelector} .js-contract-termination-date`).html( + Helper.formatDate(occupant.terminationDate) + ); + const { mindate, maxdate, ...paymentDate } = + this.validator.settings.rules.paymentDate; + this.validator.settings.rules.paymentDate = paymentDate; + } else { + $(`${domSelector} .js-lease-state`).addClass('hidden'); + } - if (rent.payments) { - rent.payments = rent.payments.map(({ date, amount, type, reference }) => ({ - paymentDate: date ? moment(date, 'DD/MM/YYYY').format('L') : '', // db format date to local format - paymentAmount: amount, - paymentType: type, - paymentReference: reference - })); - if (rent.payments.length > 1) { - for (let i = 1; i < rent.payments.length; i++) { - this.addPaymentRow(); - } - } + if (rent.payments) { + rent.payments = rent.payments.map( + ({ date, amount, type, reference }) => ({ + paymentDate: date ? moment(date, 'DD/MM/YYYY').format('L') : '', // db format date to local format + paymentAmount: amount, + paymentType: type, + paymentReference: reference, + }) + ); + if (rent.payments.length > 1) { + for (let i = 1; i < rent.payments.length; i++) { + this.addPaymentRow(); } + } + } - if (!rent.promo) { - rent.promo = ''; - } + if (!rent.promo) { + rent.promo = ''; + } - if (!rent.extracharge) { - rent.extracharge = ''; - } + if (!rent.extracharge) { + rent.extracharge = ''; } + } - afterSetData(args) { - const rent = args[0], - paymentPeriod = moment.months()[rent.month - 1] + ' ' + rent.year; + afterSetData(args) { + const rent = args[0], + paymentPeriod = moment.months()[rent.month - 1] + ' ' + rent.year; - $(domSelector + ' #occupantNameLabel').html(rent.occupant.name); - $(domSelector + ' #paymentPeriod').html(paymentPeriod); - } + $(domSelector + ' #occupantNameLabel').html(rent.occupant.name); + $(domSelector + ' #paymentPeriod').html(paymentPeriod); + } - onGetData(data) { - if (data.payments) { - data.payments = data.payments - .filter(({ paymentAmount }) => paymentAmount && Number(paymentAmount) > 0) - .map(({ paymentDate, paymentAmount, paymentType, paymentReference }) => ({ - date: paymentDate ? moment(paymentDate, 'L').format('DD/MM/YYYY') : '', // local format date to db format - amount: Number(paymentAmount), - type: paymentType, - reference: paymentReference - })); - } - return data; + onGetData(data) { + if (data.payments) { + data.payments = data.payments + .filter( + ({ paymentAmount }) => paymentAmount && Number(paymentAmount) > 0 + ) + .map( + ({ paymentDate, paymentAmount, paymentType, paymentReference }) => ({ + date: paymentDate + ? moment(paymentDate, 'L').format('DD/MM/YYYY') + : '', // local format date to db format + amount: Number(paymentAmount), + type: paymentType, + reference: paymentReference, + }) + ); } + return data; + } - onBind() { - const self = this; - // Dynamic payment rows - $(domSelector + ' #btn-add-payment').off('click').click(() => { - this.addPaymentRow(); - this.formHasBeenUpdated(); - return false; - }); - - // Remove dynamic rows - $(domSelector + ' .js-btn-form-remove-row').off('click').click(function() { - var $row = $(this).parents('.js-form-row'); - $row.remove(); - self.formHasBeenUpdated(); - return false; - }); - - $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide(); - } + onBind() { + const self = this; + // Dynamic payment rows + $(domSelector + ' #btn-add-payment') + .off('click') + .click(() => { + this.addPaymentRow(); + this.formHasBeenUpdated(); + return false; + }); - addPaymentRow() { - this.paymentRowCount++; - $(domSelector + ' #payments .js-master-form-row .datepicker').datepicker('destroy'); - const $newRow = $(domSelector + ' #payments .js-master-form-row') - .clone(true) - .removeClass('js-master-form-row'); - $('.has-error', $newRow).removeClass('has-error'); - $('label.error', $newRow).remove(); - const paymentAmount = 'paymentAmount_' + this.paymentRowCount; - const paymentType = 'paymentType_' + this.paymentRowCount; - const paymentDate = 'paymentDate_' + this.paymentRowCount; - const paymentReference = 'paymentReference_' + this.paymentRowCount; - - $('#paymentAmount_0', $newRow).attr('id', paymentAmount).attr('name', paymentAmount).val(''); - $('#paymentType_0', $newRow).attr('id', paymentType).attr('name', paymentType).val(''); - $('#paymentDate_0', $newRow).attr('id', paymentDate).attr('name', paymentDate).val(''); - $('#paymentReference_0', $newRow).attr('id', paymentReference).attr('name', paymentReference).val(''); - $('.js-btn-form-remove-row', $newRow).show(); - - // Add new payment row in DOM - $(domSelector + ' #payments').append($newRow); - - //Add jquery validation rules for new added fields - $('#' + paymentAmount, $newRow).rules('add', { - number: true, - min: 0 - }); - - $('#' + paymentType, $newRow).rules('add', { - required: { - depends: (/*element*/) => { - const amount = Number($(domSelector + ' #' + paymentAmount).val()); - return amount > 0; - } - } - }); + // Remove dynamic rows + $(domSelector + ' .js-btn-form-remove-row') + .off('click') + .click(function () { + var $row = $(this).parents('.js-form-row'); + $row.remove(); + self.formHasBeenUpdated(); + return false; + }); - $('#' + paymentDate, $newRow).rules('add', { - required: { - depends: () => { - const amount = Number($(domSelector + ' #' + paymentAmount).val()); - return amount > 0; - } - }, - date: true, - mindate: [{ - minDate, - message: i18next.t('Only the payment of rent period are authorized. Please enter a date between', { period: period(), minDate: minDate().format('L'), maxDate: maxDate().format('L') }) - }], - maxdate: [{ - maxDate, - message: i18next.t('Only the payment of rent period are authorized. Please enter a date between', { period: period(), minDate: minDate().format('L'), maxDate: maxDate().format('L') }) - }] - }); - - $('#' + paymentReference, $newRow).rules('add', { - required: { - depends: (/*element*/) => { - const ref = $(domSelector + ' #' + paymentType).val(); - const amount = Number($(domSelector + ' #' + paymentAmount).val()); - return amount > 0 && ref && ref !== 'cash'; - } + $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide(); + } + + addPaymentRow() { + this.paymentRowCount++; + $(domSelector + ' #payments .js-master-form-row .datepicker').datepicker( + 'destroy' + ); + const $newRow = $(domSelector + ' #payments .js-master-form-row') + .clone(true) + .removeClass('js-master-form-row'); + $('.has-error', $newRow).removeClass('has-error'); + $('label.error', $newRow).remove(); + const paymentAmount = 'paymentAmount_' + this.paymentRowCount; + const paymentType = 'paymentType_' + this.paymentRowCount; + const paymentDate = 'paymentDate_' + this.paymentRowCount; + const paymentReference = 'paymentReference_' + this.paymentRowCount; + + $('#paymentAmount_0', $newRow) + .attr('id', paymentAmount) + .attr('name', paymentAmount) + .val(''); + $('#paymentType_0', $newRow) + .attr('id', paymentType) + .attr('name', paymentType) + .val(''); + $('#paymentDate_0', $newRow) + .attr('id', paymentDate) + .attr('name', paymentDate) + .val(''); + $('#paymentReference_0', $newRow) + .attr('id', paymentReference) + .attr('name', paymentReference) + .val(''); + $('.js-btn-form-remove-row', $newRow).show(); + + // Add new payment row in DOM + $(domSelector + ' #payments').append($newRow); + + //Add jquery validation rules for new added fields + $('#' + paymentAmount, $newRow).rules('add', { + number: true, + min: 0, + }); + + $('#' + paymentType, $newRow).rules('add', { + required: { + depends: (/*element*/) => { + const amount = Number($(domSelector + ' #' + paymentAmount).val()); + return amount > 0; + }, + }, + }); + + $('#' + paymentDate, $newRow).rules('add', { + required: { + depends: () => { + const amount = Number($(domSelector + ' #' + paymentAmount).val()); + return amount > 0; + }, + }, + date: true, + mindate: [ + { + minDate, + message: i18next.t( + 'Only the payment of rent period are authorized. Please enter a date between', + { + period: period(), + minDate: minDate().format('L'), + maxDate: maxDate().format('L'), } - }); - } + ), + }, + ], + maxdate: [ + { + maxDate, + message: i18next.t( + 'Only the payment of rent period are authorized. Please enter a date between', + { + period: period(), + minDate: minDate().format('L'), + maxDate: maxDate().format('L'), + } + ), + }, + ], + }); + + $('#' + paymentReference, $newRow).rules('add', { + required: { + depends: (/*element*/) => { + const ref = $(domSelector + ' #' + paymentType).val(); + const amount = Number($(domSelector + ' #' + paymentAmount).val()); + return amount > 0 && ref && ref !== 'cash'; + }, + }, + }); + } } export default PaymentForm; diff --git a/frontend/js/selectrealm/middleware.js b/frontend/js/selectrealm/middleware.js index d445e97..f2fd734 100755 --- a/frontend/js/selectrealm/middleware.js +++ b/frontend/js/selectrealm/middleware.js @@ -3,30 +3,26 @@ import application from '../application'; import ViewController from '../viewcontroller'; class SelectRealmMiddleware extends ViewController { + constructor() { + super({ + domViewId: '#view-selectrealm', + }); + } - constructor() { - super({ - domViewId : '#view-selectrealm' - }); - } - - onInitListener() { - $(document).on('click', '.js-realm-action', function() { - const $action = $(this); - const realmId = $action.data('id'); - application.httpGet( - `/api/realms/${realmId}`, - (req, res) => { - const response = JSON.parse(res.responseText); - const location = window.location; - if (response.status === 'success') { - window.location = `${location.origin}/dashboard`; - } - } - ); - return false; - }); - } + onInitListener() { + $(document).on('click', '.js-realm-action', function () { + const $action = $(this); + const realmId = $action.data('id'); + application.httpGet(`/api/realms/${realmId}`, (req, res) => { + const response = JSON.parse(res.responseText); + const location = window.location; + if (response.status === 'success') { + window.location = `${location.origin}/dashboard`; + } + }); + return false; + }); + } } export default SelectRealmMiddleware; diff --git a/frontend/js/selectrealm/realmform.js b/frontend/js/selectrealm/realmform.js index 4266ef4..f1d0ae4 100644 --- a/frontend/js/selectrealm/realmform.js +++ b/frontend/js/selectrealm/realmform.js @@ -1,279 +1,266 @@ import Form from '../form'; class RealmForm extends Form { - // function RealmForm() { - // } - - // RealmForm.prototype.showErrorMessage = function (message) { - // this.$errorMsg.html(message).show(); - // }; - - // RealmForm.prototype.resetData = function() { - // //this.$form.my('reset'); - // }; - - // RealmForm.prototype.setData = function(realm) { - // if (realm) { - // this.$form.my('data', realm); - // } - // }; - - // RealmForm.prototype.bindActions = function() { - // // var $addLink = this.$form.find('a.add'), - // // $removeLinks = this.$form.find('a.remove'); - - // // $addLink.click(function(event) { - // // var $input = $addLink.prev('input'); - // // if ($input.val().isBlank()) { - // // return false; - // // } - // // var $cloneInput = $input.clone(), - // // $removeLink = $('X'); - - // // $input.attr('disabled', true); - // // $cloneInput.val(''); - // // $cloneInput.insertAfter($input); - // // $addLink.insertAfter($cloneInput); - // // $removeLink.insertAfter($input); - - // // return false; - // // }); - - // // $('#parameter-form').on('click', 'a.remove', function(event) { - // // //$removeLinks.on('click', function(event) { - // // var $removeLink = $(this), - // // $input = $removeLink.prev('input'); - // // $input.remove(); - // // $removeLink.remove(); - // // return false; - // // }); - // }; - - // RealmForm.prototype.bind = function(afterSubmitCallback) { - // var that = this; - // this.$form = $('#parameter-form'); - // if (this.$form.length===0) { - // return this.$form; - // } - // this.$errorMsg = this.$form.find('.errormsg'); - // var checkEmail = function(data, val/*, $field*/) { - // var re = /^$|^([a-z\d][-a-z\d_\+\.]*[a-z\d])@([a-z\d][-a-z\d\.]*[a-z\d]\.([a-z]{2,4})|((\d{1,3}\.){3}\d{1,3}))$/i; - // that.$errorMsg.hide(); - // if (!val || val.length === 0) { - // return ''; - // } - // if (!re.test(val)) { - // return 'Adresse E-mail non valide.'; - // } - // return ''; - // }, - // checkMinLength = function(data, val/*, $field*/) { - // that.$errorMsg.hide(); - // if (!val || val.length === 0) { - // return ''; - // } - // if (val.length < 2) { - // return 'Trop court'; - // } - // return ''; - // }, - // checkStrictPositive = function(data, value/*, $field*/) { - // that.$errorMsg.hide(); - // if (!value || value.length === 0) { - // return ''; - // } - // var val = Number(value.replace(',', '.').replace('€/m2', '').replace('m2', '').replace('%', '').replace('€', '')); - // if (isNaN(val)) { - // return 'Entrer un nombre.'; - // } - // if (val <= 0) { - // return 'Le nombre doit être supérieur à zéro.'; - // } - // return ''; - // }, - // checkFrPhone = function(data, val/*, $field*/) { - // that.$errorMsg.hide(); - // if (!val || val.length === 0) { - // return ''; - // } - // var re = /^0\d(\.|\-|\s)(\d{2}(\.|\-|\s)){3}\d{2}$/gi; - // var re2 = /^0\d{9}$/gi; - // if (re.test(val) || re2.test(val)) { - // return ''; - // } - // return 'Numéro non valide (ex : 01 45 22 35 12).'; - // }; - - // this.bindActions(); - // this.$form.my({ - // params: { - // delay: 0 - // }, - // data: { - // name: null, - // administrator: null, - // user1: null, - // user2: null, - // user3: null, - // user4: null, - // user5: null, - // user6: null, - // user7: null, - // user8: null, - // user9: null, - // user10: null, - // renter: null, - // company: null, - // legalForm: null, - // capital: null, - // rcs: null, - // vatNumber: null, - // street1: null, - // street2: null, - // zipCode: null, - // city: null, - // contact: null, - // phone1: null, - // phone2: null, - // email: null - // }, - // ui: { - // '#realm\\.name': { - // bind: 'name' - // }, - // '#realm\\.administrator': { - // bind: 'administrator' - // }, - // '#realm\\.user1': { - // bind: 'user1', - // check: checkEmail - // }, - // '#realm\\.user2': { - // bind: 'user2', - // check: checkEmail - // }, - // '#realm\\.user3': { - // bind: 'user3', - // check: checkEmail - // }, - // '#realm\\.user4': { - // bind: 'user4', - // check: checkEmail - // }, - // '#realm\\.user5': { - // bind: 'user5', - // check: checkEmail - // }, - // '#realm\\.user6': { - // bind: 'user6', - // check: checkEmail - // }, - // '#realm\\.user7': { - // bind: 'user7', - // check: checkEmail - // }, - // '#realm\\.user8': { - // bind: 'user8', - // check: checkEmail - // }, - // '#realm\\.user9': { - // bind: 'user9', - // check: checkEmail - // }, - // '#realm\\.user10': { - // bind: 'user10', - // check: checkEmail - // }, - // '#realm\\.renter': { - // bind: 'renter', - // check: checkMinLength - // }, - // '#realm\\.company': { - // bind: 'company' - // }, - // '#realm\\.legalForm': { - // bind: 'legalForm' - // }, - // '#realm\\.capital': { - // bind: 'capital', - // check: checkStrictPositive - // }, - // '#realm\\.rcs': { - // bind: 'rcs' - // }, - // '#realm\\.vatNumber': { - // bind: 'vatNumber' - // }, - // '#realm\\.street1': { - // bind: 'street1', - // check: checkMinLength - // }, - // '#realm\\.street2': { - // bind: 'street2' - // }, - // '#realm\\.zipCode': { - // bind: 'zipCode', - // check: checkMinLength - // }, - // '#realm\\.city': { - // bind: 'city', - // check: checkMinLength - // }, - // '#realm\\.contact': { - // bind: 'contact', - // check: checkMinLength - // }, - // '#realm\\.phone1': { - // bind: 'phone1', - // check: checkFrPhone - // }, - // '#realm\\.phone2': { - // bind: 'phone2', - // check: checkFrPhone - // }, - // '#realm\\.email': { - // bind: 'email', - // check: checkEmail - // }, - // '#realm\\.bank': { - // bind: 'bank', - // check: checkMinLength - // }, - // '#realm\\.rib': { - // bind: 'rib', - // check: checkMinLength - // } - // } - // }); - - // $('#parameterform-send').click(function() { - // that.$form.my('redraw', false, false); - // if (that.$form.my('valid')) { - // var data = that.$form.my('data'), - // ajaxUrl ='/parameters/update'; - - // that.loca.requester.ajax({ - // type: 'POST', - // url: ajaxUrl, - // dataType: 'json', - // data: data - // }, - // function(response) { - // if (response.errors && response.errors.length > 0) { - // that.showErrorMessage(response.errors.join('
')); - // return; - // } - // afterSubmitCallback(data); - // }, - // function() { - // that.showErrorMessage('Une erreur technique s\'est produite.'); - // }); - // } - // return false; - // }); - - // return this.$form; - // }; + // function RealmForm() { + // } + // RealmForm.prototype.showErrorMessage = function (message) { + // this.$errorMsg.html(message).show(); + // }; + // RealmForm.prototype.resetData = function() { + // //this.$form.my('reset'); + // }; + // RealmForm.prototype.setData = function(realm) { + // if (realm) { + // this.$form.my('data', realm); + // } + // }; + // RealmForm.prototype.bindActions = function() { + // // var $addLink = this.$form.find('a.add'), + // // $removeLinks = this.$form.find('a.remove'); + // // $addLink.click(function(event) { + // // var $input = $addLink.prev('input'); + // // if ($input.val().isBlank()) { + // // return false; + // // } + // // var $cloneInput = $input.clone(), + // // $removeLink = $('X'); + // // $input.attr('disabled', true); + // // $cloneInput.val(''); + // // $cloneInput.insertAfter($input); + // // $addLink.insertAfter($cloneInput); + // // $removeLink.insertAfter($input); + // // return false; + // // }); + // // $('#parameter-form').on('click', 'a.remove', function(event) { + // // //$removeLinks.on('click', function(event) { + // // var $removeLink = $(this), + // // $input = $removeLink.prev('input'); + // // $input.remove(); + // // $removeLink.remove(); + // // return false; + // // }); + // }; + // RealmForm.prototype.bind = function(afterSubmitCallback) { + // var that = this; + // this.$form = $('#parameter-form'); + // if (this.$form.length===0) { + // return this.$form; + // } + // this.$errorMsg = this.$form.find('.errormsg'); + // var checkEmail = function(data, val/*, $field*/) { + // var re = /^$|^([a-z\d][-a-z\d_\+\.]*[a-z\d])@([a-z\d][-a-z\d\.]*[a-z\d]\.([a-z]{2,4})|((\d{1,3}\.){3}\d{1,3}))$/i; + // that.$errorMsg.hide(); + // if (!val || val.length === 0) { + // return ''; + // } + // if (!re.test(val)) { + // return 'Adresse E-mail non valide.'; + // } + // return ''; + // }, + // checkMinLength = function(data, val/*, $field*/) { + // that.$errorMsg.hide(); + // if (!val || val.length === 0) { + // return ''; + // } + // if (val.length < 2) { + // return 'Trop court'; + // } + // return ''; + // }, + // checkStrictPositive = function(data, value/*, $field*/) { + // that.$errorMsg.hide(); + // if (!value || value.length === 0) { + // return ''; + // } + // var val = Number(value.replace(',', '.').replace('€/m2', '').replace('m2', '').replace('%', '').replace('€', '')); + // if (isNaN(val)) { + // return 'Entrer un nombre.'; + // } + // if (val <= 0) { + // return 'Le nombre doit être supérieur à zéro.'; + // } + // return ''; + // }, + // checkFrPhone = function(data, val/*, $field*/) { + // that.$errorMsg.hide(); + // if (!val || val.length === 0) { + // return ''; + // } + // var re = /^0\d(\.|\-|\s)(\d{2}(\.|\-|\s)){3}\d{2}$/gi; + // var re2 = /^0\d{9}$/gi; + // if (re.test(val) || re2.test(val)) { + // return ''; + // } + // return 'Numéro non valide (ex : 01 45 22 35 12).'; + // }; + // this.bindActions(); + // this.$form.my({ + // params: { + // delay: 0 + // }, + // data: { + // name: null, + // administrator: null, + // user1: null, + // user2: null, + // user3: null, + // user4: null, + // user5: null, + // user6: null, + // user7: null, + // user8: null, + // user9: null, + // user10: null, + // renter: null, + // company: null, + // legalForm: null, + // capital: null, + // rcs: null, + // vatNumber: null, + // street1: null, + // street2: null, + // zipCode: null, + // city: null, + // contact: null, + // phone1: null, + // phone2: null, + // email: null + // }, + // ui: { + // '#realm\\.name': { + // bind: 'name' + // }, + // '#realm\\.administrator': { + // bind: 'administrator' + // }, + // '#realm\\.user1': { + // bind: 'user1', + // check: checkEmail + // }, + // '#realm\\.user2': { + // bind: 'user2', + // check: checkEmail + // }, + // '#realm\\.user3': { + // bind: 'user3', + // check: checkEmail + // }, + // '#realm\\.user4': { + // bind: 'user4', + // check: checkEmail + // }, + // '#realm\\.user5': { + // bind: 'user5', + // check: checkEmail + // }, + // '#realm\\.user6': { + // bind: 'user6', + // check: checkEmail + // }, + // '#realm\\.user7': { + // bind: 'user7', + // check: checkEmail + // }, + // '#realm\\.user8': { + // bind: 'user8', + // check: checkEmail + // }, + // '#realm\\.user9': { + // bind: 'user9', + // check: checkEmail + // }, + // '#realm\\.user10': { + // bind: 'user10', + // check: checkEmail + // }, + // '#realm\\.renter': { + // bind: 'renter', + // check: checkMinLength + // }, + // '#realm\\.company': { + // bind: 'company' + // }, + // '#realm\\.legalForm': { + // bind: 'legalForm' + // }, + // '#realm\\.capital': { + // bind: 'capital', + // check: checkStrictPositive + // }, + // '#realm\\.rcs': { + // bind: 'rcs' + // }, + // '#realm\\.vatNumber': { + // bind: 'vatNumber' + // }, + // '#realm\\.street1': { + // bind: 'street1', + // check: checkMinLength + // }, + // '#realm\\.street2': { + // bind: 'street2' + // }, + // '#realm\\.zipCode': { + // bind: 'zipCode', + // check: checkMinLength + // }, + // '#realm\\.city': { + // bind: 'city', + // check: checkMinLength + // }, + // '#realm\\.contact': { + // bind: 'contact', + // check: checkMinLength + // }, + // '#realm\\.phone1': { + // bind: 'phone1', + // check: checkFrPhone + // }, + // '#realm\\.phone2': { + // bind: 'phone2', + // check: checkFrPhone + // }, + // '#realm\\.email': { + // bind: 'email', + // check: checkEmail + // }, + // '#realm\\.bank': { + // bind: 'bank', + // check: checkMinLength + // }, + // '#realm\\.rib': { + // bind: 'rib', + // check: checkMinLength + // } + // } + // }); + // $('#parameterform-send').click(function() { + // that.$form.my('redraw', false, false); + // if (that.$form.my('valid')) { + // var data = that.$form.my('data'), + // ajaxUrl ='/parameters/update'; + // that.loca.requester.ajax({ + // type: 'POST', + // url: ajaxUrl, + // dataType: 'json', + // data: data + // }, + // function(response) { + // if (response.errors && response.errors.length > 0) { + // that.showErrorMessage(response.errors.join('
')); + // return; + // } + // afterSubmitCallback(data); + // }, + // function() { + // that.showErrorMessage('Une erreur technique s\'est produite.'); + // }); + // } + // return false; + // }); + // return this.$form; + // }; } export default RealmForm; diff --git a/frontend/js/signup/middleware.js b/frontend/js/signup/middleware.js index f0dfc5b..e1c3904 100644 --- a/frontend/js/signup/middleware.js +++ b/frontend/js/signup/middleware.js @@ -4,47 +4,46 @@ import SignupForm from './signupform.js'; import frontexpress from 'frontexpress'; class SignupMiddleware extends frontexpress.Middleware { - - constructor() { - super(); - this.form = new SignupForm(); - } - - // overriden - entered() { - $('body').addClass('covered-body'); - $('body > .footer').show(); - } - - updated() { - this.form.bindForm(); - $('#signup-send').click(() => { - this.form.submit((response) => { - let message; - - if (response.status === 'success') { - $('#signup-form').submit(); // Add this to allow browsers to store username and password. Also I do redirect to home page server side look at /signededin - return; - } - - if (response.status === 'missing-field') { - message = i18next.t('Please fill missing fields'); - } else if (response.status === 'signup-email-taken') { - message = i18next.t('This user already exists'); - } else { - message = i18next.t('A technical issue has occurred (-_-\')'); - } - - this.form.showErrorMessage(message); - }); - return false; - }); - } - - // overriden - exited() { - this.form.unbindForm(); - } + constructor() { + super(); + this.form = new SignupForm(); + } + + // overriden + entered() { + $('body').addClass('covered-body'); + $('body > .footer').show(); + } + + updated() { + this.form.bindForm(); + $('#signup-send').click(() => { + this.form.submit((response) => { + let message; + + if (response.status === 'success') { + $('#signup-form').submit(); // Add this to allow browsers to store username and password. Also I do redirect to home page server side look at /signededin + return; + } + + if (response.status === 'missing-field') { + message = i18next.t('Please fill missing fields'); + } else if (response.status === 'signup-email-taken') { + message = i18next.t('This user already exists'); + } else { + message = i18next.t("A technical issue has occurred (-_-')"); + } + + this.form.showErrorMessage(message); + }); + return false; + }); + } + + // overriden + exited() { + this.form.unbindForm(); + } } -export default SignupMiddleware; \ No newline at end of file +export default SignupMiddleware; diff --git a/frontend/js/signup/signupform.js b/frontend/js/signup/signupform.js index 7add0c1..42fd7cf 100755 --- a/frontend/js/signup/signupform.js +++ b/frontend/js/signup/signupform.js @@ -1,29 +1,29 @@ import Form from '../form'; class SignupForm extends Form { - constructor() { - super({ - domSelector: '#signup-form', - httpMethod: 'POST', - uri: '/signup', - manifest: { - 'firstname': { - required: true - }, - 'lastname': { - required: true - }, - 'username': { - required: true, - email: true - }, - 'password': { - required: true - } - }, - alertOnFieldError: false - }); - } + constructor() { + super({ + domSelector: '#signup-form', + httpMethod: 'POST', + uri: '/signup', + manifest: { + firstname: { + required: true, + }, + lastname: { + required: true, + }, + username: { + required: true, + email: true, + }, + password: { + required: true, + }, + }, + alertOnFieldError: false, + }); + } } export default SignupForm; diff --git a/frontend/js/view_middleware.js b/frontend/js/view_middleware.js index 1c60e69..8303033 100644 --- a/frontend/js/view_middleware.js +++ b/frontend/js/view_middleware.js @@ -2,15 +2,15 @@ import $ from 'jquery'; import frontexpress from 'frontexpress'; class ViewMiddleware extends frontexpress.Middleware { - updated(req, res) { - // fill view container - if (res.responseText) { - const $container = $('.js-view-container'); - $container.html(res.responseText); - $container.css('visibility', 'visible'); - $container.css('opacity', 1); - } + updated(req, res) { + // fill view container + if (res.responseText) { + const $container = $('.js-view-container'); + $container.html(res.responseText); + $container.css('visibility', 'visible'); + $container.css('opacity', 1); } + } } -export default ViewMiddleware; \ No newline at end of file +export default ViewMiddleware; diff --git a/frontend/js/viewcontroller.js b/frontend/js/viewcontroller.js index f492a59..c1473ea 100644 --- a/frontend/js/viewcontroller.js +++ b/frontend/js/viewcontroller.js @@ -5,290 +5,333 @@ import anilist from './lib/anilist'; import anilayout from './lib/anilayout'; class ViewController extends BaseViewMiddleware { + constructor(config) { + super(); + this.config = config; + this.filterValue = ''; + if (config.domListId) { + this.list = new anilist( + config.domListId, + config.domViewId + '-list-row-template', + config.domViewId + '-list-content-template' + ); + } + this.initListener(); + } + + initListener() { + // Manage list selection + if (this.list) { + this.list.on('list.selection.changed', (selection) => { + const selectionCount = + selection && selection.length > 0 ? selection.length : 0; + const $selectionCardLabel = $( + this.config.domViewId + ' .js-list-selection-menu-label' + ); - constructor(config) { - super(); - this.config = config; - this.filterValue = ''; - if (config.domListId) { - this.list = new anilist( - config.domListId, - config.domViewId + '-list-row-template', - config.domViewId + '-list-content-template' + if (selectionCount > 0) { + const $monoSelectionActions = $( + this.config.domViewId + ' .js-user-action.js-only-mono-selection' + ); + + if (selectionCount > 1) { + $monoSelectionActions.addClass('disabled'); + $selectionCardLabel.html( + i18next.t(this.config.listSelectionLabel, { + count: selectionCount, + }) + + ' (' + + selectionCount + + ')' ); - } - this.initListener(); - } + } else { + $monoSelectionActions.removeClass('disabled'); + $selectionCardLabel.html( + i18next.t(this.config.listSelectionLabel, { + count: selectionCount, + }) + ); + } - initListener() { - // Manage list selection - if (this.list) { - this.list.on('list.selection.changed', (selection) => { - const selectionCount = (selection && selection.length>0)?selection.length:0; - const $selectionCardLabel = $(this.config.domViewId + ' .js-list-selection-menu-label'); - - if (selectionCount>0) { - const $monoSelectionActions = $(this.config.domViewId + ' .js-user-action.js-only-mono-selection'); - - if (selectionCount>1) { - $monoSelectionActions.addClass('disabled'); - $selectionCardLabel.html(i18next.t(this.config.listSelectionLabel, {count: selectionCount})+' ('+selectionCount+')'); - } - else { - $monoSelectionActions.removeClass('disabled'); - $selectionCardLabel.html(i18next.t(this.config.listSelectionLabel, {count: selectionCount})); - } - - $(this.config.domViewId + ' .js-list-selected-elements').html(this.templateSelectedRow({rows: selection})); - - anilayout.showMenu(this.config.listSelectionMenuId); - } - else { - if (!this.config.defaultMenuId) { - anilayout.hideMenu(); - } else if (!anilayout.isMenuVisible(this.config.defaultMenuId)) { - anilayout.hideMenu(() => { - if (this.config.defaultMenuId) { - anilayout.showMenu(this.config.defaultMenuId); - } - }); - } - } + $(this.config.domViewId + ' .js-list-selected-elements').html( + this.templateSelectedRow({ rows: selection }) + ); + + anilayout.showMenu(this.config.listSelectionMenuId); + } else { + if (!this.config.defaultMenuId) { + anilayout.hideMenu(); + } else if (!anilayout.isMenuVisible(this.config.defaultMenuId)) { + anilayout.hideMenu(() => { + if (this.config.defaultMenuId) { + anilayout.showMenu(this.config.defaultMenuId); + } }); + } } + }); + } - // Manage click on view (form and list) - const self = this; - $(document).on('click', this.config.domViewId + ' .js-user-action', function() { - const $action = $(this), - actionId = $action.data('id'); - - // here manage cancel selection (internal action) - if ($(this).hasClass('js-cancel-selection')) { - if (self.list) { - self.list.unselectAll(); - } - return false; - } - - // here manage cancel on form (internal action) - if ($(this).hasClass('js-cancel-form')) { - self.closeForm(() => { - if (self.list) { - self.list.showAllRows(); - } - }); - return false; - } - if ($action.hasClass('disabled') || - ($action.hasClass('js-only-mono-selection') && self.list && self.list.getSelection().length>1)) { - return false; - } + // Manage click on view (form and list) + const self = this; + $(document).on( + 'click', + this.config.domViewId + ' .js-user-action', + function () { + const $action = $(this), + actionId = $action.data('id'); + + // here manage cancel selection (internal action) + if ($(this).hasClass('js-cancel-selection')) { + if (self.list) { + self.list.unselectAll(); + } + return false; + } + // here manage cancel on form (internal action) + if ($(this).hasClass('js-cancel-form')) { + self.closeForm(() => { if (self.list) { - if (actionId==='list-filter') { - self.list.unselectAll(); - self.filterValue = $action.data('value'); - self.list.filter(self.filterValue); - $(self.config.domViewId + ' .js-filterbar .js-user-action').removeClass('active'); - if (self.filterValue) { - $(self.config.domViewId + ' .js-filterbar .js-user-action[data-value="'+self.filterValue+'"]').addClass('active'); - } else { - $(self.config.domViewId + ' .js-filterbar .js-default-filter.js-user-action').addClass('active'); - } - } - else if (actionId==='remove-item-from-selection') { - if (!$action.parent().hasClass('fixed')) { - self.list.unselect($action.attr('id')); - } - } - else if (self.onUserAction) { - self.onUserAction($action, actionId); - } - } - else if (self.onUserAction) { - self.onUserAction($action, actionId); + self.list.showAllRows(); } - return false; - }); - - if (self.onInitListener) { - self.onInitListener(); + }); + return false; + } + if ( + $action.hasClass('disabled') || + ($action.hasClass('js-only-mono-selection') && + self.list && + self.list.getSelection().length > 1) + ) { + return false; } - } - // overriden - updated() { - super.updated(); - const callbackEx = () => { - if (this.onDataChanged) { - this.onDataChanged(); - } - if (this.config.defaultMenuId) { - anilayout.showMenu(this.config.defaultMenuId); + if (self.list) { + if (actionId === 'list-filter') { + self.list.unselectAll(); + self.filterValue = $action.data('value'); + self.list.filter(self.filterValue); + $( + self.config.domViewId + ' .js-filterbar .js-user-action' + ).removeClass('active'); + if (self.filterValue) { + $( + self.config.domViewId + + ' .js-filterbar .js-user-action[data-value="' + + self.filterValue + + '"]' + ).addClass('active'); + } else { + $( + self.config.domViewId + + ' .js-filterbar .js-default-filter.js-user-action' + ).addClass('active'); } - }; - - if (!this.loaded) { - if (this.onInitTemplate) { - this.onInitTemplate(); + } else if (actionId === 'remove-item-from-selection') { + if (!$action.parent().hasClass('fixed')) { + self.list.unselect($action.attr('id')); } - - if (this.list) { - this.filterValue = $(this.config.domViewId + ' .js-filterbar .active.js-user-action').data('value'); - this.list.setFilterText(this.filterValue); - } - - this.loaded = true; + } else if (self.onUserAction) { + self.onUserAction($action, actionId); + } + } else if (self.onUserAction) { + self.onUserAction($action, actionId); } + return false; + } + ); - if (this.list) { - this.list.bindDom(); - this.loadList(callbackEx); - return; - } - callbackEx(); + if (self.onInitListener) { + self.onInitListener(); } - - loadList(callback) { - application.httpGet( - this.config.urls.overview, - (req, res) => { - const overviewItems = JSON.parse(res.responseText); - const countAll = overviewItems.countAll; - const countFree = overviewItems.countFree | overviewItems.countInactive; - const countBusy = overviewItems.countBusy | overviewItems.countActive; - $(this.config.domViewId + ' .js-all-filter-label').html('('+countAll+')'); - $(this.config.domViewId + ' .all-active-filter-label').html('('+countBusy+')'); - $(this.config.domViewId + ' .all-inactive-filter-label').html('('+countFree+')'); - - $(this.config.domViewId + ' .js-filterbar .js-user-action').removeClass('active'); - if (this.filterValue) { - $(this.config.domViewId + ' .js-filterbar .js-user-action[data-value="'+this.filterValue+'"]').addClass('active'); - } else { - $(this.config.domViewId + ' .js-filterbar .js-default-filter.js-user-action').addClass('active'); - } - - application.httpGet( - this.config.urls.items, - (req, res) => { - const jsonItems = JSON.parse(res.responseText); - if (this.list) { - this.list.init({rows: jsonItems}); - } - if (callback) { - callback(jsonItems); - } - }); - } - ); + } + + // overriden + updated() { + super.updated(); + const callbackEx = () => { + if (this.onDataChanged) { + this.onDataChanged(); + } + if (this.config.defaultMenuId) { + anilayout.showMenu(this.config.defaultMenuId); + } + }; + + if (!this.loaded) { + if (this.onInitTemplate) { + this.onInitTemplate(); + } + + if (this.list) { + this.filterValue = $( + this.config.domViewId + ' .js-filterbar .active.js-user-action' + ).data('value'); + this.list.setFilterText(this.filterValue); + } + + this.loaded = true; } - entered() { - super.entered(); - - if (this.onPageEntered) { - this.onPageEntered(); - } + if (this.list) { + this.list.bindDom(); + this.loadList(callbackEx); + return; } + callbackEx(); + } + + loadList(callback) { + application.httpGet(this.config.urls.overview, (req, res) => { + const overviewItems = JSON.parse(res.responseText); + const countAll = overviewItems.countAll; + const countFree = overviewItems.countFree | overviewItems.countInactive; + const countBusy = overviewItems.countBusy | overviewItems.countActive; + $(this.config.domViewId + ' .js-all-filter-label').html( + '(' + countAll + ')' + ); + $(this.config.domViewId + ' .all-active-filter-label').html( + '(' + countBusy + ')' + ); + $(this.config.domViewId + ' .all-inactive-filter-label').html( + '(' + countFree + ')' + ); + + $(this.config.domViewId + ' .js-filterbar .js-user-action').removeClass( + 'active' + ); + if (this.filterValue) { + $( + this.config.domViewId + + ' .js-filterbar .js-user-action[data-value="' + + this.filterValue + + '"]' + ).addClass('active'); + } else { + $( + this.config.domViewId + + ' .js-filterbar .js-default-filter.js-user-action' + ).addClass('active'); + } + + application.httpGet(this.config.urls.items, (req, res) => { + const jsonItems = JSON.parse(res.responseText); + if (this.list) { + this.list.init({ rows: jsonItems }); + } + if (callback) { + callback(jsonItems); + } + }); + }); + } - pageExited(callback) { - var callbackEx = () => { - if (callback) { - callback(); - } - if (this.onPageExited) { - this.onPageExited(); - } - }; + entered() { + super.entered(); - anilayout.hideMenu(() => { - if (this.list) { - this.list.hideAllRows(() => { - callbackEx(); - }); - } else { - callbackEx(); - } - }); + if (this.onPageEntered) { + this.onPageEntered(); } + } + + pageExited(callback) { + var callbackEx = () => { + if (callback) { + callback(); + } + if (this.onPageExited) { + this.onPageExited(); + } + }; + + anilayout.hideMenu(() => { + if (this.list) { + this.list.hideAllRows(() => { + callbackEx(); + }); + } else { + callbackEx(); + } + }); + } - getSelectedIds() { - const selection = []; + getSelectedIds() { + const selection = []; - if (this.list) { - const rowsSelected = this.list.getSelectedData(); + if (this.list) { + const rowsSelected = this.list.getSelectedData(); - for (let i=0; i { + anilayout.showMenu(menuId ? menuId : formId + '-menu', () => { + if (callback) { + callback(); + } + window.scroll(0, 0); + }); + }; - openForm(formId, menuId, callback) { - const menuCallback = () => { - anilayout.showMenu(menuId ? menuId : formId+'-menu', () => { - if (callback) { - callback(); - } - window.scroll(0, 0); - }); - }; - - this.scrollY = window.scrollY; + this.scrollY = window.scrollY; - anilayout.hideMenu(() => { - if (this.list) { - this.list.hideAllRows(() => { - anilayout.showSheet(formId); - menuCallback(); - }); - } else { - anilayout.showSheet(formId, menuCallback); - menuCallback(); - } + anilayout.hideMenu(() => { + if (this.list) { + this.list.hideAllRows(() => { + anilayout.showSheet(formId); + menuCallback(); }); - } - - closeForm(callback) { - const callbackEx = () => { - if (callback) { - callback(); - } - window.scroll(0, this.scrollY); - }; - anilayout.hideSheet(); - if (this.list && this.list.getSelection().length>0) { - anilayout.hideMenu(() => { - anilayout.showMenu(this.config.listSelectionMenuId); - callbackEx(); - }); - } else { - anilayout.hideMenu(() => { - if (this.config.defaultMenuId) { - anilayout.showMenu(this.config.defaultMenuId); - } - callbackEx(); - }); + } else { + anilayout.showSheet(formId, menuCallback); + menuCallback(); + } + }); + } + + closeForm(callback) { + const callbackEx = () => { + if (callback) { + callback(); + } + window.scroll(0, this.scrollY); + }; + anilayout.hideSheet(); + if (this.list && this.list.getSelection().length > 0) { + anilayout.hideMenu(() => { + anilayout.showMenu(this.config.listSelectionMenuId); + callbackEx(); + }); + } else { + anilayout.hideMenu(() => { + if (this.config.defaultMenuId) { + anilayout.showMenu(this.config.defaultMenuId); } + callbackEx(); + }); } + } - showMenu(dataId, callback) { - anilayout.showMenu(dataId, callback); - } + showMenu(dataId, callback) { + anilayout.showMenu(dataId, callback); + } - hideMenu(callback) { - anilayout.hideMenu(callback); - } + hideMenu(callback) { + anilayout.hideMenu(callback); + } - scrollToElement(selector) { - const $element = $(selector); - if ($element.length > 0) { - const bodyTopPadding = parseInt($('body').css('padding-top'), 10); - window.scroll(0, $element.offset().top - bodyTopPadding); - } + scrollToElement(selector) { + const $element = $(selector); + if ($element.length > 0) { + const bodyTopPadding = parseInt($('body').css('padding-top'), 10); + window.scroll(0, $element.offset().top - bodyTopPadding); } + } } export default ViewController; diff --git a/frontend/js/website/middleware.js b/frontend/js/website/middleware.js index 9a5d22d..674b80e 100755 --- a/frontend/js/website/middleware.js +++ b/frontend/js/website/middleware.js @@ -2,12 +2,11 @@ import $ from 'jquery'; import frontexpress from 'frontexpress'; class WebsiteMiddleware extends frontexpress.Middleware { - - // overriden - entered() { - $('body').addClass('covered-body'); - $('body > .footer').show(); - } + // overriden + entered() { + $('body').addClass('covered-body'); + $('body > .footer').show(); + } } export default WebsiteMiddleware; diff --git a/frontend/less/accounting.less b/frontend/less/accounting.less index 63ef9e8..b4827eb 100644 --- a/frontend/less/accounting.less +++ b/frontend/less/accounting.less @@ -1,41 +1,41 @@ #view-accounting { - .table { - width: inherit; - } + .table { + width: inherit; + } - tbody tr td { - height: 95px; - min-width: 90px; - max-width: 90px; - width: 90px; - &.inactive { - background-color: lighten(@gray-base, 80%); - } + tbody tr td { + height: 95px; + min-width: 90px; + max-width: 90px; + width: 90px; + &.inactive { + background-color: lighten(@gray-base, 80%); } + } - tbody tr td:first-child { - max-width: none; - white-space: nowrap; - } + tbody tr td:first-child { + max-width: none; + white-space: nowrap; + } - #accounting-payments-table-top-hscroll { - overflow-x: scroll; - overflow-y: hidden; - #accounting-payments-fake-table { - height: 1px; // hack to display the h scrollbar - } - } - #accounting-payments-per-year-table.table { - margin-bottom: 0; + #accounting-payments-table-top-hscroll { + overflow-x: scroll; + overflow-y: hidden; + #accounting-payments-fake-table { + height: 1px; // hack to display the h scrollbar } + } + #accounting-payments-per-year-table.table { + margin-bottom: 0; + } - #accounting-payments-table tbody tr td:first-child { - max-width: none; - white-space: nowrap; - } + #accounting-payments-table tbody tr td:first-child { + max-width: none; + white-space: nowrap; + } - small { - font-size: ceil((@font-size-base * 0.90)); - color: @gray-light; - } + small { + font-size: ceil((@font-size-base * 0.9)); + color: @gray-light; + } } diff --git a/frontend/less/bootswatch.less b/frontend/less/bootswatch.less index 3e3e072..303680a 100644 --- a/frontend/less/bootswatch.less +++ b/frontend/less/bootswatch.less @@ -3,7 +3,7 @@ // ----------------------------------------------------- .btn-shadow(@color) { - #gradient > .vertical-three-colors(lighten(@color, 8%), @color, 60%, darken(@color, 4%)); + #gradient > .vertical-three-colors(lighten(@color, 8%), @color, 60%, darken(@color, 4%));; filter: none; border-bottom: 1px solid darken(@color, 10%); } @@ -16,7 +16,6 @@ .box-shadow(0 1px 10px rgba(0, 0, 0, 0.1)); &-default { - .badge { background-color: #fff; color: @navbar-default-bg; @@ -24,7 +23,7 @@ } &-inverse { - #gradient > .vertical-three-colors(lighten(@navbar-inverse-bg, 8%), lighten(@navbar-inverse-bg, 4%), 60%, darken(@navbar-inverse-bg, 2%)); + #gradient > .vertical-three-colors(lighten(@navbar-inverse-bg, 8%), lighten(@navbar-inverse-bg, 4%), 60%, darken(@navbar-inverse-bg, 2%));; filter: none; border-bottom: 1px solid darken(@navbar-inverse-bg, 10%); @@ -41,9 +40,7 @@ } @media (max-width: @grid-float-breakpoint-max) { - .navbar { - .dropdown-header { color: #fff; } @@ -53,7 +50,6 @@ // Buttons ==================================================================== .btn { - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); .caret { @@ -62,7 +58,6 @@ } .btn-default { - .btn-shadow(@btn-default-bg); &:hover { @@ -117,7 +112,6 @@ .panel-warning, .panel-danger, .panel-info { - .panel-heading, .panel-title { color: #fff; diff --git a/frontend/less/card.less b/frontend/less/card.less index 2aa696f..146edaa 100644 --- a/frontend/less/card.less +++ b/frontend/less/card.less @@ -1,8 +1,8 @@ .card { - padding: 20px; + padding: 20px; - background-color: @well-bg; - border-color: @well-border; - border-radius: @border-radius-base; - box-shadow: 0 0 0 1px rgba(0,0,0,.1),0 2px 3px rgba(0,0,0,.2); + background-color: @well-bg; + border-color: @well-border; + border-radius: @border-radius-base; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 2px 3px rgba(0, 0, 0, 0.2); } diff --git a/frontend/less/datepicker.less b/frontend/less/datepicker.less index 93a4f53..786e7ed 100644 --- a/frontend/less/datepicker.less +++ b/frontend/less/datepicker.less @@ -1,8 +1,11 @@ .month-picker, .year-picker { - display: none; - .datepicker-months, .datepicker-years, .datepicker-decades, .datepicker-centuries { - background-color: #fff; - border: 1px solid @well-border; - } -} \ No newline at end of file + display: none; + .datepicker-months, + .datepicker-years, + .datepicker-decades, + .datepicker-centuries { + background-color: #fff; + border: 1px solid @well-border; + } +} diff --git a/frontend/less/form.less b/frontend/less/form.less index 3f7a9d2..e129167 100644 --- a/frontend/less/form.less +++ b/frontend/less/form.less @@ -1,75 +1,84 @@ -.huge { - font-size: @jumbotron-font-size; - height: @jumbotron-font-size * 2; -} - -.form-control[readonly="readonly"] { - color: @text-color; - background-color: @body-bg; - border: none; - box-shadow: none; - cursor: default; - -webkit-appearance: none; - -moz-appearance: none; - &::-ms-expand { - display: none; - } -} - -form { - .form-error { - display: none; - margin-bottom: 15px; - .fa { - color: @brand-warning; - font-size: 20px; - } - } -} - -.form-card { - background-color: @well-bg; - max-width: 500px; - margin: auto; - margin-top: 40px; - .btn { - width: 100%; - } - p { - margin-top: 10px; - } - padding: 10px 0; - box-shadow: none; - .form-group { - margin-bottom: 5px; - &:last-of-type { - margin-bottom: 15px; - } - } - .help-block { - margin-top: 10px; - margin-bottom: 0; - } - - .page-header { - padding: 0 40px 20px 40px; - margin: 0; - h1, h2, h3, h4, h5, h6, - .h1, .h2, .h3, .h4, .h5, .h6 { - color: @text-color; - } - } - form { - padding: 0 50px; - } - -} - -body.covered-body { - .form-card { - color: @text-color; - a { - color: @text-color; - } - } -} +.huge { + font-size: @jumbotron-font-size; + height: @jumbotron-font-size * 2; +} + +.form-control[readonly='readonly'] { + color: @text-color; + background-color: @body-bg; + border: none; + box-shadow: none; + cursor: default; + -webkit-appearance: none; + -moz-appearance: none; + &::-ms-expand { + display: none; + } +} + +form { + .form-error { + display: none; + margin-bottom: 15px; + .fa { + color: @brand-warning; + font-size: 20px; + } + } +} + +.form-card { + background-color: @well-bg; + max-width: 500px; + margin: auto; + margin-top: 40px; + .btn { + width: 100%; + } + p { + margin-top: 10px; + } + padding: 10px 0; + box-shadow: none; + .form-group { + margin-bottom: 5px; + &:last-of-type { + margin-bottom: 15px; + } + } + .help-block { + margin-top: 10px; + margin-bottom: 0; + } + + .page-header { + padding: 0 40px 20px 40px; + margin: 0; + h1, + h2, + h3, + h4, + h5, + h6, + .h1, + .h2, + .h3, + .h4, + .h5, + .h6 { + color: @text-color; + } + } + form { + padding: 0 50px; + } +} + +body.covered-body { + .form-card { + color: @text-color; + a { + color: @text-color; + } + } +} diff --git a/frontend/less/index.less b/frontend/less/index.less index c5d97a1..b624d4f 100644 --- a/frontend/less/index.less +++ b/frontend/less/index.less @@ -1,67 +1,66 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:400,700); -// Core variables and mixins -@import "../../node_modules/bootstrap/less/variables.less"; -@import "../../node_modules/bootstrap/less/mixins.less"; - -// Reset and dependencies -@import "../../node_modules/bootstrap/less/normalize.less"; -//@import "../../node_modules/bootstrap/less/print.less"; -//@import "../../node_modules/bootstrap/less/glyphicons.less"; - -// Core CSS -@import "../../node_modules/bootstrap/less/scaffolding.less"; -@import "../../node_modules/bootstrap/less/type.less"; -//@import "../../node_modules/bootstrap/less/code.less"; -@import "../../node_modules/bootstrap/less/grid.less"; -@import "../../node_modules/bootstrap/less/tables.less"; -@import "../../node_modules/bootstrap/less/forms.less"; -@import "../../node_modules/bootstrap/less/buttons.less"; - -// Components -//@import "../../node_modules/bootstrap/less/component-animations.less"; -@import "../../node_modules/bootstrap/less/dropdowns.less"; -@import "../../node_modules/bootstrap/less/button-groups.less"; -@import "../../node_modules/bootstrap/less/input-groups.less"; -@import "../../node_modules/bootstrap/less/navs.less"; -@import "../../node_modules/bootstrap/less/navbar.less"; -//@import "../../node_modules/bootstrap/less/breadcrumbs.less"; -//@import "../../node_modules/bootstrap/less/pagination.less"; -//@import "../../node_modules/bootstrap/less/pager.less"; -@import "../../node_modules/bootstrap/less/labels.less"; -//@import "../../node_modules/bootstrap/less/badges.less"; -//@import "../../node_modules/bootstrap/less/jumbotron.less"; -//@import "../../node_modules/bootstrap/less/thumbnails.less"; -@import "../../node_modules/bootstrap/less/alerts.less"; -//@import "../../node_modules/bootstrap/less/progress-bars.less"; -//@import "../../node_modules/bootstrap/less/media.less"; -@import "../../node_modules/bootstrap/less/list-group.less"; -@import "../../node_modules/bootstrap/less/panels.less"; -// @import "../../node_modules/bootstrap/less/responsive-embed.less"; -@import "../../node_modules/bootstrap/less/wells.less"; -@import "../../node_modules/bootstrap/less/close.less"; - -// Components w/ JavaScript -@import "../../node_modules/bootstrap/less/modals.less"; -@import "../../node_modules/bootstrap/less/tooltip.less"; -//@import "../../node_modules/bootstrap/less/popovers.less"; -@import "../../node_modules/bootstrap/less/carousel.less"; - -// Utility classes -@import "../../node_modules/bootstrap/less/utilities.less"; -// @import "../../node_modules/bootstrap/less/responsive-utilities.less"; - -@import "variables.less"; -@import "bootswatch.less"; -@import "../../node_modules/bootstrap-datepicker/less/datepicker3.less"; -@import "list.less"; -@import "table.less"; -@import "style.less"; -@import "layout.less"; -@import "menu.less"; -@import "tiles.less"; -@import "form.less"; -@import "card.less"; -@import "datepicker.less"; -@import "accounting.less"; -@import "website.less"; - +@import url(https://fonts.googleapis.com/css?family=Roboto:400,700); +// Core variables and mixins +@import '../../node_modules/bootstrap/less/variables.less'; +@import '../../node_modules/bootstrap/less/mixins.less'; + +// Reset and dependencies +@import '../../node_modules/bootstrap/less/normalize.less'; +//@import "../../node_modules/bootstrap/less/print.less"; +//@import "../../node_modules/bootstrap/less/glyphicons.less"; + +// Core CSS +@import '../../node_modules/bootstrap/less/scaffolding.less'; +@import '../../node_modules/bootstrap/less/type.less'; +//@import "../../node_modules/bootstrap/less/code.less"; +@import '../../node_modules/bootstrap/less/grid.less'; +@import '../../node_modules/bootstrap/less/tables.less'; +@import '../../node_modules/bootstrap/less/forms.less'; +@import '../../node_modules/bootstrap/less/buttons.less'; + +// Components +//@import "../../node_modules/bootstrap/less/component-animations.less"; +@import '../../node_modules/bootstrap/less/dropdowns.less'; +@import '../../node_modules/bootstrap/less/button-groups.less'; +@import '../../node_modules/bootstrap/less/input-groups.less'; +@import '../../node_modules/bootstrap/less/navs.less'; +@import '../../node_modules/bootstrap/less/navbar.less'; +//@import "../../node_modules/bootstrap/less/breadcrumbs.less"; +//@import "../../node_modules/bootstrap/less/pagination.less"; +//@import "../../node_modules/bootstrap/less/pager.less"; +@import '../../node_modules/bootstrap/less/labels.less'; +//@import "../../node_modules/bootstrap/less/badges.less"; +//@import "../../node_modules/bootstrap/less/jumbotron.less"; +//@import "../../node_modules/bootstrap/less/thumbnails.less"; +@import '../../node_modules/bootstrap/less/alerts.less'; +//@import "../../node_modules/bootstrap/less/progress-bars.less"; +//@import "../../node_modules/bootstrap/less/media.less"; +@import '../../node_modules/bootstrap/less/list-group.less'; +@import '../../node_modules/bootstrap/less/panels.less'; +// @import "../../node_modules/bootstrap/less/responsive-embed.less"; +@import '../../node_modules/bootstrap/less/wells.less'; +@import '../../node_modules/bootstrap/less/close.less'; + +// Components w/ JavaScript +@import '../../node_modules/bootstrap/less/modals.less'; +@import '../../node_modules/bootstrap/less/tooltip.less'; +//@import "../../node_modules/bootstrap/less/popovers.less"; +@import '../../node_modules/bootstrap/less/carousel.less'; + +// Utility classes +@import '../../node_modules/bootstrap/less/utilities.less'; +// @import "../../node_modules/bootstrap/less/responsive-utilities.less"; + +@import 'variables.less'; +@import 'bootswatch.less'; +@import '../../node_modules/bootstrap-datepicker/less/datepicker3.less'; +@import 'list.less'; +@import 'table.less'; +@import 'style.less'; +@import 'layout.less'; +@import 'menu.less'; +@import 'tiles.less'; +@import 'form.less'; +@import 'card.less'; +@import 'datepicker.less'; +@import 'accounting.less'; +@import 'website.less'; diff --git a/frontend/less/layout.less b/frontend/less/layout.less index 5c71a7d..ccd4c4d 100644 --- a/frontend/less/layout.less +++ b/frontend/less/layout.less @@ -1,85 +1,94 @@ -html { - position: relative; - min-height: 100%; -} - -body { - overflow-x: hidden; - background-color: @body-bg; - padding-top: 70px; - padding-bottom: 15px; - margin-bottom: @footer-height; - -} -.covered-body { - background: url('/public/images/manhattan.jpg') no-repeat center center fixed; - background-size: cover; - - font-family: "Open Sans",Arial,sans-serif; - .view { - color: #fff; - a { - color: #fff; - } - h1, h2, h3, h4, h5, h6, - .h1, .h2, .h3, .h4, .h5, .h6 { - color: #fff; - } - } -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - height: @footer-height; - color: @navbar-default-color; - background: @navbar-default-bg; - font-size: 11px; - padding: 15px 0; - - a { - color: @navbar-default-link-color; - padding-right: 5px; - &:focus { - outline: 0; - } - } -} - -.sheet { - display: none; -} - -.modal { - z-index: 9999; -} - -// SPECIFIC -#waitwindow { - position: fixed; - height: 60px; - width: 230px; - top: 50%; - left: 50%; - margin-top: -60px/2; - margin-left: -230px/2; - color: #ffffff; - background-color: #f0ad4e; - border-color: #eea236; - - .loading { - padding-left: 10px; - font-size: 18px; - } -} - -@media(max-width: @screen-md) { - body { - padding-top: 0; - overflow-x: auto; - } - .footer { - width: @container-desktop; - } -} +html { + position: relative; + min-height: 100%; +} + +body { + overflow-x: hidden; + background-color: @body-bg; + padding-top: 70px; + padding-bottom: 15px; + margin-bottom: @footer-height; +} +.covered-body { + background: url('/public/images/manhattan.jpg') no-repeat center center fixed; + background-size: cover; + + font-family: 'Open Sans', Arial, sans-serif; + .view { + color: #fff; + a { + color: #fff; + } + h1, + h2, + h3, + h4, + h5, + h6, + .h1, + .h2, + .h3, + .h4, + .h5, + .h6 { + color: #fff; + } + } +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: @footer-height; + color: @navbar-default-color; + background: @navbar-default-bg; + font-size: 11px; + padding: 15px 0; + + a { + color: @navbar-default-link-color; + padding-right: 5px; + &:focus { + outline: 0; + } + } +} + +.sheet { + display: none; +} + +.modal { + z-index: 9999; +} + +// SPECIFIC +#waitwindow { + position: fixed; + height: 60px; + width: 230px; + top: 50%; + left: 50%; + margin-top: -60px/2; + margin-left: -230px/2; + color: #ffffff; + background-color: #f0ad4e; + border-color: #eea236; + + .loading { + padding-left: 10px; + font-size: 18px; + } +} + +@media (max-width: @screen-md) { + body { + padding-top: 0; + overflow-x: auto; + } + .footer { + width: @container-desktop; + } +} diff --git a/frontend/less/list.less b/frontend/less/list.less index e4149d1..b57eec6 100644 --- a/frontend/less/list.less +++ b/frontend/less/list.less @@ -1,147 +1,149 @@ -.list { - position: relative; -} - -.list-row { - display: table; - position: relative; - - width: 100%; - - padding: 5px 20px; - white-space: nowrap; - - .list-col { - display: table-cell; - vertical-align: middle; - width: 33.3333%; - } - - .list-col:first-child { - width: 66.6666%; - } - - .label { - font-size: ceil((@font-size-small * 0.8)); - } - - &:not(.fixed) { - cursor: pointer; - } - - &:not(:last-child) { - margin-bottom: 20px; - } - - .list-selection-overlay { - position: absolute; - display: none; - top: 0; - left: 0; - height: 100%; - width: 100%; - font-size: 40px; - border-radius: @border-radius-base; - border: solid 1px @well-border; - - &::before { - content: " "; - display: block; - height: 100%; - width: 100%; - } - - .fa { - display: none; - position: absolute; - color: @link-color; - height: 40px; - top: 50%; - transform: translateY(-50%); - left: 20px; - } - } - - &.active:not(.fixed) { - color: white; - .list-payment-price { - color: @text-color; - - } - .text-success, .text-danger, .text-warning { - color: #fff; - } - - .label, .list-payment-price { - color: #fff; - border: 1px solid #fff; - background-color: transparent; - } - - .list-avatar-col .fa { - color: white; - } - - background-color: lighten(@navbar-default-link-active-bg, 25%); - } - a, a:hover { - text-transform: lowercase; - color: @text-color; - text-decoration: none; - } - - .list-comment-link { - position: absolute; - right:2px; - top : 0px; - } - .list-title { - padding-top: 15px; - font-size: @font-size-large; - white-space: normal; - } - .list-avatar-col { - position: relative; - padding: 10px 20px 10px 0; - .fa { - color: @link-color; - } - .fa-inverse { - color: @text-color; - } - .icon-text { - color: @link-color; - font-size: 20px; - margin-top: 4px; - } - - } - .list-label, - .list-header-label { - font-size: @font-size-small; - } - .list-header-label { - padding-top: @font-size-large - @font-size-small; - } - .list-payment-price { - font-size: @font-size-large; - font-weight: bold; - background-color: lighten(@well-border, 40%); - border: solid 1px @well-border; - border-radius: @border-radius-base; - margin-top: 10px; - margin-bottom: 10px; - padding: 5px 4px; - width: 100%; - } - .list-balance-price .price-symbol { - font-size: ceil((@font-size-small * 0.65)); - } - .list-inline { - white-space: normal; - li { - padding-right: 0; - padding-bottom: 5px; - } - } -} +.list { + position: relative; +} + +.list-row { + display: table; + position: relative; + + width: 100%; + + padding: 5px 20px; + white-space: nowrap; + + .list-col { + display: table-cell; + vertical-align: middle; + width: 33.3333%; + } + + .list-col:first-child { + width: 66.6666%; + } + + .label { + font-size: ceil((@font-size-small * 0.8)); + } + + &:not(.fixed) { + cursor: pointer; + } + + &:not(:last-child) { + margin-bottom: 20px; + } + + .list-selection-overlay { + position: absolute; + display: none; + top: 0; + left: 0; + height: 100%; + width: 100%; + font-size: 40px; + border-radius: @border-radius-base; + border: solid 1px @well-border; + + &::before { + content: ' '; + display: block; + height: 100%; + width: 100%; + } + + .fa { + display: none; + position: absolute; + color: @link-color; + height: 40px; + top: 50%; + transform: translateY(-50%); + left: 20px; + } + } + + &.active:not(.fixed) { + color: white; + .list-payment-price { + color: @text-color; + } + .text-success, + .text-danger, + .text-warning { + color: #fff; + } + + .label, + .list-payment-price { + color: #fff; + border: 1px solid #fff; + background-color: transparent; + } + + .list-avatar-col .fa { + color: white; + } + + background-color: lighten(@navbar-default-link-active-bg, 25%); + } + a, + a:hover { + text-transform: lowercase; + color: @text-color; + text-decoration: none; + } + + .list-comment-link { + position: absolute; + right: 2px; + top: 0px; + } + .list-title { + padding-top: 15px; + font-size: @font-size-large; + white-space: normal; + } + .list-avatar-col { + position: relative; + padding: 10px 20px 10px 0; + .fa { + color: @link-color; + } + .fa-inverse { + color: @text-color; + } + .icon-text { + color: @link-color; + font-size: 20px; + margin-top: 4px; + } + } + .list-label, + .list-header-label { + font-size: @font-size-small; + } + .list-header-label { + padding-top: @font-size-large - @font-size-small; + } + .list-payment-price { + font-size: @font-size-large; + font-weight: bold; + background-color: lighten(@well-border, 40%); + border: solid 1px @well-border; + border-radius: @border-radius-base; + margin-top: 10px; + margin-bottom: 10px; + padding: 5px 4px; + width: 100%; + } + .list-balance-price .price-symbol { + font-size: ceil((@font-size-small * 0.65)); + } + .list-inline { + white-space: normal; + li { + padding-right: 0; + padding-bottom: 5px; + } + } +} diff --git a/frontend/less/menu.less b/frontend/less/menu.less index 1ba6487..abf353d 100644 --- a/frontend/less/menu.less +++ b/frontend/less/menu.less @@ -1,51 +1,51 @@ .side-menu { - display: none; - position: fixed; + display: none; + position: fixed; - .row { - margin: 0; - } - .page-header { - margin-top: 0; - } - .btn { - text-align: left; - } + .row { + margin: 0; + } + .page-header { + margin-top: 0; + } + .btn { + text-align: left; + } - .list-group { - max-height: 350px; - overflow-y: auto; - margin-bottom: 0; - } + .list-group { + max-height: 350px; + overflow-y: auto; + margin-bottom: 0; + } - .list-group.list-group-selection { - .list-group-item { - padding: 2px 10px 0 10px; - .fa-times { - cursor: pointer; - } - } - } + .list-group.list-group-selection { + .list-group-item { + padding: 2px 10px 0 10px; + .fa-times { + cursor: pointer; + } + } + } - .list-group.list-group-selection { - &.fixed .list-group-item { - .fa-times { - display: none; - } - } + .list-group.list-group-selection { + &.fixed .list-group-item { + .fa-times { + display: none; + } } + } - .panel:last-of-type { - margin-bottom: 0; - } + .panel:last-of-type { + margin-bottom: 0; + } } -@media(max-width: @screen-md) { - .navbar-fixed-top { - position: relative; - width: @container-desktop; - } - .side-menu { - position: relative; - } -} \ No newline at end of file +@media (max-width: @screen-md) { + .navbar-fixed-top { + position: relative; + width: @container-desktop; + } + .side-menu { + position: relative; + } +} diff --git a/frontend/less/print.less b/frontend/less/print.less index 40b117b..d4f0169 100644 --- a/frontend/less/print.less +++ b/frontend/less/print.less @@ -1,369 +1,371 @@ -@logo-font-size: 18pt; -@default-font-size: 10pt; -@normal-font-size: 11.5pt; -@large-font-size: 14pt; -@xlarge-font-size: 15.5pt; -@small-font-size: 9pt; -@xsmall-font-size: 8pt; - -@margin: 10mm; -@thin-border: 1pt solid #555; -@border: 2pt solid #555; -@large-border: 2.8pt solid #555; - -body { - margin: 0; - padding: 0; - font: @default-font-size "Tahoma"; -} -* { - box-sizing: border-box; - -moz-box-sizing: border-box; -} -h1, h2, h3 { - margin: 0; - padding: 0; -} -table { - border: none; - border-collapse: collapse; -} - -span, -span:focus, -span:active{ - border: 2px solid #ccc; - outline: none; - padding: 2px; - background-color: #FFFFE0; -} -span.error { - border: 2px solid red; -} -.page { - position: relative; - width: 210mm; - height: 296mm; - border: 1px solid #ccc; - page-break-inside: avoid; -} - -.logo { - position: absolute; - top: @margin; - left: @margin; - h1 { - display: inline-block; - font-size: @logo-font-size; - color: #330066; - } - img { - display: inline-block; - } -} -.contact { - border-top : @border; - margin: 2pt 0 0 0; - padding: 5pt 0 0 0; -} -.customer { - position: absolute; - top: @margin + 35mm; - left: @margin + 95mm; - - h3{ - margin-bottom: 5pt; - } -} - -.object { - position: absolute; - top: @margin + 95mm; - left: @margin; -} - -.certificate .object { - position: absolute; - top: @margin + 95mm; - left: @margin; - right: @margin; - padding: 5pt; - border: @border; - text-align: center; - text-transform: uppercase; - font-size: @xlarge-font-size; - font-weight: bold; -} - -.contract { - .object { - position: absolute; - top: @margin + 65mm; - left: @margin; - right: @margin; - padding: 15pt; - border: @border; - text-align: center; - text-transform: uppercase; - font-size: @xlarge-font-size; - font-weight: bold; - background-color: #ccc; - small { - font-weight: normal; - text-transform: none; - font-style: italic; - } - } - - .items { - top: @margin; - font-size: @normal-font-size; - line-height: @default-font-size + 5; - table { - border: @thin-border; - th { - vertical-align: middle; - } - td { - vertical-align: middle; - } - tr { - border: none; - } - } - small { - font-size: @xsmall-font-size; - } - } - - &:first-child .items { - top: @margin + 90mm; - } - - .footer { - font-size: @xsmall-font-size; - } - -} - -.title { - position: absolute; - - top: @margin + 84.7mm; - left: @margin + 95mm; - - color: #333; - font-weight: bold; - font-size: @large-font-size; - text-transform:uppercase; - letter-spacing: 2pt; - - .invoice { - top: @margin + 90mm; - } -} - -.limit { - position: absolute; - - top: @margin + 100mm; - left: @margin; -} - -.paymentinfo { - position: absolute; - - top: @margin + 220mm; - left: @margin; - font-size: @small-font-size; - - .columns { - display: flex; - flex-direction: row; - div { - width: 50%; - } - } -} - -.footer { - position: absolute; - bottom: @margin; - left: @margin; - right: @margin; -} - -.txtAlignRight, .subtotal,.total, .due { - text-align: right; - white-space: nowrap; -} - - -.reference { - position: absolute; - top: @margin + 70mm; - left: @margin; - - table { - width: 200pt; - font-size: @small-font-size; - } - - td { - padding: 2pt; - text-align: right; - } - - td.label { - text-align: left; - font-size: @small-font-size; - } - table td, table th { - - padding: 2pt; - margin: 0; -} -} - -.today { - position: absolute; - - top: @margin + 70mm; - left: @margin + 95mm; - font: @default-font-size "Tahoma"; - text-align: left; -} - - -.items { - position: absolute; - top: @margin + 110mm; - left: @margin; - width: 210mm - 2*@margin; - - table { - //border: @thin-border; - border-top: @thin-border; - border-bottom: @thin-border; - width: 100%; - } - - th { - border-right: @thin-border; - border-bottom: @thin-border; - } - - th:first-child { - border-left: @thin-border; - } - - td { - padding-right: 10pt; - border-right: @thin-border; - vertical-align: top; - } - - td:first-child { - border-left: @thin-border; - } - - td.description { - width: 220pt; - padding-left:10pt; - } - - td.total-line { - border-right: none; - text-align: left; - padding-left:10pt; - border-left: @thin-border; - border-top: @thin-border; - } - - td.total-value { - border-left: none; - border-top: @thin-border; - } - - td.blank:last-child { - border-bottom: @thin-border; - } -} - -.certificate .items { - font-size: @large-font-size; - line-height: @large-font-size + 12; -} - -.certificate .signature { - font-size: @default-font-size; - line-height: normal; -} - -.rubber-stamp { - position: absolute; - top: @margin + 55mm; - left: @margin + 10mm; - font-size: @xlarge-font-size; - font-weight: bold; - text-align: center; - - border-radius: 10pt; - border: @large-border; - padding: 5pt; - - font-family: 'Vollkorn', serif; - text-transform: uppercase; - transform:rotate(-20deg); - transform-origin: bottom left; - -ms-transform:rotate(-20deg); - -moz-transform: rotate(-20deg); - -webkit-transform: rotate(-20deg); - -o-transform: rotate(-20deg); - -o-transform-origin: bottom left; -} - -.printbutton { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 40px; - background-color: #ccc; - text-align: center; - - button { - height: 35px; - text-transform: uppercase; - font-size: 20px; - } -} - -.red { - color: #d00; - border-color: #d00; -} - -.text-right { - text-align: right; -} - -@page { - size: A4 portrait; - margin: 0; -} -@media print { - span, - span:focus, - span:active{ - border: none; - padding: 0; - background-color: transparent; - } - .page { - border: none; - } - .printbutton { - display: none; - } -} +@logo-font-size: 18pt; +@default-font-size: 10pt; +@normal-font-size: 11.5pt; +@large-font-size: 14pt; +@xlarge-font-size: 15.5pt; +@small-font-size: 9pt; +@xsmall-font-size: 8pt; + +@margin: 10mm; +@thin-border: 1pt solid #555; +@border: 2pt solid #555; +@large-border: 2.8pt solid #555; + +body { + margin: 0; + padding: 0; + font: @default-font-size 'Tahoma'; +} +* { + box-sizing: border-box; + -moz-box-sizing: border-box; +} +h1, +h2, +h3 { + margin: 0; + padding: 0; +} +table { + border: none; + border-collapse: collapse; +} + +span, +span:focus, +span:active { + border: 2px solid #ccc; + outline: none; + padding: 2px; + background-color: #ffffe0; +} +span.error { + border: 2px solid red; +} +.page { + position: relative; + width: 210mm; + height: 296mm; + border: 1px solid #ccc; + page-break-inside: avoid; +} + +.logo { + position: absolute; + top: @margin; + left: @margin; + h1 { + display: inline-block; + font-size: @logo-font-size; + color: #330066; + } + img { + display: inline-block; + } +} +.contact { + border-top: @border; + margin: 2pt 0 0 0; + padding: 5pt 0 0 0; +} +.customer { + position: absolute; + top: @margin + 35mm; + left: @margin + 95mm; + + h3 { + margin-bottom: 5pt; + } +} + +.object { + position: absolute; + top: @margin + 95mm; + left: @margin; +} + +.certificate .object { + position: absolute; + top: @margin + 95mm; + left: @margin; + right: @margin; + padding: 5pt; + border: @border; + text-align: center; + text-transform: uppercase; + font-size: @xlarge-font-size; + font-weight: bold; +} + +.contract { + .object { + position: absolute; + top: @margin + 65mm; + left: @margin; + right: @margin; + padding: 15pt; + border: @border; + text-align: center; + text-transform: uppercase; + font-size: @xlarge-font-size; + font-weight: bold; + background-color: #ccc; + small { + font-weight: normal; + text-transform: none; + font-style: italic; + } + } + + .items { + top: @margin; + font-size: @normal-font-size; + line-height: @default-font-size + 5; + table { + border: @thin-border; + th { + vertical-align: middle; + } + td { + vertical-align: middle; + } + tr { + border: none; + } + } + small { + font-size: @xsmall-font-size; + } + } + + &:first-child .items { + top: @margin + 90mm; + } + + .footer { + font-size: @xsmall-font-size; + } +} + +.title { + position: absolute; + + top: @margin + 84.7mm; + left: @margin + 95mm; + + color: #333; + font-weight: bold; + font-size: @large-font-size; + text-transform: uppercase; + letter-spacing: 2pt; + + .invoice { + top: @margin + 90mm; + } +} + +.limit { + position: absolute; + + top: @margin + 100mm; + left: @margin; +} + +.paymentinfo { + position: absolute; + + top: @margin + 220mm; + left: @margin; + font-size: @small-font-size; + + .columns { + display: flex; + flex-direction: row; + div { + width: 50%; + } + } +} + +.footer { + position: absolute; + bottom: @margin; + left: @margin; + right: @margin; +} + +.txtAlignRight, +.subtotal, +.total, +.due { + text-align: right; + white-space: nowrap; +} + +.reference { + position: absolute; + top: @margin + 70mm; + left: @margin; + + table { + width: 200pt; + font-size: @small-font-size; + } + + td { + padding: 2pt; + text-align: right; + } + + td.label { + text-align: left; + font-size: @small-font-size; + } + table td, + table th { + padding: 2pt; + margin: 0; + } +} + +.today { + position: absolute; + + top: @margin + 70mm; + left: @margin + 95mm; + font: @default-font-size 'Tahoma'; + text-align: left; +} + +.items { + position: absolute; + top: @margin + 110mm; + left: @margin; + width: 210mm - 2 * @margin; + + table { + //border: @thin-border; + border-top: @thin-border; + border-bottom: @thin-border; + width: 100%; + } + + th { + border-right: @thin-border; + border-bottom: @thin-border; + } + + th:first-child { + border-left: @thin-border; + } + + td { + padding-right: 10pt; + border-right: @thin-border; + vertical-align: top; + } + + td:first-child { + border-left: @thin-border; + } + + td.description { + width: 220pt; + padding-left: 10pt; + } + + td.total-line { + border-right: none; + text-align: left; + padding-left: 10pt; + border-left: @thin-border; + border-top: @thin-border; + } + + td.total-value { + border-left: none; + border-top: @thin-border; + } + + td.blank:last-child { + border-bottom: @thin-border; + } +} + +.certificate .items { + font-size: @large-font-size; + line-height: @large-font-size + 12; +} + +.certificate .signature { + font-size: @default-font-size; + line-height: normal; +} + +.rubber-stamp { + position: absolute; + top: @margin + 55mm; + left: @margin + 10mm; + font-size: @xlarge-font-size; + font-weight: bold; + text-align: center; + + border-radius: 10pt; + border: @large-border; + padding: 5pt; + + font-family: 'Vollkorn', serif; + text-transform: uppercase; + transform: rotate(-20deg); + transform-origin: bottom left; + -ms-transform: rotate(-20deg); + -moz-transform: rotate(-20deg); + -webkit-transform: rotate(-20deg); + -o-transform: rotate(-20deg); + -o-transform-origin: bottom left; +} + +.printbutton { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 40px; + background-color: #ccc; + text-align: center; + + button { + height: 35px; + text-transform: uppercase; + font-size: 20px; + } +} + +.red { + color: #d00; + border-color: #d00; +} + +.text-right { + text-align: right; +} + +@page { + size: A4 portrait; + margin: 0; +} +@media print { + span, + span:focus, + span:active { + border: none; + padding: 0; + background-color: transparent; + } + .page { + border: none; + } + .printbutton { + display: none; + } +} diff --git a/frontend/less/style.less b/frontend/less/style.less index 46dcef8..063131e 100644 --- a/frontend/less/style.less +++ b/frontend/less/style.less @@ -1,27 +1,27 @@ -.label { - border-radius: 10px; - color: #fff; - padding-top: 5px; - padding-bottom: 5px; - margin-bottom: 2px; - - &.label-danger { - background-color: lighten(@brand-danger, 20%); - } - &.label-warning { - background-color: lighten(@brand-warning, 20%); - } - &.label-success { - background-color: lighten(@brand-success, 10%); - } -} - -.price-content { - position: relative; - padding-right: 10px; -} -.price-symbol { - font-size: ceil((@font-size-base * 0.65)); - position: absolute; - top:0; -} +.label { + border-radius: 10px; + color: #fff; + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 2px; + + &.label-danger { + background-color: lighten(@brand-danger, 20%); + } + &.label-warning { + background-color: lighten(@brand-warning, 20%); + } + &.label-success { + background-color: lighten(@brand-success, 10%); + } +} + +.price-content { + position: relative; + padding-right: 10px; +} +.price-symbol { + font-size: ceil((@font-size-base * 0.65)); + position: absolute; + top: 0; +} diff --git a/frontend/less/table.less b/frontend/less/table.less index 0aa8c1b..02e95d8 100644 --- a/frontend/less/table.less +++ b/frontend/less/table.less @@ -1,13 +1,16 @@ -.table > tbody > tr > td, .table > tbody > tr > th, -.table > tfoot > tr > td, .table > tfoot > tr > th, -.table > thead > tr > td, .table > thead > tr > th { - padding: 4px; +.table > tbody > tr > td, +.table > tbody > tr > th, +.table > tfoot > tr > td, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > thead > tr > th { + padding: 4px; } .table .table { - background-color: transparent; + background-color: transparent; } .table .table.table-borderless td { - border: none; -} \ No newline at end of file + border: none; +} diff --git a/frontend/less/tiles.less b/frontend/less/tiles.less index e1d9540..d1e906f 100644 --- a/frontend/less/tiles.less +++ b/frontend/less/tiles.less @@ -1,144 +1,176 @@ .tiles { - .col-xs-1, - .col-xs-2, - .col-xs-3, - .col-xs-4, - .col-xs-5, - .col-xs-6, - .col-xs-7, - .col-xs-8, - .col-xs-9, - .col-xs-10, - .col-xs-11, - .col-xs-12 { - padding-left: 0; - padding-right: 5px; - padding-bottom: 5px; - &.last-col { - padding-right: 0; - } - &.last-row { - padding-bottom: 0; - } + .col-xs-1, + .col-xs-2, + .col-xs-3, + .col-xs-4, + .col-xs-5, + .col-xs-6, + .col-xs-7, + .col-xs-8, + .col-xs-9, + .col-xs-10, + .col-xs-11, + .col-xs-12 { + padding-left: 0; + padding-right: 5px; + padding-bottom: 5px; + &.last-col { + padding-right: 0; } + &.last-row { + padding-bottom: 0; + } + } - .tile { - height: 195px; - padding: 10px; - color: white; - &:not(.disabled) { - cursor: pointer; - } + .tile { + height: 195px; + padding: 10px; + color: white; + &:not(.disabled) { + cursor: pointer; + } - .h1, .h2, .h3, .h4, .h5, - h1, h2, h3, h4, h5 { - color: white; - } + .h1, + .h2, + .h3, + .h4, + .h5, + h1, + h2, + h3, + h4, + h5 { + color: white; + } - .carousel { - height: 175px; // - padding - .item { - height: 175px; //- padding - } - } + .carousel { + height: 175px; // - padding + .item { + height: 175px; //- padding + } + } - .vertical-center { - position: relative; - top: 50%; - transform: translateY(-50%); - margin: 0; - .h1, .h2, .h3, .h4, .h5, - h1, h2, h3, h4, h5 { - &:first-child { - margin-top: 0; - } - } + .vertical-center { + position: relative; + top: 50%; + transform: translateY(-50%); + margin: 0; + .h1, + .h2, + .h3, + .h4, + .h5, + h1, + h2, + h3, + h4, + h5 { + &:first-child { + margin-top: 0; } + } + } - .tilecaption { - font-size: 70px; - } + .tilecaption { + font-size: 70px; + } - .tilecurrency { - font-size: 24px; - font-weight: bold; - white-space: nowrap; - } + .tilecurrency { + font-size: 24px; + font-weight: bold; + white-space: nowrap; + } - &.blue { - background: rgb(0,172,238); - &:hover:not(.disabled) { - background-color: darken(rgb(0,172,238), 10%) - } - &.lighter { - background: rgb(62,157,215); - &:hover:not(.disabled) { - background-color: darken(rgb(62,157,215), 10%) - } - } - &.light { - background: rgb(71,193,228); - &:hover:not(.disabled) { - background-color: darken(rgb(71,193,228), 10%) - } - } - &.dark { - background: rgb(0,93,233); - &:hover:not(.disabled) { - background-color: darken(rgb(0,93,233), 10%) - } - } + &.blue { + background: rgb(0, 172, 238); + &:hover:not(.disabled) { + background-color: darken(rgb(0, 172, 238), 10%); + } + &.lighter { + background: rgb(62, 157, 215); + &:hover:not(.disabled) { + background-color: darken(rgb(62, 157, 215), 10%); } - - &.grey { - color: #555; - .h1, .h2, .h3, .h4, .h5, - h1, h2, h3, h4, h5 { - color: #555; - } - background: rgb(243,243,243); - &:hover:not(.disabled) { - background-color: darken(rgb(243,243,243), 10%) - } + } + &.light { + background: rgb(71, 193, 228); + &:hover:not(.disabled) { + background-color: darken(rgb(71, 193, 228), 10%); } - - &.cover { - background-size: cover; - background-image: url('/public/images/manhattan.jpg'); - background-position: center center; - font-weight: bold; - .h1, .h2, .h3, .h4, .h5, - h1, h2, h3, h4, h5 { - font-weight: bold; - } + } + &.dark { + background: rgb(0, 93, 233); + &:hover:not(.disabled) { + background-color: darken(rgb(0, 93, 233), 10%); } + } + } - &.red { - background: rgb(175,26,63); - &:hover:not(.disabled) { - background-color: darken(rgb(175,26,63), 10%) - } - } + &.grey { + color: #555; + .h1, + .h2, + .h3, + .h4, + .h5, + h1, + h2, + h3, + h4, + h5 { + color: #555; + } + background: rgb(243, 243, 243); + &:hover:not(.disabled) { + background-color: darken(rgb(243, 243, 243), 10%); + } + } - &.white { - background: white; - &:hover:not(.disabled) { - background-color: darken(white, 10%) - } - } + &.cover { + background-size: cover; + background-image: url('/public/images/manhattan.jpg'); + background-position: center center; + font-weight: bold; + .h1, + .h2, + .h3, + .h4, + .h5, + h1, + h2, + h3, + h4, + h5 { + font-weight: bold; + } + } - &.orange { - background: rgb(209,70,37); - &:hover:not(.disabled) { - background-color: darken(rgb(209,70,37), 10%) - } - } + &.red { + background: rgb(175, 26, 63); + &:hover:not(.disabled) { + background-color: darken(rgb(175, 26, 63), 10%); + } + } - &.green { - background: rgb(0,142,0); - &:hover:not(.disabled) { - background-color: darken(rgb(0,142,0), 10%) - } - } + &.white { + background: white; + &:hover:not(.disabled) { + background-color: darken(white, 10%); + } + } + + &.orange { + background: rgb(209, 70, 37); + &:hover:not(.disabled) { + background-color: darken(rgb(209, 70, 37), 10%); + } + } + + &.green { + background: rgb(0, 142, 0); + &:hover:not(.disabled) { + background-color: darken(rgb(0, 142, 0), 10%); + } } + } } diff --git a/frontend/less/variables.less b/frontend/less/variables.less index dab838d..60f5952 100644 --- a/frontend/less/variables.less +++ b/frontend/less/variables.less @@ -9,259 +9,257 @@ // //## Gray and brand colors for use across Bootstrap. -@gray-base: #000; -@gray-darker: lighten(@gray-base, 13.5%); // #222 -@gray-dark: lighten(@gray-base, 20%); // #333 -@gray: lighten(@gray-base, 33.5%); // #555 -@gray-light: lighten(@gray-base, 60%); // #999 -@gray-lighter: lighten(@gray-base, 93.5%); // #eee - -@brand-primary: #2FA4E7; -@brand-success: #73A839; -@brand-info: #033C73; -@brand-warning: #DD5600; -@brand-danger: #C71C22; +@gray-base: #000; +@gray-darker: lighten(@gray-base, 13.5%); // #222 +@gray-dark: lighten(@gray-base, 20%); // #333 +@gray: lighten(@gray-base, 33.5%); // #555 +@gray-light: lighten(@gray-base, 60%); // #999 +@gray-lighter: lighten(@gray-base, 93.5%); // #eee + +@brand-primary: #2fa4e7; +@brand-success: #73a839; +@brand-info: #033c73; +@brand-warning: #dd5600; +@brand-danger: #c71c22; //== Scaffolding // //## Settings for some of the most global styles. //** Background color for ``. -@body-bg: #F5F5F5; +@body-bg: #f5f5f5; //** Global text color on ``. -@text-color: @gray; +@text-color: @gray; //** Global textual link color. -@link-color: @brand-primary; +@link-color: @brand-primary; //** Link hover color set via `darken()` function. -@link-hover-color: darken(@link-color, 15%); +@link-hover-color: darken(@link-color, 15%); //** Link hover decoration. @link-hover-decoration: underline; - //== Typography // //## Font, line-height, and color for body text, headings, and more. -@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; -@font-family-serif: Georgia, "Times New Roman", Times, serif; +@font-family-sans-serif: 'Helvetica Neue', Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, 'Times New Roman', Times, serif; //** Default monospace fonts for ``, ``, and `
`.
-@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
-@font-family-base:        @font-family-sans-serif;
+@font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
+@font-family-base: @font-family-sans-serif;
 
-@font-size-base:          14px;
-@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
-@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+@font-size-base: 14px;
+@font-size-large: ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small: ceil((@font-size-base * 0.85)); // ~12px
 
-@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
-@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
-@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
-@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
-@font-size-h5:            @font-size-base;
-@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+@font-size-h1: floor((@font-size-base * 2.6)); // ~36px
+@font-size-h2: floor((@font-size-base * 2.15)); // ~30px
+@font-size-h3: ceil((@font-size-base * 1.7)); // ~24px
+@font-size-h4: ceil((@font-size-base * 1.25)); // ~18px
+@font-size-h5: @font-size-base;
+@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px
 
 //** Unit-less `line-height` for use in components like buttons.
-@line-height-base:        1.428571429; // 20/14
+@line-height-base: 1.428571429; // 20/14
 //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px
 
 //** By default, this inherits from the ``.
-@headings-font-family:    @font-family-base;
-@headings-font-weight:    500;
-@headings-line-height:    1.2;
-@headings-color:          #317EAC;
-
+@headings-font-family: @font-family-base;
+@headings-font-weight: 500;
+@headings-line-height: 1.2;
+@headings-color: #317eac;
 
 //== Iconography
 //
 //## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
 
 //** Load fonts from this directory.
-@icon-font-path:          "../fonts/";
+@icon-font-path: '../fonts/';
 //** File name for all font files.
-@icon-font-name:          "glyphicons-halflings-regular";
+@icon-font-name: 'glyphicons-halflings-regular';
 //** Element ID within SVG icon file.
-@icon-font-svg-id:        "glyphicons_halflingsregular";
-
+@icon-font-svg-id: 'glyphicons_halflingsregular';
 
 //== Components
 //
 //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 
-@padding-base-vertical:     8px;
-@padding-base-horizontal:   12px;
+@padding-base-vertical: 8px;
+@padding-base-horizontal: 12px;
 
-@padding-large-vertical:    14px;
-@padding-large-horizontal:  16px;
+@padding-large-vertical: 14px;
+@padding-large-horizontal: 16px;
 
-@padding-small-vertical:    5px;
-@padding-small-horizontal:  10px;
+@padding-small-vertical: 5px;
+@padding-small-horizontal: 10px;
 
-@padding-xs-vertical:       1px;
-@padding-xs-horizontal:     5px;
+@padding-xs-vertical: 1px;
+@padding-xs-horizontal: 5px;
 
-@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
-@line-height-small:         1.5;
+@line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome
+@line-height-small: 1.5;
 
-@border-radius-base:        4px;
-@border-radius-large:       6px;
-@border-radius-small:       3px;
+@border-radius-base: 4px;
+@border-radius-large: 6px;
+@border-radius-small: 3px;
 
 //** Global color for active items (e.g., navs or dropdowns).
-@component-active-color:    #fff;
+@component-active-color: #fff;
 //** Global background color for active items (e.g., navs or dropdowns).
-@component-active-bg:       @brand-primary;
+@component-active-bg: @brand-primary;
 
 //** Width of the `border` for generating carets that indicator dropdowns.
-@caret-width-base:          4px;
+@caret-width-base: 4px;
 //** Carets increase slightly in size for larger components.
-@caret-width-large:         5px;
-
+@caret-width-large: 5px;
 
 //== Tables
 //
 //## Customizes the `.table` component with basic values, each used across all table variations.
 
 //** Padding for ``s and ``s.
-@table-cell-padding:            8px;
+@table-cell-padding: 8px;
 //** Padding for cells in `.table-condensed`.
-@table-condensed-cell-padding:  5px;
+@table-condensed-cell-padding: 5px;
 
 //** Default background color used for all tables.
-@table-bg:                      transparent;
+@table-bg: transparent;
 //** Background color used for `.table-striped`.
-@table-bg-accent:               #f9f9f9;
+@table-bg-accent: #f9f9f9;
 //** Background color used for `.table-hover`.
-@table-bg-hover:                #f5f5f5;
-@table-bg-active:               @table-bg-hover;
+@table-bg-hover: #f5f5f5;
+@table-bg-active: @table-bg-hover;
 
 //** Border color for table and cell borders.
-@table-border-color:            #ddd;
-
+@table-border-color: #ddd;
 
 //== Buttons
 //
 //## For each of Bootstrap's buttons, define text, background and border color.
 
-@btn-font-weight:                normal;
+@btn-font-weight: normal;
 
-@btn-default-color:              @text-color;
-@btn-default-bg:                 #fff;
-@btn-default-border:             rgba(0, 0, 0, 0.1);
+@btn-default-color: @text-color;
+@btn-default-bg: #fff;
+@btn-default-border: rgba(0, 0, 0, 0.1);
 
-@btn-primary-color:              #fff;
-@btn-primary-bg:                 @brand-primary;
-@btn-primary-border:             @btn-primary-bg;
+@btn-primary-color: #fff;
+@btn-primary-bg: @brand-primary;
+@btn-primary-border: @btn-primary-bg;
 
-@btn-success-color:              #fff;
-@btn-success-bg:                 @brand-success;
-@btn-success-border:             @btn-success-bg;
+@btn-success-color: #fff;
+@btn-success-bg: @brand-success;
+@btn-success-border: @btn-success-bg;
 
-@btn-info-color:                 #fff;
-@btn-info-bg:                    @brand-info;
-@btn-info-border:                @btn-info-bg;
+@btn-info-color: #fff;
+@btn-info-bg: @brand-info;
+@btn-info-border: @btn-info-bg;
 
-@btn-warning-color:              #fff;
-@btn-warning-bg:                 @brand-warning;
-@btn-warning-border:             @btn-warning-bg;
+@btn-warning-color: #fff;
+@btn-warning-bg: @brand-warning;
+@btn-warning-border: @btn-warning-bg;
 
-@btn-danger-color:               #fff;
-@btn-danger-bg:                  @brand-danger;
-@btn-danger-border:              @btn-danger-bg;
+@btn-danger-color: #fff;
+@btn-danger-bg: @brand-danger;
+@btn-danger-border: @btn-danger-bg;
 
-@btn-link-disabled-color:        @gray-light;
+@btn-link-disabled-color: @gray-light;
 
 // Allows for customizing button radius independently from global border radius
-@btn-border-radius-base:         @border-radius-base;
-@btn-border-radius-large:        @border-radius-large;
-@btn-border-radius-small:        @border-radius-small;
-
+@btn-border-radius-base: @border-radius-base;
+@btn-border-radius-large: @border-radius-large;
+@btn-border-radius-small: @border-radius-small;
 
 //== Forms
 //
 //##
 
 //** `` background color
-@input-bg:                       #fff;
+@input-bg: #fff;
 //** `` background color
-@input-bg-disabled:              @gray-lighter;
+@input-bg-disabled: @gray-lighter;
 
 //** Text color for ``s
-@input-color:                    @text-color;
+@input-color: @text-color;
 //** `` border color
-@input-border:                   #ccc;
+@input-border: #ccc;
 
 // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
 //** Default `.form-control` border radius
 // This has no effect on ``s in CSS.
-@input-border-radius:            @border-radius-base;
+@input-border-radius: @border-radius-base;
 //** Large `.form-control` border radius
-@input-border-radius-large:      @border-radius-large;
+@input-border-radius-large: @border-radius-large;
 //** Small `.form-control` border radius
-@input-border-radius-small:      @border-radius-small;
+@input-border-radius-small: @border-radius-small;
 
 //** Border color for inputs on focus
-@input-border-focus:             #66afe9;
+@input-border-focus: #66afe9;
 
 //** Placeholder text color
-@input-color-placeholder:        @gray-light;
+@input-color-placeholder: @gray-light;
 
 //** Default `.form-control` height
-@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2);
 //** Large `.form-control` height
-@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+@input-height-large: (
+  ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) +
+    2
+);
 //** Small `.form-control` height
-@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+@input-height-small: (
+  floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) +
+    2
+);
 
 //** `.form-group` margin
-@form-group-margin-bottom:       15px;
+@form-group-margin-bottom: 15px;
 
-@legend-color:                   @text-color;
-@legend-border-color:            #e5e5e5;
+@legend-color: @text-color;
+@legend-border-color: #e5e5e5;
 
 //** Background color for textual input addons
-@input-group-addon-bg:           @gray-lighter;
+@input-group-addon-bg: @gray-lighter;
 //** Border color for textual input addons
 @input-group-addon-border-color: @input-border;
 
 //** Disabled cursor for form controls and buttons.
-@cursor-disabled:                not-allowed;
-
+@cursor-disabled: not-allowed;
 
 //== Dropdowns
 //
 //## Dropdown menu container and contents.
 
 //** Background for the dropdown menu.
-@dropdown-bg:                    #fff;
+@dropdown-bg: #fff;
 //** Dropdown menu `border-color`.
-@dropdown-border:                rgba(0,0,0,.15);
+@dropdown-border: rgba(0, 0, 0, 0.15);
 //** Dropdown menu `border-color` **for IE8**.
-@dropdown-fallback-border:       #ccc;
+@dropdown-fallback-border: #ccc;
 //** Divider color for between dropdown items.
-@dropdown-divider-bg:            #e5e5e5;
+@dropdown-divider-bg: #e5e5e5;
 
 //** Dropdown link text color.
-@dropdown-link-color:            @gray-dark;
+@dropdown-link-color: @gray-dark;
 //** Hover color for dropdown links.
-@dropdown-link-hover-color:      #fff;
+@dropdown-link-hover-color: #fff;
 //** Hover background for dropdown links.
-@dropdown-link-hover-bg:         @component-active-bg;
+@dropdown-link-hover-bg: @component-active-bg;
 
 //** Active dropdown menu item text color.
-@dropdown-link-active-color:     #fff;
+@dropdown-link-active-color: #fff;
 //** Active dropdown menu item background color.
-@dropdown-link-active-bg:        @component-active-bg;
+@dropdown-link-active-bg: @component-active-bg;
 
 //** Disabled dropdown menu item background color.
-@dropdown-link-disabled-color:   @gray-light;
+@dropdown-link-disabled-color: @gray-light;
 
 //** Text color for headers within dropdown menus.
-@dropdown-header-color:          @gray-light;
+@dropdown-header-color: @gray-light;
 
 //** Deprecated `@dropdown-caret-color` as of v3.1.0
-@dropdown-caret-color:           #000;
-
+@dropdown-caret-color: #000;
 
 //-- Z-index master list
 //
@@ -270,14 +268,13 @@
 //
 // Note: These variables are not generated into the Customizer.
 
-@zindex-navbar:            1000;
-@zindex-dropdown:          1000;
-@zindex-popover:           1060;
-@zindex-tooltip:           1070;
-@zindex-navbar-fixed:      1030;
-@zindex-modal-background:  1040;
-@zindex-modal:             1050;
-
+@zindex-navbar: 1000;
+@zindex-dropdown: 1000;
+@zindex-popover: 1060;
+@zindex-tooltip: 1070;
+@zindex-navbar-fixed: 1030;
+@zindex-modal-background: 1040;
+@zindex-modal: 1050;
 
 //== Media queries breakpoints
 //
@@ -286,567 +283,545 @@
 // Extra small screen / phone
 //** Deprecated `@screen-xs` as of v3.0.1
 // @screen-xs:                  480px; // disabled phone screen
-@screen-xs:                  0px;
+@screen-xs: 0px;
 //** Deprecated `@screen-xs-min` as of v3.2.0
-@screen-xs-min:              @screen-xs;
+@screen-xs-min: @screen-xs;
 //** Deprecated `@screen-phone` as of v3.0.1
-@screen-phone:               @screen-xs-min;
+@screen-phone: @screen-xs-min;
 
 // Small screen / tablet
 //** Deprecated `@screen-sm` as of v3.0.1
 //@screen-sm:                  768px; // disabled phone screen
-@screen-sm:                  @screen-xs;
-@screen-sm-min:              @screen-sm;
+@screen-sm: @screen-xs;
+@screen-sm-min: @screen-sm;
 //** Deprecated `@screen-tablet` as of v3.0.1
-@screen-tablet:              @screen-sm-min;
+@screen-tablet: @screen-sm-min;
 
 // Medium screen / desktop
 //** Deprecated `@screen-md` as of v3.0.1
 //@screen-md:                  992px; // set minimum screen to 1024px
-@screen-md:                  1024px;
+@screen-md: 1024px;
 //@screen-md-min:              @screen-md;
-@screen-md-min:              @screen-xs-min; // set minimum screen to 1024px
+@screen-md-min: @screen-xs-min; // set minimum screen to 1024px
 //** Deprecated `@screen-desktop` as of v3.0.1
-@screen-desktop:             @screen-md-min;
+@screen-desktop: @screen-md-min;
 
 // Large screen / wide desktop
 //** Deprecated `@screen-lg` as of v3.0.1
-@screen-lg:                  1200px;
-@screen-lg-min:              @screen-lg;
+@screen-lg: 1200px;
+@screen-lg-min: @screen-lg;
 //** Deprecated `@screen-lg-desktop` as of v3.0.1
-@screen-lg-desktop:          @screen-lg-min;
+@screen-lg-desktop: @screen-lg-min;
 
 // So media queries don't overlap when required, provide a maximum
-@screen-xs-max:              (@screen-sm-min - 1);
-@screen-sm-max:              (@screen-md-min - 1);
-@screen-md-max:              (@screen-lg-min - 1);
-
+@screen-xs-max: (@screen-sm-min - 1);
+@screen-sm-max: (@screen-md-min - 1);
+@screen-md-max: (@screen-lg-min - 1);
 
 //== Grid system
 //
 //## Define your custom responsive grid.
 
 //** Number of columns in the grid.
-@grid-columns:              12;
+@grid-columns: 12;
 //** Padding between columns. Gets divided in half for the left and right.
-@grid-gutter-width:         30px;
+@grid-gutter-width: 30px;
 // Navbar collapse
 //** Point at which the navbar becomes uncollapsed.
-@grid-float-breakpoint:     @screen-sm-min;
+@grid-float-breakpoint: @screen-sm-min;
 //** Point at which the navbar begins collapsing.
 @grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
 
-
 //== Container sizes
 //
 //## Define the maximum width of `.container` for different screen sizes.
 
 // Small screen / tablet
-@container-tablet:             (720px + @grid-gutter-width);
+@container-tablet: (720px + @grid-gutter-width);
 //** For `@screen-sm-min` and up.
-@container-sm:                 @container-tablet;
+@container-sm: @container-tablet;
 
 // Medium screen / desktop
 //@container-desktop:            (940px + @grid-gutter-width);
-@container-desktop:            (1024px + @grid-gutter-width);
+@container-desktop: (1024px + @grid-gutter-width);
 //** For `@screen-md-min` and up.
-@container-md:                 @container-desktop;
+@container-md: @container-desktop;
 
 // Large screen / wide desktop
 //@container-large-desktop:      (1140px + @grid-gutter-width);
-@container-large-desktop:      (1200px + @grid-gutter-width);
+@container-large-desktop: (1200px + @grid-gutter-width);
 //** For `@screen-lg-min` and up.
-@container-lg:                 @container-large-desktop;
-
+@container-lg: @container-large-desktop;
 
 //== Navbar
 //
 //##
 
 // Basics of a navbar
-@navbar-height:                    50px;
-@navbar-margin-bottom:             @line-height-computed;
-@navbar-border-radius:             @border-radius-base;
-@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
-@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
-@navbar-collapse-max-height:       340px;
+@navbar-height: 50px;
+@navbar-margin-bottom: @line-height-computed;
+@navbar-border-radius: @border-radius-base;
+@navbar-padding-horizontal: floor((@grid-gutter-width / 2));
+@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height: 340px;
 
-@navbar-default-color:             #ddd;
-@navbar-default-bg:                @brand-primary;
-@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-color: #ddd;
+@navbar-default-bg: @brand-primary;
+@navbar-default-border: darken(@navbar-default-bg, 6.5%);
 
 // Navbar links
-@navbar-default-link-color:                #fff;
-@navbar-default-link-hover-color:          #fff;
-@navbar-default-link-hover-bg:             darken(@navbar-default-bg, 10%);
-@navbar-default-link-active-color:         #fff;
-@navbar-default-link-active-bg:            darken(@navbar-default-bg, 10%);
-@navbar-default-link-disabled-color:       #ddd;
-@navbar-default-link-disabled-bg:          transparent;
+@navbar-default-link-color: #fff;
+@navbar-default-link-hover-color: #fff;
+@navbar-default-link-hover-bg: darken(@navbar-default-bg, 10%);
+@navbar-default-link-active-color: #fff;
+@navbar-default-link-active-bg: darken(@navbar-default-bg, 10%);
+@navbar-default-link-disabled-color: #ddd;
+@navbar-default-link-disabled-bg: transparent;
 
 // Navbar brand label
-@navbar-default-brand-color:               @navbar-default-link-color;
-@navbar-default-brand-hover-color:         #fff;
-@navbar-default-brand-hover-bg:            none;
+@navbar-default-brand-color: @navbar-default-link-color;
+@navbar-default-brand-hover-color: #fff;
+@navbar-default-brand-hover-bg: none;
 
 // Navbar toggle
-@navbar-default-toggle-hover-bg:           darken(@navbar-default-bg, 10%);
-@navbar-default-toggle-icon-bar-bg:        #fff;
-@navbar-default-toggle-border-color:       darken(@navbar-default-bg, 10%);
-
+@navbar-default-toggle-hover-bg: darken(@navbar-default-bg, 10%);
+@navbar-default-toggle-icon-bar-bg: #fff;
+@navbar-default-toggle-border-color: darken(@navbar-default-bg, 10%);
 
 //=== Inverted navbar
 // Reset inverted navbar basics
-@navbar-inverse-color:                      #fff;
-@navbar-inverse-bg:                         @brand-info;
-@navbar-inverse-border:                     darken(@navbar-inverse-bg, 5%);
+@navbar-inverse-color: #fff;
+@navbar-inverse-bg: @brand-info;
+@navbar-inverse-border: darken(@navbar-inverse-bg, 5%);
 
 // Inverted navbar links
-@navbar-inverse-link-color:                 #fff;
-@navbar-inverse-link-hover-color:           #fff;
-@navbar-inverse-link-hover-bg:              darken(@navbar-inverse-bg, 5%);
-@navbar-inverse-link-active-color:          #fff;
-@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 5%);
-@navbar-inverse-link-disabled-color:        #ccc;
-@navbar-inverse-link-disabled-bg:           transparent;
+@navbar-inverse-link-color: #fff;
+@navbar-inverse-link-hover-color: #fff;
+@navbar-inverse-link-hover-bg: darken(@navbar-inverse-bg, 5%);
+@navbar-inverse-link-active-color: #fff;
+@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 5%);
+@navbar-inverse-link-disabled-color: #ccc;
+@navbar-inverse-link-disabled-bg: transparent;
 
 // Inverted navbar brand label
-@navbar-inverse-brand-color:                @navbar-inverse-link-color;
-@navbar-inverse-brand-hover-color:          #fff;
-@navbar-inverse-brand-hover-bg:             none;
+@navbar-inverse-brand-color: @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color: #fff;
+@navbar-inverse-brand-hover-bg: none;
 
 // Inverted navbar toggle
-@navbar-inverse-toggle-hover-bg:            darken(@navbar-inverse-bg, 5%);
-@navbar-inverse-toggle-icon-bar-bg:         #fff;
-@navbar-inverse-toggle-border-color:        darken(@navbar-inverse-bg, 5%);
-
+@navbar-inverse-toggle-hover-bg: darken(@navbar-inverse-bg, 5%);
+@navbar-inverse-toggle-icon-bar-bg: #fff;
+@navbar-inverse-toggle-border-color: darken(@navbar-inverse-bg, 5%);
 
 //== Navs
 //
 //##
 
 //=== Shared nav styles
-@nav-link-padding:                          10px 15px;
-@nav-link-hover-bg:                         @gray-lighter;
+@nav-link-padding: 10px 15px;
+@nav-link-hover-bg: @gray-lighter;
 
-@nav-disabled-link-color:                   @gray-light;
-@nav-disabled-link-hover-color:             @gray-light;
+@nav-disabled-link-color: @gray-light;
+@nav-disabled-link-hover-color: @gray-light;
 
 //== Tabs
-@nav-tabs-border-color:                     #ddd;
+@nav-tabs-border-color: #ddd;
 
-@nav-tabs-link-hover-border-color:          @gray-lighter;
+@nav-tabs-link-hover-border-color: @gray-lighter;
 
-@nav-tabs-active-link-hover-bg:             @body-bg;
-@nav-tabs-active-link-hover-color:          @gray;
-@nav-tabs-active-link-hover-border-color:   #ddd;
+@nav-tabs-active-link-hover-bg: @body-bg;
+@nav-tabs-active-link-hover-color: @gray;
+@nav-tabs-active-link-hover-border-color: #ddd;
 
-@nav-tabs-justified-link-border-color:            #ddd;
-@nav-tabs-justified-active-link-border-color:     @body-bg;
+@nav-tabs-justified-link-border-color: #ddd;
+@nav-tabs-justified-active-link-border-color: @body-bg;
 
 //== Pills
-@nav-pills-border-radius:                   @border-radius-base;
-@nav-pills-active-link-hover-bg:            @component-active-bg;
-@nav-pills-active-link-hover-color:         @component-active-color;
-
+@nav-pills-border-radius: @border-radius-base;
+@nav-pills-active-link-hover-bg: @component-active-bg;
+@nav-pills-active-link-hover-color: @component-active-color;
 
 //== Pagination
 //
 //##
 
-@pagination-color:                     @link-color;
-@pagination-bg:                        #fff;
-@pagination-border:                    #ddd;
+@pagination-color: @link-color;
+@pagination-bg: #fff;
+@pagination-border: #ddd;
 
-@pagination-hover-color:               @link-hover-color;
-@pagination-hover-bg:                  @gray-lighter;
-@pagination-hover-border:              #ddd;
+@pagination-hover-color: @link-hover-color;
+@pagination-hover-bg: @gray-lighter;
+@pagination-hover-border: #ddd;
 
-@pagination-active-color:              @gray-light;
-@pagination-active-bg:                 #f5f5f5;
-@pagination-active-border:             @pagination-hover-border;
-
-@pagination-disabled-color:            @gray-light;
-@pagination-disabled-bg:               #fff;
-@pagination-disabled-border:           #ddd;
+@pagination-active-color: @gray-light;
+@pagination-active-bg: #f5f5f5;
+@pagination-active-border: @pagination-hover-border;
 
+@pagination-disabled-color: @gray-light;
+@pagination-disabled-bg: #fff;
+@pagination-disabled-border: #ddd;
 
 //== Pager
 //
 //##
 
-@pager-bg:                             @pagination-bg;
-@pager-border:                         @pagination-border;
-@pager-border-radius:                  15px;
-
-@pager-hover-bg:                       @pagination-hover-bg;
+@pager-bg: @pagination-bg;
+@pager-border: @pagination-border;
+@pager-border-radius: 15px;
 
-@pager-active-bg:                      @pagination-active-bg;
-@pager-active-color:                   @pagination-active-color;
+@pager-hover-bg: @pagination-hover-bg;
 
-@pager-disabled-color:                 @gray-light;
+@pager-active-bg: @pagination-active-bg;
+@pager-active-color: @pagination-active-color;
 
+@pager-disabled-color: @gray-light;
 
 //== Jumbotron
 //
 //##
 
-@jumbotron-padding:              30px;
-@jumbotron-color:                inherit;
-@jumbotron-bg:                   @gray-lighter;
-@jumbotron-heading-color:        inherit;
-@jumbotron-font-size:            ceil((@font-size-base * 1.5));
-@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
-
+@jumbotron-padding: 30px;
+@jumbotron-color: inherit;
+@jumbotron-bg: @gray-lighter;
+@jumbotron-heading-color: inherit;
+@jumbotron-font-size: ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size: ceil((@font-size-base * 4.5));
 
 //== Form states and alerts
 //
 //## Define colors for form feedback states and, by default, alerts.
 
-@state-success-text:             #468847;
-@state-success-bg:               #dff0d8;
-@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
-
-@state-info-text:                #3a87ad;
-@state-info-bg:                  #d9edf7;
-@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
+@state-success-text: #468847;
+@state-success-bg: #dff0d8;
+@state-success-border: darken(spin(@state-success-bg, -10), 5%);
 
-@state-warning-text:             #c09853;
-@state-warning-bg:               #fcf8e3;
-@state-warning-border:           darken(spin(@state-warning-bg, -10), 3%);
+@state-info-text: #3a87ad;
+@state-info-bg: #d9edf7;
+@state-info-border: darken(spin(@state-info-bg, -10), 7%);
 
-@state-danger-text:              #b94a48;
-@state-danger-bg:                #f2dede;
-@state-danger-border:            darken(spin(@state-danger-bg, -10), 3%);
+@state-warning-text: #c09853;
+@state-warning-bg: #fcf8e3;
+@state-warning-border: darken(spin(@state-warning-bg, -10), 3%);
 
+@state-danger-text: #b94a48;
+@state-danger-bg: #f2dede;
+@state-danger-border: darken(spin(@state-danger-bg, -10), 3%);
 
 //== Tooltips
 //
 //##
 
 //** Tooltip max width
-@tooltip-max-width:           200px;
+@tooltip-max-width: 200px;
 //** Tooltip text color
-@tooltip-color:               #fff;
+@tooltip-color: #fff;
 //** Tooltip background color
-@tooltip-bg:                  #000;
-@tooltip-opacity:             .9;
+@tooltip-bg: #000;
+@tooltip-opacity: 0.9;
 
 //** Tooltip arrow width
-@tooltip-arrow-width:         5px;
+@tooltip-arrow-width: 5px;
 //** Tooltip arrow color
-@tooltip-arrow-color:         @tooltip-bg;
-
+@tooltip-arrow-color: @tooltip-bg;
 
 //== Popovers
 //
 //##
 
 //** Popover body background color
-@popover-bg:                          #fff;
+@popover-bg: #fff;
 //** Popover maximum width
-@popover-max-width:                   276px;
+@popover-max-width: 276px;
 //** Popover border color
-@popover-border-color:                rgba(0,0,0,.2);
+@popover-border-color: rgba(0, 0, 0, 0.2);
 //** Popover fallback border color
-@popover-fallback-border-color:       #ccc;
+@popover-fallback-border-color: #ccc;
 
 //** Popover title background color
-@popover-title-bg:                    darken(@popover-bg, 3%);
+@popover-title-bg: darken(@popover-bg, 3%);
 
 //** Popover arrow width
-@popover-arrow-width:                 10px;
+@popover-arrow-width: 10px;
 //** Popover arrow color
-@popover-arrow-color:                 @popover-bg;
+@popover-arrow-color: @popover-bg;
 
 //** Popover outer arrow width
-@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+@popover-arrow-outer-width: (@popover-arrow-width + 1);
 //** Popover outer arrow color
-@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
+@popover-arrow-outer-color: fadein(@popover-border-color, 5%);
 //** Popover outer arrow fallback color
-@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
-
+@popover-arrow-outer-fallback-color: darken(
+  @popover-fallback-border-color,
+  20%
+);
 
 //== Labels
 //
 //##
 
 //** Default label background color
-@label-default-bg:            @gray-light;
+@label-default-bg: @gray-light;
 //** Primary label background color
-@label-primary-bg:            @brand-primary;
+@label-primary-bg: @brand-primary;
 //** Success label background color
-@label-success-bg:            @brand-success;
+@label-success-bg: @brand-success;
 //** Info label background color
-@label-info-bg:               @brand-info;
+@label-info-bg: @brand-info;
 //** Warning label background color
-@label-warning-bg:            @brand-warning;
+@label-warning-bg: @brand-warning;
 //** Danger label background color
-@label-danger-bg:             @brand-danger;
+@label-danger-bg: @brand-danger;
 
 //** Default label text color
-@label-color:                 #fff;
+@label-color: #fff;
 //** Default text color of a linked label
-@label-link-hover-color:      #fff;
-
+@label-link-hover-color: #fff;
 
 //== Modals
 //
 //##
 
 //** Padding applied to the modal body
-@modal-inner-padding:         20px;
+@modal-inner-padding: 20px;
 
 //** Padding applied to the modal title
-@modal-title-padding:         15px;
+@modal-title-padding: 15px;
 //** Modal title line-height
-@modal-title-line-height:     @line-height-base;
+@modal-title-line-height: @line-height-base;
 
 //** Background color of modal content area
-@modal-content-bg:                             #fff;
+@modal-content-bg: #fff;
 //** Modal content border color
-@modal-content-border-color:                   rgba(0,0,0,.2);
+@modal-content-border-color: rgba(0, 0, 0, 0.2);
 //** Modal content border color **for IE8**
-@modal-content-fallback-border-color:          #999;
+@modal-content-fallback-border-color: #999;
 
 //** Modal backdrop background color
-@modal-backdrop-bg:           #000;
+@modal-backdrop-bg: #000;
 //** Modal backdrop opacity
-@modal-backdrop-opacity:      .5;
+@modal-backdrop-opacity: 0.5;
 //** Modal header border color
-@modal-header-border-color:   #e5e5e5;
+@modal-header-border-color: #e5e5e5;
 //** Modal footer border color
-@modal-footer-border-color:   @modal-header-border-color;
-
-@modal-lg:                    900px;
-@modal-md:                    600px;
-@modal-sm:                    300px;
+@modal-footer-border-color: @modal-header-border-color;
 
+@modal-lg: 900px;
+@modal-md: 600px;
+@modal-sm: 300px;
 
 //== Alerts
 //
 //## Define alert colors, border radius, and padding.
 
-@alert-padding:               15px;
-@alert-border-radius:         @border-radius-base;
-@alert-link-font-weight:      bold;
-
-@alert-success-bg:            @state-success-bg;
-@alert-success-text:          @state-success-text;
-@alert-success-border:        @state-success-border;
+@alert-padding: 15px;
+@alert-border-radius: @border-radius-base;
+@alert-link-font-weight: bold;
 
-@alert-info-bg:               @state-info-bg;
-@alert-info-text:             @state-info-text;
-@alert-info-border:           @state-info-border;
+@alert-success-bg: @state-success-bg;
+@alert-success-text: @state-success-text;
+@alert-success-border: @state-success-border;
 
-@alert-warning-bg:            @state-warning-bg;
-@alert-warning-text:          @state-warning-text;
-@alert-warning-border:        @state-warning-border;
+@alert-info-bg: @state-info-bg;
+@alert-info-text: @state-info-text;
+@alert-info-border: @state-info-border;
 
-@alert-danger-bg:             @state-danger-bg;
-@alert-danger-text:           @state-danger-text;
-@alert-danger-border:         @state-danger-border;
+@alert-warning-bg: @state-warning-bg;
+@alert-warning-text: @state-warning-text;
+@alert-warning-border: @state-warning-border;
 
+@alert-danger-bg: @state-danger-bg;
+@alert-danger-text: @state-danger-text;
+@alert-danger-border: @state-danger-border;
 
 //== Progress bars
 //
 //##
 
 //** Background color of the whole progress component
-@progress-bg:                 #f5f5f5;
+@progress-bg: #f5f5f5;
 //** Progress bar text color
-@progress-bar-color:          #fff;
+@progress-bar-color: #fff;
 //** Variable for setting rounded corners on progress bar.
-@progress-border-radius:      @border-radius-base;
+@progress-border-radius: @border-radius-base;
 
 //** Default progress bar color
-@progress-bar-bg:             @brand-primary;
+@progress-bar-bg: @brand-primary;
 //** Success progress bar color
-@progress-bar-success-bg:     @brand-success;
+@progress-bar-success-bg: @brand-success;
 //** Warning progress bar color
-@progress-bar-warning-bg:     @brand-warning;
+@progress-bar-warning-bg: @brand-warning;
 //** Danger progress bar color
-@progress-bar-danger-bg:      @brand-danger;
+@progress-bar-danger-bg: @brand-danger;
 //** Info progress bar color
-@progress-bar-info-bg:        @brand-info;
-
+@progress-bar-info-bg: @brand-info;
 
 //== List group
 //
 //##
 
 //** Background color on `.list-group-item`
-@list-group-bg:                 #fff;
+@list-group-bg: #fff;
 //** `.list-group-item` border color
-@list-group-border:             #ddd;
+@list-group-border: #ddd;
 //** List group border radius
-@list-group-border-radius:      @border-radius-base;
+@list-group-border-radius: @border-radius-base;
 
 //** Background color of single list items on hover
-@list-group-hover-bg:           #f5f5f5;
+@list-group-hover-bg: #f5f5f5;
 //** Text color of active list items
-@list-group-active-color:       @component-active-color;
+@list-group-active-color: @component-active-color;
 //** Background color of active list items
-@list-group-active-bg:          @component-active-bg;
+@list-group-active-bg: @component-active-bg;
 //** Border color of active list elements
-@list-group-active-border:      @list-group-active-bg;
+@list-group-active-border: @list-group-active-bg;
 //** Text color for content within active list items
-@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+@list-group-active-text-color: lighten(@list-group-active-bg, 40%);
 
 //** Text color of disabled list items
-@list-group-disabled-color:      @gray-light;
+@list-group-disabled-color: @gray-light;
 //** Background color of disabled list items
-@list-group-disabled-bg:         @gray-lighter;
+@list-group-disabled-bg: @gray-lighter;
 //** Text color for content within disabled list items
 @list-group-disabled-text-color: @list-group-disabled-color;
 
-@list-group-link-color:         #555;
-@list-group-link-hover-color:   @list-group-link-color;
+@list-group-link-color: #555;
+@list-group-link-hover-color: @list-group-link-color;
 @list-group-link-heading-color: #333;
 
-
 //== Panels
 //
 //##
 
-@panel-bg:                    #fff;
-@panel-body-padding:          15px;
-@panel-heading-padding:       10px 15px;
-@panel-footer-padding:        @panel-heading-padding;
-@panel-border-radius:         @border-radius-base;
+@panel-bg: #fff;
+@panel-body-padding: 15px;
+@panel-heading-padding: 10px 15px;
+@panel-footer-padding: @panel-heading-padding;
+@panel-border-radius: @border-radius-base;
 
 //** Border color for elements within panels
-@panel-inner-border:          #ddd;
-@panel-footer-bg:             #f5f5f5;
+@panel-inner-border: #ddd;
+@panel-footer-bg: #f5f5f5;
 
-@panel-default-text:          @text-color;
-@panel-default-border:        #ddd;
-@panel-default-heading-bg:    #f5f5f5;
+@panel-default-text: @text-color;
+@panel-default-border: #ddd;
+@panel-default-heading-bg: #f5f5f5;
 
-@panel-primary-text:          #fff;
-@panel-primary-border:        @panel-default-border;
-@panel-primary-heading-bg:    @brand-primary;
+@panel-primary-text: #fff;
+@panel-primary-border: @panel-default-border;
+@panel-primary-heading-bg: @brand-primary;
 
-@panel-success-text:          @state-success-text;
-@panel-success-border:        @panel-default-border;
-@panel-success-heading-bg:    @brand-success;
+@panel-success-text: @state-success-text;
+@panel-success-border: @panel-default-border;
+@panel-success-heading-bg: @brand-success;
 
-@panel-info-text:             @state-info-text;
-@panel-info-border:           @panel-default-border;
-@panel-info-heading-bg:       @brand-info;
+@panel-info-text: @state-info-text;
+@panel-info-border: @panel-default-border;
+@panel-info-heading-bg: @brand-info;
 
-@panel-warning-text:          @state-warning-text;
-@panel-warning-border:        @panel-default-border;
-@panel-warning-heading-bg:    @brand-warning;
-
-@panel-danger-text:           @state-danger-text;
-@panel-danger-border:         @panel-default-border;
-@panel-danger-heading-bg:     @brand-danger;
+@panel-warning-text: @state-warning-text;
+@panel-warning-border: @panel-default-border;
+@panel-warning-heading-bg: @brand-warning;
 
+@panel-danger-text: @state-danger-text;
+@panel-danger-border: @panel-default-border;
+@panel-danger-heading-bg: @brand-danger;
 
 //== Thumbnails
 //
 //##
 
 //** Padding around the thumbnail image
-@thumbnail-padding:           4px;
+@thumbnail-padding: 4px;
 //** Thumbnail background color
-@thumbnail-bg:                @body-bg;
+@thumbnail-bg: @body-bg;
 //** Thumbnail border color
-@thumbnail-border:            #ddd;
+@thumbnail-border: #ddd;
 //** Thumbnail border radius
-@thumbnail-border-radius:     @border-radius-base;
+@thumbnail-border-radius: @border-radius-base;
 
 //** Custom text color for thumbnail captions
-@thumbnail-caption-color:     @text-color;
+@thumbnail-caption-color: @text-color;
 //** Padding around the thumbnail caption
-@thumbnail-caption-padding:   9px;
-
+@thumbnail-caption-padding: 9px;
 
 //== Wells
 //
 //##
 
-@well-bg:                     #fff;
-@well-border:                 darken(@well-bg, 15%); //#84878A;
-
+@well-bg: #fff;
+@well-border: darken(@well-bg, 15%); //#84878A;
 
 //== Badges
 //
 //##
 
-@badge-color:                 #fff;
+@badge-color: #fff;
 //** Linked badge text color on hover
-@badge-link-hover-color:      #fff;
-@badge-bg:                    @brand-primary;
+@badge-link-hover-color: #fff;
+@badge-bg: @brand-primary;
 
 //** Badge text color in active nav link
-@badge-active-color:          @link-color;
+@badge-active-color: @link-color;
 //** Badge background color in active nav link
-@badge-active-bg:             #fff;
-
-@badge-font-weight:           bold;
-@badge-line-height:           1;
-@badge-border-radius:         10px;
+@badge-active-bg: #fff;
 
+@badge-font-weight: bold;
+@badge-line-height: 1;
+@badge-border-radius: 10px;
 
 //== Breadcrumbs
 //
 //##
 
-@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-vertical: 8px;
 @breadcrumb-padding-horizontal: 15px;
 //** Breadcrumb background color
-@breadcrumb-bg:                 #f5f5f5;
+@breadcrumb-bg: #f5f5f5;
 //** Breadcrumb text color
-@breadcrumb-color:              #ccc;
+@breadcrumb-color: #ccc;
 //** Text color of current page in the breadcrumb
-@breadcrumb-active-color:       @gray-light;
+@breadcrumb-active-color: @gray-light;
 //** Textual separator for between breadcrumb elements
-@breadcrumb-separator:          "/";
-
+@breadcrumb-separator: '/';
 
 //== Carousel
 //
 //##
 
-@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
-
-@carousel-control-color:                      #fff;
-@carousel-control-width:                      15%;
-@carousel-control-opacity:                    .5;
-@carousel-control-font-size:                  20px;
+@carousel-text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
 
-@carousel-indicator-active-bg:                #fff;
-@carousel-indicator-border-color:             #fff;
+@carousel-control-color: #fff;
+@carousel-control-width: 15%;
+@carousel-control-opacity: 0.5;
+@carousel-control-font-size: 20px;
 
-@carousel-caption-color:                      #fff;
+@carousel-indicator-active-bg: #fff;
+@carousel-indicator-border-color: #fff;
 
+@carousel-caption-color: #fff;
 
 //== Close
 //
 //##
 
-@close-font-weight:           bold;
-@close-color:                 #000;
-@close-text-shadow:           0 1px 0 #fff;
-
+@close-font-weight: bold;
+@close-color: #000;
+@close-text-shadow: 0 1px 0 #fff;
 
 //== Code
 //
 //##
 
-@code-color:                  #c7254e;
-@code-bg:                     #f9f2f4;
-
-@kbd-color:                   #fff;
-@kbd-bg:                      #333;
+@code-color: #c7254e;
+@code-bg: #f9f2f4;
 
-@pre-bg:                      #f5f5f5;
-@pre-color:                   @gray-dark;
-@pre-border-color:            #ccc;
-@pre-scrollable-max-height:   340px;
+@kbd-color: #fff;
+@kbd-bg: #333;
 
+@pre-bg: #f5f5f5;
+@pre-color: @gray-dark;
+@pre-border-color: #ccc;
+@pre-scrollable-max-height: 340px;
 
 //== Type
 //
@@ -855,22 +830,22 @@
 //** Horizontal offset for forms and lists.
 @component-offset-horizontal: 180px;
 //** Text muted color
-@text-muted:                  @gray-light;
+@text-muted: @gray-light;
 //** Abbreviations and acronyms border color
-@abbr-border-color:           @gray-light;
+@abbr-border-color: @gray-light;
 //** Headings small color
-@headings-small-color:        @gray-light;
+@headings-small-color: @gray-light;
 //** Blockquote small color
-@blockquote-small-color:      @gray-light;
+@blockquote-small-color: @gray-light;
 //** Blockquote font size
-@blockquote-font-size:        (@font-size-base * 1.25);
+@blockquote-font-size: (@font-size-base * 1.25);
 //** Blockquote border color
-@blockquote-border-color:     @gray-lighter;
+@blockquote-border-color: @gray-lighter;
 //** Page header border color
-@page-header-border-color:    @gray-lighter;
+@page-header-border-color: @gray-lighter;
 //** Width of horizontal description list titles
-@dl-horizontal-offset:        @component-offset-horizontal;
+@dl-horizontal-offset: @component-offset-horizontal;
 //** Point at which .dl-horizontal becomes horizontal
-@dl-horizontal-breakpoint:    @grid-float-breakpoint;
+@dl-horizontal-breakpoint: @grid-float-breakpoint;
 //** Horizontal line color.
-@hr-border:                   @gray-lighter;
+@hr-border: @gray-lighter;
diff --git a/frontend/less/website.less b/frontend/less/website.less
index eddaec3..ef14a82 100644
--- a/frontend/less/website.less
+++ b/frontend/less/website.less
@@ -1,9 +1,9 @@
-.centeredbtn {
-	margin-top: 80px;
-	p {
-		margin-top: 25px;
-	}
-	.btn {
-		width: 220px;
-	}
-}
+.centeredbtn {
+  margin-top: 80px;
+  p {
+    margin-top: 25px;
+  }
+  .btn {
+    width: 220px;
+  }
+}
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
index b2b139c..165cf1d 100644
--- a/frontend/locales/en.json
+++ b/frontend/locales/en.json
@@ -1,279 +1,279 @@
 {
-    "__currency_code": "USD",
-    "__currency_symbol": "$",
-    "[code: ]": "[code: {{code}}]",
-    "#": "#{{number}}",
-    "'s documents": "{{name}}'s documents",
-    "ATI": "ATI",
-    "A technical issue has occurred (-_-')": "A technical issue has occurred (-_-')",
-    "Account": "Account",
-    "Accounting": "Accounting",
-    "Add": "Add",
-    "Add a contact": "Add a contact",
-    "Add a property": "Add a property",
-    "Add a payment": "Add a payment",
-    "Additional cost": "Additional cost",
-    "Additional cost on current rent": "Additional cost on current rent",
-    "Address": "Address",
-    "Address where invoices and rent notices will be sent": "Address where invoices and rent notices will be sent",
-    "Administrator": "Administrator",
-    "All": "All",
-    "Amount": "Amount",
-    "Amount of deposit refund": "Amount of deposit refund",
-    "Amount without VAT": "Amount without VAT",
-    "Are you sure to remove this tenant?": "Are you sure to remove this tenant?",
-    "Are you sure to remove this property?": "Are you sure to remove this property?",
-    "Are you sure to send invoices by email?": "Are you sure to send invoices by email?",
-    "Are you sure to send rent notices by email?": "Are you sure to send rent notices by email?",
-    "Area": "Area",
-    "Available": "Available",
-    "Available_plural": "Availables",
-    "Bad password": "Bad password",
-    "Balance": "Balance",
-    "Bank name": "Bank name",
-    "Bank on which payments shall be made": "Bank on which payments shall be made",
-    "Banking establishment": "Banking establishment",
-    "Billing": "Billing",
-    "Billing address": "Billing address",
-    "Building": "Bâtiment",
-    "Business entity": "Business entity",
-    "Business lease contract 3,6,9 years": "Business lease contract 3,6,9 years",
-    "Business lease contract": "Business lease contract",
-    "Business lease contract terminable after 3,6,9 years": "Business lease contract terminable after 3,6,9 years",
-    "By clicking Register, you accept the": "By clicking Register, you accept the",
-    "CAM Fees": "CAM Fees",
-    "Cancel": "Cancel",
-    "Cancel selection": "Cancel selection",
-    "Car park": "Car park",
-    "Caution": "Caution",
-    "Certificate of deposit": "Certificate of deposit",
-    "City": "City",
-    "Comments": "Comments",
-    "Comments that will not appear on the rent invoice": "Comments that will not appear on the rent invoice",
-    "Company": "Company",
-    "Company name": "Company name",
-    "Complete the fields to specify the terms of the agreement with the tenant": "Complete the fields to specify the terms of the agreement with the tenant",
-    "Contact person for all administrative procedures (payment of rents, claims ...)": "Contact person for all administrative procedures (payment of rents, claims ...)",
-    "Customer Identification Number": "Customer Identification Number",
-    "cash": "cash",
-    "cheque": "cheque",
-    "Contact": "Contact",
-    "Contacts": "Contacts",
-    "Contract": "Contract",
-    "Create your account in few clicks": "Create your account in few clicks",
-    "Domiciliation contract": "Domiciliation contract",
-    "Domiciliation contract (mailbox rental)": "Domiciliation contract (mailbox rental)",
-    "Dashboard": "Dashboard",
-    "Date": "Date",
-    "Demonstration": "Demonstration",
-    "Deposit": "Deposit",
-    "Deposit amount": "Deposit amount",
-    "Deposit refund": "Deposit refund",
-    "Describe the property to rent": "Describe the property to rent",
-    "Description": "Description",
-    "Discount": "Discount",
-    "Discount on current rent": "Discount on current rent",
-    "Documents": "Documents",
-    "DOS": "DOS",
-    "emails cannot be sent in demo mode": "emails cannot be sent in demo mode",
-    "E-mail": "E-mail",
-    "Edit": "Edit",
-    "Effective manager (first and last name)": "Effective manager (first and last name)",
-    "End date": "End date",
-    "ECAMEVAT": "ECAMEVAT",
-    "Entries": "Entries",
-    "Entry date": "Entry date",
-    "Exit date": "Exit date",
-    "Exits": "Exits",
-    "Expiration date": "Expiration date",
-    "Excluding Common Area Maintenance fees Excluding VAT": "Excluding Common Area Maintenance fees Excluding VAT",
-    "EVAT": "EVAT",
-    "Excluding VAT": "Excluding VAT",
-    "Fill the empty fields before printing the document": "Fill the empty fields before printing the document",
-    "First and last name": "First and last name",
-    "First name": "First name",
-    "Forgot password?": "Forgot password?",
-    "Free subscription": "Free subscription",
-    "Friendly reminder for rent payment" : "Friendly reminder for rent payment",
-    "Get started - it's free.": "Get started - it's free.",
-    "has expired": "{{document}} has expired the {{date}}",
-    "Hello": "Hello",
-    "impossible to send document to": "impossible to send document to",
-    "IBAN": "IBAN",
-    "ICAMEVAT": "ICAMEVAT",
-    "ICAMIVAT": "ICAMIVAT",
-    "Including Common Area Maintenance fees Excluding VAT": "Including Common Area Maintenance fees Excluding VAT",
-    "Including Common Area Maintenance fees Including VAT": "Including Common Area Maintenance fees Including VAT",
-    "Indicate the date, the amount, and the type of payment": "Indicate the date, the amount, and the type of payment",
-    "Indicate the effective date of the lease termination": "Indicate the effective date of the lease termination",
-    "Information about the monthly rent": "Information about the monthly rent",
-    "Internal note": "Internal note",
-    "Internal server error": "Internal server error",
-    "Individual": "Individual",
-    "Inventory": "Inventory",
-    "Invoice stating the payment of the rent": "Invoice stating the payment of the rent",
-    "Issue": "Issue",
-    "It's going OK! No problems (^_^)": "It's going OK! No problems (^_^)",
-    "Label": "Label",
-    "Landloard": "Landloard",
-    "Landloard information": "Landloard information",
-    "Last name": "Last name",
-    "Last rent notice reminder": "Last rent notice reminder",
-    "late month": "{{count}} month late",
-    "late month_plural": "{{count}} months late",
-    "Late rent - First reminder": "Late rent - First reminder",
-    "Late rent - Second reminder": "Late rent - Second reminder",
-    "Late rent - Last reminder": "Late rent - Last reminder",
-    "Lease": "Lease",
-    "Lease_plural": "Leases",
-    "Lease broken": "Lease broken",
-    "Lease contract": "Lease contract",
-    "Lease in progress": "Lease in progress",
-    "Lease terminated": "Lease terminated",
-    "Lease termination": "Lease termination",
-    "Leased": "Leased",
-    "Leased_plural": "Leased",
-    "Leased by": "Leased by {{name}}",
-    "Leased properties": "Leased properties",
-    "Letterbox": "Letterbox",
-    "Letter for requesting the insurance certificate - evidence of insurance": "Letter for requesting the insurance certificate - evidence of insurance",
-    "Letter for requesting the payment of the deposit": "Letter for requesting the payment of the deposit",
-    "Letter that certifies the payment of deposit by the tenant": "Letter that certifies the payment of deposit by the tenant",
-    "Level": "Level",
-    "levy": "levy",
-    "Loading": "Loading",
-    "Location": "Location",
-    "Manage documents": "Manage documents",
-    "Manage your real estates": "Manage your real estates",
-    "Month": "Month",
-    "month": "{{count}} month",
-    "month_plural": "{{count}} months",
-    "Monthly CAM amount without VAT": "Charges mensuelles hors taxes",
-    "Monthly rent amount without CAM without VAT": "Monthly rent amount without CAM without VAT",
-    "no emails defined for tenant": "no emails defined for tenant",
-    "New": "New",
-    "New organization": "New organization",
-    "Next": "Next",
-    "No": "No",
-    "No account yet?": "No account yet?",
-    "Not paid": "Not paid",
-    "Notice letter for rent payment": "Notice letter for rent payment",
-    "Only the payment of rent period are authorized. Please enter a date between": "Only the rent payment of {{period}} is authorized. The payment date must be between {{minDate}} and {{maxDate}}",
-    "Order to pay or vacate":  "Order to pay or vacate",
-    "Organization": "Organization",
-    "Organizations": "Organizations",
-    "Organization name": "Organization name",
-    "Ownership equity": "Ownership equity",
-    "Page not found on server": "Page not found on server",
-    "Paid": "Paid",
-    "Paid_plural": "Paid",
-    "Paid date": "Paid date: {{date}}",
-    "Partially paid": "Partially paid",
-    "Password": "Password",
-    "Payment": "Payment",
-    "Payments": "Payments",
-    "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)": "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)",
-    "Period": "Period",
-    "Phone number": "Phone number",
-    "Please enter a valid phone number": "Please enter a valid phone number",
-    "Please fill missing fields": "Please fill missing fields",
-    "Please set a date after the": "Please set a date after the {{date}}",
-    "Please set a date before the": "Please set a date before the {{date}}",
-    "Previous": "Previous",
-    "Print": "Print",
-    "Print and fill this form before giving the keys to the tenant": "Print and fill this form before giving the keys to the tenant",
-    "Problem during data decoding [JSON]": "Problem during data decoding [JSON]",
-    "Properties": "Properties",
-    "Property": "Property",
-    "Property_plural": "Properties",
-    "Property to rent": "Property to rent",
-    "Real estate management": "Real estate management",
-    "Reason of additional cost": "Reason of additional cost",
-    "Reason for reduction": "Reason for discount",
-    "Recovery of unpaid rents": "Recovery of unpaid rents",
-    "Reference": "Reference",
-    "Remove": "Remove",
-    "Rent": "Rent",
-    "Rent_plural": "Rents",
-    "Rental invoice": "Rental invoice",
-    "Rental invoices": "Rental invoices",
-    "Rents": "Rents",
-    "Rent information": "Rent information",
-    "Rent notice": "Rent notice",
-    "Rent notice reminder": "Rent notice reminder",
-    "Rent to pay": "Rent to pay",
-    "Rent with CAM fees": "Rent with CAM fees",
-    "Rent with CAM fees:": "Rent with CAM fees: {{amount}}",
-    "Rent with CAM fees tenanted by": "Rent with CAM fees: {{amount}} tenanted by {{name}}",
-    "Rent without CAM fees": "Rent without CAM fees",
-    "Request cancelled on server": "Request cancelled on server",
-    "Request of certificate of deposit": "Request of certificate of deposit",
-    "Request of insurance certificate": "Request of insurance certificate",
-    "Request rent recovery by a third party (company, justice)": "Request rent recovery by a third party (company, justice)",
-    "Residence lease contract 9 years": "Residence lease contract 9 years",
-    "Room": "Room",
-    "successfuly sent to": "successfuly sent to",
-    "Save": "Save",
-    "Select properties to rent": "Select properties to rent",
-    "Selected tenant": "Selected tenant",
-    "Selected tenant_plural": "Selected tenants",
-    "Selected rent": "Selected rent",
-    "Selected rent_plural": "Selected rents",
-    "Selected property": "Selected property",
-    "Selected property_plural": "Selected properties",
-    "Sent to": "Sent to {{email}} on {{datetime}}",
-    "Server access problem. Check your network connection": "Server access problem. Check your network connection",
-    "Server is taking too long to reply": "Server is taking too long to reply",
-    "Short business lease contract": "Short business lease contract",
-    "Short business lease contract maximum duration 2 years": "Short business lease contract maximum duration 2 years",
-    "Sign in": "Sign in",
-    "Sign in now": "Sign in now",
-    "Sign out": "Sign out",
-    "Site data is reset every 30 minutes": "Site data is reset every 30 minutes",
-    "Specify the elements for the establishment of invoices and rent notices": "Specify the elements for the establishment of invoices and rent notices",
-    "Status": "Status",
-    "Start date": "Start date",
-    "Subject to VAT": "Subject to VAT",
-    "Register": "S'inscrire",
-    "Tenant": "Tenant",
-    "Tenant_plural": "Tenants",
-    "Tenants": "Tenants",
-    "Tenant information": "Tenant information",
-    "Term of use": "Term of use",
-    "Terminated lease": "Terminated lease",
-    "Terminated lease_plural": "Terminated leases",
-    "The date is not valid (Sample date:)": "The date is not valid (Sample date: {{date}})",
-    "The end date of contract is not compatible with contract selected": "The end date of contract is not compatible with contract selected",
-    "The form is not valid. Please check the field with error": "The form is not valid. Please check the field with error",
-    "The form is not valid. Please check the field with error_plural": "The form is not valid. Please check the fields with error",
-    "This user already exists": "This user already exists",
-    "This user does not manage any real estate accounts": "This user does not manage any real estate accounts",
-    "Timeline": "Timeline",
-    "Timeline rents": "Timeline rents",
-    "There are no documents attached to the lease contract. Is the insurance certficate is missing?": "There are no documents attached to the lease contract. Is the insurance certficate is missing?",
-    "transfer": "transfer",
-    "Total": "Total",
-    "Type": "Type",
-    "User": "User",
-    "VAT": "VAT",
-    "VAT Identification number": "VAT Identification number",
-    "VAT ratio": "VAT ratio",
-    "VAT ratio in %": "VAT ratio in %",
-    "UIN": "UIN",
-    "Uh-oh!": "Uh-oh!",
-    "Unfriendly reminder for rent payment" : "Unfriendly reminder for rent payment",
-    "Unknown user": "Unknown user",
-    "unknown": "unknown",
-    "Unknown error": "Unknown error",
-    "Warning": "Warning",
-    "with share capital of": "with share capital of",
-    "Where the property is located": "Where the property is located",
-    "Year": "Year",
-    "Yes": "Yes",
-    "You already have an account?": "You already have an account?",
-    "Your email address": "Your email address",
-    "Your mailings": "Your mailings",
-    "Your password": "Your password",
-    "Your session has expired, Please reconnect": "Your session has expired, Please reconnect",
-    "ZIP code": "ZIP code"
+  "__currency_code": "USD",
+  "__currency_symbol": "$",
+  "[code: ]": "[code: {{code}}]",
+  "#": "#{{number}}",
+  "'s documents": "{{name}}'s documents",
+  "ATI": "ATI",
+  "A technical issue has occurred (-_-')": "A technical issue has occurred (-_-')",
+  "Account": "Account",
+  "Accounting": "Accounting",
+  "Add": "Add",
+  "Add a contact": "Add a contact",
+  "Add a property": "Add a property",
+  "Add a payment": "Add a payment",
+  "Additional cost": "Additional cost",
+  "Additional cost on current rent": "Additional cost on current rent",
+  "Address": "Address",
+  "Address where invoices and rent notices will be sent": "Address where invoices and rent notices will be sent",
+  "Administrator": "Administrator",
+  "All": "All",
+  "Amount": "Amount",
+  "Amount of deposit refund": "Amount of deposit refund",
+  "Amount without VAT": "Amount without VAT",
+  "Are you sure to remove this tenant?": "Are you sure to remove this tenant?",
+  "Are you sure to remove this property?": "Are you sure to remove this property?",
+  "Are you sure to send invoices by email?": "Are you sure to send invoices by email?",
+  "Are you sure to send rent notices by email?": "Are you sure to send rent notices by email?",
+  "Area": "Area",
+  "Available": "Available",
+  "Available_plural": "Availables",
+  "Bad password": "Bad password",
+  "Balance": "Balance",
+  "Bank name": "Bank name",
+  "Bank on which payments shall be made": "Bank on which payments shall be made",
+  "Banking establishment": "Banking establishment",
+  "Billing": "Billing",
+  "Billing address": "Billing address",
+  "Building": "Bâtiment",
+  "Business entity": "Business entity",
+  "Business lease contract 3,6,9 years": "Business lease contract 3,6,9 years",
+  "Business lease contract": "Business lease contract",
+  "Business lease contract terminable after 3,6,9 years": "Business lease contract terminable after 3,6,9 years",
+  "By clicking Register, you accept the": "By clicking Register, you accept the",
+  "CAM Fees": "CAM Fees",
+  "Cancel": "Cancel",
+  "Cancel selection": "Cancel selection",
+  "Car park": "Car park",
+  "Caution": "Caution",
+  "Certificate of deposit": "Certificate of deposit",
+  "City": "City",
+  "Comments": "Comments",
+  "Comments that will not appear on the rent invoice": "Comments that will not appear on the rent invoice",
+  "Company": "Company",
+  "Company name": "Company name",
+  "Complete the fields to specify the terms of the agreement with the tenant": "Complete the fields to specify the terms of the agreement with the tenant",
+  "Contact person for all administrative procedures (payment of rents, claims ...)": "Contact person for all administrative procedures (payment of rents, claims ...)",
+  "Customer Identification Number": "Customer Identification Number",
+  "cash": "cash",
+  "cheque": "cheque",
+  "Contact": "Contact",
+  "Contacts": "Contacts",
+  "Contract": "Contract",
+  "Create your account in few clicks": "Create your account in few clicks",
+  "Domiciliation contract": "Domiciliation contract",
+  "Domiciliation contract (mailbox rental)": "Domiciliation contract (mailbox rental)",
+  "Dashboard": "Dashboard",
+  "Date": "Date",
+  "Demonstration": "Demonstration",
+  "Deposit": "Deposit",
+  "Deposit amount": "Deposit amount",
+  "Deposit refund": "Deposit refund",
+  "Describe the property to rent": "Describe the property to rent",
+  "Description": "Description",
+  "Discount": "Discount",
+  "Discount on current rent": "Discount on current rent",
+  "Documents": "Documents",
+  "DOS": "DOS",
+  "emails cannot be sent in demo mode": "emails cannot be sent in demo mode",
+  "E-mail": "E-mail",
+  "Edit": "Edit",
+  "Effective manager (first and last name)": "Effective manager (first and last name)",
+  "End date": "End date",
+  "ECAMEVAT": "ECAMEVAT",
+  "Entries": "Entries",
+  "Entry date": "Entry date",
+  "Exit date": "Exit date",
+  "Exits": "Exits",
+  "Expiration date": "Expiration date",
+  "Excluding Common Area Maintenance fees Excluding VAT": "Excluding Common Area Maintenance fees Excluding VAT",
+  "EVAT": "EVAT",
+  "Excluding VAT": "Excluding VAT",
+  "Fill the empty fields before printing the document": "Fill the empty fields before printing the document",
+  "First and last name": "First and last name",
+  "First name": "First name",
+  "Forgot password?": "Forgot password?",
+  "Free subscription": "Free subscription",
+  "Friendly reminder for rent payment": "Friendly reminder for rent payment",
+  "Get started - it's free.": "Get started - it's free.",
+  "has expired": "{{document}} has expired the {{date}}",
+  "Hello": "Hello",
+  "impossible to send document to": "impossible to send document to",
+  "IBAN": "IBAN",
+  "ICAMEVAT": "ICAMEVAT",
+  "ICAMIVAT": "ICAMIVAT",
+  "Including Common Area Maintenance fees Excluding VAT": "Including Common Area Maintenance fees Excluding VAT",
+  "Including Common Area Maintenance fees Including VAT": "Including Common Area Maintenance fees Including VAT",
+  "Indicate the date, the amount, and the type of payment": "Indicate the date, the amount, and the type of payment",
+  "Indicate the effective date of the lease termination": "Indicate the effective date of the lease termination",
+  "Information about the monthly rent": "Information about the monthly rent",
+  "Internal note": "Internal note",
+  "Internal server error": "Internal server error",
+  "Individual": "Individual",
+  "Inventory": "Inventory",
+  "Invoice stating the payment of the rent": "Invoice stating the payment of the rent",
+  "Issue": "Issue",
+  "It's going OK! No problems (^_^)": "It's going OK! No problems (^_^)",
+  "Label": "Label",
+  "Landloard": "Landloard",
+  "Landloard information": "Landloard information",
+  "Last name": "Last name",
+  "Last rent notice reminder": "Last rent notice reminder",
+  "late month": "{{count}} month late",
+  "late month_plural": "{{count}} months late",
+  "Late rent - First reminder": "Late rent - First reminder",
+  "Late rent - Second reminder": "Late rent - Second reminder",
+  "Late rent - Last reminder": "Late rent - Last reminder",
+  "Lease": "Lease",
+  "Lease_plural": "Leases",
+  "Lease broken": "Lease broken",
+  "Lease contract": "Lease contract",
+  "Lease in progress": "Lease in progress",
+  "Lease terminated": "Lease terminated",
+  "Lease termination": "Lease termination",
+  "Leased": "Leased",
+  "Leased_plural": "Leased",
+  "Leased by": "Leased by {{name}}",
+  "Leased properties": "Leased properties",
+  "Letterbox": "Letterbox",
+  "Letter for requesting the insurance certificate - evidence of insurance": "Letter for requesting the insurance certificate - evidence of insurance",
+  "Letter for requesting the payment of the deposit": "Letter for requesting the payment of the deposit",
+  "Letter that certifies the payment of deposit by the tenant": "Letter that certifies the payment of deposit by the tenant",
+  "Level": "Level",
+  "levy": "levy",
+  "Loading": "Loading",
+  "Location": "Location",
+  "Manage documents": "Manage documents",
+  "Manage your real estates": "Manage your real estates",
+  "Month": "Month",
+  "month": "{{count}} month",
+  "month_plural": "{{count}} months",
+  "Monthly CAM amount without VAT": "Charges mensuelles hors taxes",
+  "Monthly rent amount without CAM without VAT": "Monthly rent amount without CAM without VAT",
+  "no emails defined for tenant": "no emails defined for tenant",
+  "New": "New",
+  "New organization": "New organization",
+  "Next": "Next",
+  "No": "No",
+  "No account yet?": "No account yet?",
+  "Not paid": "Not paid",
+  "Notice letter for rent payment": "Notice letter for rent payment",
+  "Only the payment of rent period are authorized. Please enter a date between": "Only the rent payment of {{period}} is authorized. The payment date must be between {{minDate}} and {{maxDate}}",
+  "Order to pay or vacate": "Order to pay or vacate",
+  "Organization": "Organization",
+  "Organizations": "Organizations",
+  "Organization name": "Organization name",
+  "Ownership equity": "Ownership equity",
+  "Page not found on server": "Page not found on server",
+  "Paid": "Paid",
+  "Paid_plural": "Paid",
+  "Paid date": "Paid date: {{date}}",
+  "Partially paid": "Partially paid",
+  "Password": "Password",
+  "Payment": "Payment",
+  "Payments": "Payments",
+  "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)": "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)",
+  "Period": "Period",
+  "Phone number": "Phone number",
+  "Please enter a valid phone number": "Please enter a valid phone number",
+  "Please fill missing fields": "Please fill missing fields",
+  "Please set a date after the": "Please set a date after the {{date}}",
+  "Please set a date before the": "Please set a date before the {{date}}",
+  "Previous": "Previous",
+  "Print": "Print",
+  "Print and fill this form before giving the keys to the tenant": "Print and fill this form before giving the keys to the tenant",
+  "Problem during data decoding [JSON]": "Problem during data decoding [JSON]",
+  "Properties": "Properties",
+  "Property": "Property",
+  "Property_plural": "Properties",
+  "Property to rent": "Property to rent",
+  "Real estate management": "Real estate management",
+  "Reason of additional cost": "Reason of additional cost",
+  "Reason for reduction": "Reason for discount",
+  "Recovery of unpaid rents": "Recovery of unpaid rents",
+  "Reference": "Reference",
+  "Remove": "Remove",
+  "Rent": "Rent",
+  "Rent_plural": "Rents",
+  "Rental invoice": "Rental invoice",
+  "Rental invoices": "Rental invoices",
+  "Rents": "Rents",
+  "Rent information": "Rent information",
+  "Rent notice": "Rent notice",
+  "Rent notice reminder": "Rent notice reminder",
+  "Rent to pay": "Rent to pay",
+  "Rent with CAM fees": "Rent with CAM fees",
+  "Rent with CAM fees:": "Rent with CAM fees: {{amount}}",
+  "Rent with CAM fees tenanted by": "Rent with CAM fees: {{amount}} tenanted by {{name}}",
+  "Rent without CAM fees": "Rent without CAM fees",
+  "Request cancelled on server": "Request cancelled on server",
+  "Request of certificate of deposit": "Request of certificate of deposit",
+  "Request of insurance certificate": "Request of insurance certificate",
+  "Request rent recovery by a third party (company, justice)": "Request rent recovery by a third party (company, justice)",
+  "Residence lease contract 9 years": "Residence lease contract 9 years",
+  "Room": "Room",
+  "successfuly sent to": "successfuly sent to",
+  "Save": "Save",
+  "Select properties to rent": "Select properties to rent",
+  "Selected tenant": "Selected tenant",
+  "Selected tenant_plural": "Selected tenants",
+  "Selected rent": "Selected rent",
+  "Selected rent_plural": "Selected rents",
+  "Selected property": "Selected property",
+  "Selected property_plural": "Selected properties",
+  "Sent to": "Sent to {{email}} on {{datetime}}",
+  "Server access problem. Check your network connection": "Server access problem. Check your network connection",
+  "Server is taking too long to reply": "Server is taking too long to reply",
+  "Short business lease contract": "Short business lease contract",
+  "Short business lease contract maximum duration 2 years": "Short business lease contract maximum duration 2 years",
+  "Sign in": "Sign in",
+  "Sign in now": "Sign in now",
+  "Sign out": "Sign out",
+  "Site data is reset every 30 minutes": "Site data is reset every 30 minutes",
+  "Specify the elements for the establishment of invoices and rent notices": "Specify the elements for the establishment of invoices and rent notices",
+  "Status": "Status",
+  "Start date": "Start date",
+  "Subject to VAT": "Subject to VAT",
+  "Register": "S'inscrire",
+  "Tenant": "Tenant",
+  "Tenant_plural": "Tenants",
+  "Tenants": "Tenants",
+  "Tenant information": "Tenant information",
+  "Term of use": "Term of use",
+  "Terminated lease": "Terminated lease",
+  "Terminated lease_plural": "Terminated leases",
+  "The date is not valid (Sample date:)": "The date is not valid (Sample date: {{date}})",
+  "The end date of contract is not compatible with contract selected": "The end date of contract is not compatible with contract selected",
+  "The form is not valid. Please check the field with error": "The form is not valid. Please check the field with error",
+  "The form is not valid. Please check the field with error_plural": "The form is not valid. Please check the fields with error",
+  "This user already exists": "This user already exists",
+  "This user does not manage any real estate accounts": "This user does not manage any real estate accounts",
+  "Timeline": "Timeline",
+  "Timeline rents": "Timeline rents",
+  "There are no documents attached to the lease contract. Is the insurance certficate is missing?": "There are no documents attached to the lease contract. Is the insurance certficate is missing?",
+  "transfer": "transfer",
+  "Total": "Total",
+  "Type": "Type",
+  "User": "User",
+  "VAT": "VAT",
+  "VAT Identification number": "VAT Identification number",
+  "VAT ratio": "VAT ratio",
+  "VAT ratio in %": "VAT ratio in %",
+  "UIN": "UIN",
+  "Uh-oh!": "Uh-oh!",
+  "Unfriendly reminder for rent payment": "Unfriendly reminder for rent payment",
+  "Unknown user": "Unknown user",
+  "unknown": "unknown",
+  "Unknown error": "Unknown error",
+  "Warning": "Warning",
+  "with share capital of": "with share capital of",
+  "Where the property is located": "Where the property is located",
+  "Year": "Year",
+  "Yes": "Yes",
+  "You already have an account?": "You already have an account?",
+  "Your email address": "Your email address",
+  "Your mailings": "Your mailings",
+  "Your password": "Your password",
+  "Your session has expired, Please reconnect": "Your session has expired, Please reconnect",
+  "ZIP code": "ZIP code"
 }
diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json
index 0bdd5b9..a079c5a 100644
--- a/frontend/locales/fr.json
+++ b/frontend/locales/fr.json
@@ -1,279 +1,279 @@
 {
-    "__currency_code": "EUR",
-    "__currency_symbol": "€",
-    "[code: ]": "[code : {{code}}]",
-    "#": "n°{{number}}",
-    "'s documents": "Documents de {{name}}",
-    "ATI": "T.T.C.",
-    "A technical issue has occurred (-_-')": "Une erreur technique s'est produite (-_-')",
-    "Account": "Compte",
-    "Accounting": "Comptabilité",
-    "Add": "Ajouter",
-    "Add a contact": "Ajouter un contact",
-    "Add a property": "Ajouter un bien",
-    "Add a payment": "Ajouter un règlement",
-    "Additional cost": "Coût supplémentaire",
-    "Additional cost on current rent": "Coût supplémentaire sur le loyer courant",
-    "Address": "Adresse",
-    "Address where invoices and rent notices will be sent": "Adresse pour l'établissment des quittances et des avis d'échéances",
-    "Administrator": "Administrateur",
-    "All": "Tous",
-    "Amount": "Montant",
-    "Amount of deposit refund": "Montant du remboursement du dépôt de garantie",
-    "Amount without VAT": "Montant H.T.",
-    "Are you sure to remove this tenant?": "Êtes-vous sûr de vouloir supprimer ce locataire ?",
-    "Are you sure to remove this property?": "Êtes-vous sûr de vouloir supprimer ce bien ?",
-    "Are you sure to send invoices by email?": "Êtes-vous sûr de vouloir envoyer les quittances par email ?",
-    "Are you sure to send rent notices by email?": "Êtes-vous sûr de vouloir envoyer les avis d'échéances par email ?",
-    "Area": "Surface",
-    "Available": "Disponible",
-    "Available_plural": "Disponible",
-    "Bad password": "Mot de passe incorrect",
-    "Balance": "Solde ",
-    "Bank name": "Dénomination de la banque",
-    "Bank on which payments shall be made": "Banque sur laquelle les règlements seront effectués",
-    "Banking establishment": "Établissement bancaire",
-    "Billing": "Facturation",
-    "Billing address": "Adresse de facturation",
-    "Building": "Bâtiment",
-    "Business entity": "Forme juridique",
-    "Business lease contract 3,6,9 years": "Contrat de bail commercial 3, 6, 9 ans",
-    "Business lease contract": "Contrat de bail commercial",
-    "Business lease contract terminable after 3,6,9 years": "Contrat de bail commercial resiliable après 3, 6, 9 ans",
-    "By clicking Register, you accept the": "En cliquant sur S’inscrire, vous acceptez les",
-    "CAM Fees": "Charges",
-    "Cancel": "Annuler",
-    "Cancel selection": "Annuler la séléction",
-    "Car park": "Parking",
-    "Caution": "Avertissement",
-    "Certificate of deposit": "Attestation dépôt de garantie",
-    "City": "Ville",
-    "Comments": "Commentaires",
-    "Comments that will not appear on the rent invoice": "Commentaire n'apparaissant pas sur la quittance",
-    "Company": "Société",
-    "Company name": "Dénomination de la société",
-    "Complete the fields to specify the terms of the agreement with the tenant": "Compléter les champs pour préciser les modalités du contrat conclu avec le locataire",
-    "Contact person for all administrative procedures (payment of rents, claims ...)": "Personne à contacter pour toutes démarches administratives (règlements des loyers, réclamations...)",
-    "Customer Identification Number": "Référence client",
-    "cash": "espèce",
-    "cheque": "chèque",
-    "Contact": "Contact",
-    "Contacts": "Contacts",
-    "Contract": "Contrat",
-    "Create your account in few clicks": "Créez votre compte en quelques clics",
-    "Domiciliation contract": "Contrat de domiciliation",
-    "Domiciliation contract (mailbox rental)": "Contrat de domiciliation (location de boîte aux lettres)",
-    "Dashboard": "Tableau de bord",
-    "Date": "Date",
-    "Demonstration": "Démonstration",
-    "Deposit": "Dépôt de garantie",
-    "Deposit amount": "Montant du dépôt de garantie",
-    "Deposit refund": "Remboursement dépôt de garantie",
-    "Describe the property to rent": "Décrire le bien à louer",
-    "Description": "Description",
-    "Discount": "Réduction",
-    "Discount on current rent": "Réduction sur le loyer courant",
-    "Documents": "Documents",
-    "DOS": "RCS",
-    "emails cannot be sent in demo mode": "les emails ne peuvent pas être envoyés en mode démo",
-    "E-mail": "E-mail",
-    "Edit": "Editer",
-    "Effective manager (first and last name)": "Responsable légal (prénom et nom)",
-    "End date": "Date de fin",
-    "ECAMEVAT": "H.C.H.T.",
-    "EVAT": "H.T.",
-    "Entries": "Entrées",
-    "Entry date": "Date d'entrée",
-    "Exits": "Sorties",
-    "Exit date": "Date de sortie",
-    "Excluding Common Area Maintenance fees Excluding VAT": "Hors Charges Hors Taxes",
-    "Excluding VAT": "Hors taxes",
-    "Expiration date": "Date d'expiration",
-    "Fill the empty fields before printing the document": "Remplir les champs vides avant l'impression du document",
-    "First and last name": "Prénom et nom",
-    "First name": "Prénom",
-    "Forgot password?": "Mot de passe oublié ?",
-    "Free subscription": "S'inscrire gratuitement",
-    "Friendly reminder for rent payment" : "Rappel amical pour le paiement du loyer",
-    "Get started - it's free.": "Lancez-vous, c’est gratuit.",
-    "has expired": "{{document}} a expiré le {{date}}",
-    "Hello": "Bonjour",
-    "impossible to send document to": "impossible d'envoyer le document à",
-    "IBAN": "IBAN",
-    "ICAMEVAT": "C.C.H.T.",
-    "ICAMIVAT": "C.C.T.T.C",
-    "Including Common Area Maintenance fees Excluding VAT": "Charges Comprises Hors Taxes",
-    "Including Common Area Maintenance fees Including VAT": "Charges Comprises Toutes Taxes Comprises",
-    "Indicate the date, the amount, and the type of payment": "Indiquer la date, le montant, et le mode de règlement",
-    "Indicate the effective date of the lease termination": "Indiquer la date d'effet de fin de bail",
-    "Information about the monthly rent": "Indications sur le loyer mensuel",
-    "Internal note": "Note interne",
-    "Internal server error": "Erreur interne du serveur",
-    "Individual": "Particulier",
-    "Inventory": "Etat des lieux",
-    "Invoice stating the payment of the rent": "Facture attestant le bon règlement du loyer",
-    "Issue": "Anomalie",
-    "It's going OK! No problems (^_^)": "Ca roule! pas de problèmes (^_^)",
-    "Label": "Intitulé",
-    "Landloard": "Propriétaire",
-    "Landloard information": "Information sur le propriétaire",
-    "Last name": "Nom",
-    "Last rent notice reminder": "Dernier rappel d'avis d'échéance",
-    "late month": "{{count}} mois de retard",
-    "late month_plural": "{{count}} mois de retard",
-    "Late rent - First reminder": "Loyer en retard - 1er rappel",
-    "Late rent - Second reminder": "Loyer en retard - 2ème rappel",
-    "Late rent - Last reminder": "Loyer en retard - Dernier rappel",
-    "Lease": "Bail",
-    "Lease_plural": "Baux",
-    "Lease broken": "Rupture de bail",
-    "Lease contract": "Contrat de bail",
-    "Lease in progress": "Bail en cours",
-    "Lease terminated": "Bail résilié",
-    "Lease termination": "Résiliation de bail",
-    "Leased": "Loué",
-    "Leased_plural": "Loués",
-    "Leased by": "Loué par {{name}}",
-    "Leased properties": "Biens loués",
-    "Letterbox": "Boîte aux lettres",
-    "Letter for requesting the insurance certificate - evidence of insurance": "Demande par courrier d'une attestation d'assurance / preuve d'assurance",
-    "Letter for requesting the payment of the deposit": "Demande par courrier du règlement du dépôt de garantie",
-    "Letter that certifies the payment of deposit by the tenant": "Courrier certifiant le bon règlement du dépôt de garantie par le locataire",
-    "Level": "Etage",
-    "levy": "prélèvement",
-    "Loading": "Chargement",
-    "Location": "Emplacement",
-    "Manage documents": "Gérer les documents",
-    "Manage your real estates": "Gérer vos biens immobiliers",
-    "Month": "Mois",
-    "month": "{{count}} mois",
-    "month_plural": "{{count}} mois",
-    "Monthly CAM amount without VAT": "Charges mensuelles hors taxes",
-    "Monthly rent amount without CAM without VAT": "Loyer mensuel hors charges hors taxes",
-    "no emails defined for tenant": "pas d'email enregistré pour ce locataire",
-    "New": "Nouveau",
-    "New organization": "Nouvelle organisation",
-    "Next": "Suivant",
-    "No": "Non",
-    "No account yet?": "Pas encore de compte ?",
-    "Not paid": "Impayé",
-    "Notice letter for rent payment": "Simple demande de paiement de loyer",
-    "Only the payment of rent period are authorized. Please enter a date between": "Seul le paiement du loyer de {{period}} est autorisé. La date de règlement doit être entre le {{minDate}} et le {{maxDate}}",
-    "Order to pay or vacate": "Demande de règlement de loyer impayé sous peine d'expulsion",
-    "Organization": "Organisation",
-    "Organizations": "Organisations",
-    "Organization name": "Nom de l'organisation",
-    "Ownership equity": "Capital social",
-    "Page not found on server": "Page non trouvée sur le serveur",
-    "Paid": "Payé",
-    "Paid_plural": "Payés",
-    "Paid date": "Payé le {{date}}",
-    "Partially paid": "Paiement partiel",
-    "Password": "Mot de passe",
-    "Payment": "Règlement",
-    "Payments": "Règlements",
-    "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)": "Personnes à contacter pour toutes démarches administratives (demande de règlement des loyers, réclamation de documents...)",
-    "Period": "Période",
-    "Phone number": "Téléphone",
-    "Please enter a valid phone number": "Veuillez entrer un numéro valide",
-    "Please fill missing fields": "Veuillez remplir les champs manquants",
-    "Please set a date after the": "Veuillez indiquer une date après le {{date}}",
-    "Please set a date before the": "Veuillez indiquer une date avant le {{date}}",
-    "Previous": "Précédent",
-    "Print": "Imprimer",
-    "Print and fill this form before giving the keys to the tenant": "Imprimer et remplir ce formulaire avant de donner les clés au locataire",
-    "Problem during data decoding [JSON]": "Problème lors du décodage des données [JSON]",
-    "Properties": "Biens",
-    "Property": "Bien",
-    "Property_plural": "Biens",
-    "Property to rent": "Bien à louer",
-    "Real estate management": "Gestion de vos biens immobiliers",
-    "Reason of additional cost": "Raison du coût supplémentaire",
-    "Reason for reduction": "Raison de la réduction",
-    "Recovery of unpaid rents": "Demande de recouvrement des loyers par un huissier",
-    "Reference": "Référence",
-    "Remove": "Supprimer",
-    "Rent": "Loyer",
-    "Rent_plural": "Loyers",
-    "Rental invoice": "Quittance",
-    "Rental invoices": "Quittances",
-    "Rents": "Loyers",
-    "Rent information": "Détail du loyer",
-    "Rent notice": "Avis d'échéance",
-    "Rent notice reminder": "Rappel d'avis d'échéance",
-    "Rent to pay": "Loyer à régler",
-    "Rent with CAM fees": "Loyer C.C.",
-    "Rent with CAM fees:": "Loyer C.C. : {{amount}}",
-    "Rent with CAM fees tenanted by": "Loyer C.C. : {{amount}} occupé par {{name}}",
-    "Rent without CAM fees": "Loyer H.C.",
-    "Request cancelled on server": "Requête annulée sur le serveur",
-    "Request of certificate of deposit": "Demande de dépôt de garantie",
-    "Request of insurance certificate": "Demande d'attestation d'assurance",
-    "Request rent recovery by a third party (company, justice)": "Demande de commandement de payer par un huissier de justice",
-    "Residence lease contract 9 years": "Contrat de bail residence principale 9 ans",
-    "Room": "Local",
-    "successfuly sent to": "envoi réussi à",
-    "Save": "Enregistrer",
-    "Select properties to rent": "Séléctionner les biens à louer",
-    "Selected tenant": "Locataire sélectionné",
-    "Selected tenant_plural": "Locataires sélectionnés",
-    "Selected rent": "Loyer sélectionné",
-    "Selected rent_plural": "Loyers sélectionnés",
-    "Selected property": "Bien sélectionné",
-    "Selected property_plural": "Biens sélectionnés",
-    "Sent to": "Envoyé à {{email}} le {{datetime}}",
-    "Server access problem. Check your network connection": "Problème d'accès au serveur. Vérifiez votre connexion reseau",
-    "Server is taking too long to reply": "Réponse trop longue du serveur",
-    "Short business lease contract": "Contrat de bail précaire",
-    "Short business lease contract maximum duration 2 years": "Contrat de bail précaire durée maximum 2 ans",
-    "Sign in": "Se connecter",
-    "Sign in now": "Connectez vous maintenant",
-    "Sign out": "Deconnexion",
-    "Site data is reset every 30 minutes": "Les données du site sont réinitialisées toutes les 30 minutes",
-    "Specify the elements for the establishment of invoices and rent notices": "Préciser les éléments pour l'établissement des quittances et des avis d'échéances",
-    "Status": "Statut",
-    "Start date": "Date de début",
-    "Subject to VAT": "Soumis à la T.V.A.",
-    "Register": "S'inscrire",
-    "Tenant": "Locataire",
-    "Tenant_plural": "Locataires",
-    "Tenants": "Locataires",
-    "Tenant information": "Information sur le locataire",
-    "Term of use": "Conditions d'utilisation",
-    "Terminated lease": "Bail resilié",
-    "Terminated lease_plural": "Baux resiliés",
-    "The date is not valid (Sample date:)": "Date non valide (ex : {{date}})",
-    "The end date of contract is not compatible with contract selected": "La date de fin de bail n'est pas compatible avec le type de contrat",
-    "The form is not valid. Please check the field with error": "The Votre formulaire n'est pas valide. Veuillez vérifier le champ en erreur",
-    "The form is not valid. Please check the field with error_plural": "The Votre formulaire n'est pas valide. Veuillez vérifier les champs en erreur",
-    "This user already exists": "Cet utilisateur existe déjà",
-    "This user does not manage any real estate accounts": "Cet utilisateur ne gère aucun bien immobilier",
-    "Timeline": "Échéancier",
-    "Timeline rents": "Échéancier des loyers",
-    "There are no documents attached to the lease contract. Is the insurance certficate is missing?": "Aucun document n'est associé au contrat de bail. L'assurance du bien loué est-elle manquante ?",
-    "transfer": "virement",
-    "Total": "Total",
-    "Type": "Type",
-    "User": "Utilisateur",
-    "VAT": "T.V.A.",
-    "VAT Identification number": "Numéro de TVA Intracommunautaire",
-    "VAT ratio": "Taux T.V.A.",
-    "VAT ratio in %": "Taux T.V.A. en %",
-    "UIN": "SIRET",
-    "Uh-oh!": "houlà !",
-    "Unfriendly reminder for rent payment" : "Rappel inamical pour le paiement du loyer",
-    "Unknown user": "Utilisateur inconnu",
-    "unknown": "inconnu",
-    "Unknown error": "Erreur non répertoriée",
-    "Warning": "Attention",
-    "Where the property is located": "Lieu où se trouve le bien",
-    "with share capital of": "au capital de",
-    "Year": "Année",
-    "Yes": "Oui",
-    "You already have an account?": "Vous avez déjà un compte ?",
-    "Your email address": "Votre E-mail",
-    "Your mailings": "Vos envois",
-    "Your password": "Votre mot de passe",
-    "Your session has expired, Please reconnect": "Votre session a expiré, merci de vous reconnecter",
-    "ZIP code": "Code postal"
+  "__currency_code": "EUR",
+  "__currency_symbol": "€",
+  "[code: ]": "[code : {{code}}]",
+  "#": "n°{{number}}",
+  "'s documents": "Documents de {{name}}",
+  "ATI": "T.T.C.",
+  "A technical issue has occurred (-_-')": "Une erreur technique s'est produite (-_-')",
+  "Account": "Compte",
+  "Accounting": "Comptabilité",
+  "Add": "Ajouter",
+  "Add a contact": "Ajouter un contact",
+  "Add a property": "Ajouter un bien",
+  "Add a payment": "Ajouter un règlement",
+  "Additional cost": "Coût supplémentaire",
+  "Additional cost on current rent": "Coût supplémentaire sur le loyer courant",
+  "Address": "Adresse",
+  "Address where invoices and rent notices will be sent": "Adresse pour l'établissment des quittances et des avis d'échéances",
+  "Administrator": "Administrateur",
+  "All": "Tous",
+  "Amount": "Montant",
+  "Amount of deposit refund": "Montant du remboursement du dépôt de garantie",
+  "Amount without VAT": "Montant H.T.",
+  "Are you sure to remove this tenant?": "Êtes-vous sûr de vouloir supprimer ce locataire ?",
+  "Are you sure to remove this property?": "Êtes-vous sûr de vouloir supprimer ce bien ?",
+  "Are you sure to send invoices by email?": "Êtes-vous sûr de vouloir envoyer les quittances par email ?",
+  "Are you sure to send rent notices by email?": "Êtes-vous sûr de vouloir envoyer les avis d'échéances par email ?",
+  "Area": "Surface",
+  "Available": "Disponible",
+  "Available_plural": "Disponible",
+  "Bad password": "Mot de passe incorrect",
+  "Balance": "Solde ",
+  "Bank name": "Dénomination de la banque",
+  "Bank on which payments shall be made": "Banque sur laquelle les règlements seront effectués",
+  "Banking establishment": "Établissement bancaire",
+  "Billing": "Facturation",
+  "Billing address": "Adresse de facturation",
+  "Building": "Bâtiment",
+  "Business entity": "Forme juridique",
+  "Business lease contract 3,6,9 years": "Contrat de bail commercial 3, 6, 9 ans",
+  "Business lease contract": "Contrat de bail commercial",
+  "Business lease contract terminable after 3,6,9 years": "Contrat de bail commercial resiliable après 3, 6, 9 ans",
+  "By clicking Register, you accept the": "En cliquant sur S’inscrire, vous acceptez les",
+  "CAM Fees": "Charges",
+  "Cancel": "Annuler",
+  "Cancel selection": "Annuler la séléction",
+  "Car park": "Parking",
+  "Caution": "Avertissement",
+  "Certificate of deposit": "Attestation dépôt de garantie",
+  "City": "Ville",
+  "Comments": "Commentaires",
+  "Comments that will not appear on the rent invoice": "Commentaire n'apparaissant pas sur la quittance",
+  "Company": "Société",
+  "Company name": "Dénomination de la société",
+  "Complete the fields to specify the terms of the agreement with the tenant": "Compléter les champs pour préciser les modalités du contrat conclu avec le locataire",
+  "Contact person for all administrative procedures (payment of rents, claims ...)": "Personne à contacter pour toutes démarches administratives (règlements des loyers, réclamations...)",
+  "Customer Identification Number": "Référence client",
+  "cash": "espèce",
+  "cheque": "chèque",
+  "Contact": "Contact",
+  "Contacts": "Contacts",
+  "Contract": "Contrat",
+  "Create your account in few clicks": "Créez votre compte en quelques clics",
+  "Domiciliation contract": "Contrat de domiciliation",
+  "Domiciliation contract (mailbox rental)": "Contrat de domiciliation (location de boîte aux lettres)",
+  "Dashboard": "Tableau de bord",
+  "Date": "Date",
+  "Demonstration": "Démonstration",
+  "Deposit": "Dépôt de garantie",
+  "Deposit amount": "Montant du dépôt de garantie",
+  "Deposit refund": "Remboursement dépôt de garantie",
+  "Describe the property to rent": "Décrire le bien à louer",
+  "Description": "Description",
+  "Discount": "Réduction",
+  "Discount on current rent": "Réduction sur le loyer courant",
+  "Documents": "Documents",
+  "DOS": "RCS",
+  "emails cannot be sent in demo mode": "les emails ne peuvent pas être envoyés en mode démo",
+  "E-mail": "E-mail",
+  "Edit": "Editer",
+  "Effective manager (first and last name)": "Responsable légal (prénom et nom)",
+  "End date": "Date de fin",
+  "ECAMEVAT": "H.C.H.T.",
+  "EVAT": "H.T.",
+  "Entries": "Entrées",
+  "Entry date": "Date d'entrée",
+  "Exits": "Sorties",
+  "Exit date": "Date de sortie",
+  "Excluding Common Area Maintenance fees Excluding VAT": "Hors Charges Hors Taxes",
+  "Excluding VAT": "Hors taxes",
+  "Expiration date": "Date d'expiration",
+  "Fill the empty fields before printing the document": "Remplir les champs vides avant l'impression du document",
+  "First and last name": "Prénom et nom",
+  "First name": "Prénom",
+  "Forgot password?": "Mot de passe oublié ?",
+  "Free subscription": "S'inscrire gratuitement",
+  "Friendly reminder for rent payment": "Rappel amical pour le paiement du loyer",
+  "Get started - it's free.": "Lancez-vous, c’est gratuit.",
+  "has expired": "{{document}} a expiré le {{date}}",
+  "Hello": "Bonjour",
+  "impossible to send document to": "impossible d'envoyer le document à",
+  "IBAN": "IBAN",
+  "ICAMEVAT": "C.C.H.T.",
+  "ICAMIVAT": "C.C.T.T.C",
+  "Including Common Area Maintenance fees Excluding VAT": "Charges Comprises Hors Taxes",
+  "Including Common Area Maintenance fees Including VAT": "Charges Comprises Toutes Taxes Comprises",
+  "Indicate the date, the amount, and the type of payment": "Indiquer la date, le montant, et le mode de règlement",
+  "Indicate the effective date of the lease termination": "Indiquer la date d'effet de fin de bail",
+  "Information about the monthly rent": "Indications sur le loyer mensuel",
+  "Internal note": "Note interne",
+  "Internal server error": "Erreur interne du serveur",
+  "Individual": "Particulier",
+  "Inventory": "Etat des lieux",
+  "Invoice stating the payment of the rent": "Facture attestant le bon règlement du loyer",
+  "Issue": "Anomalie",
+  "It's going OK! No problems (^_^)": "Ca roule! pas de problèmes (^_^)",
+  "Label": "Intitulé",
+  "Landloard": "Propriétaire",
+  "Landloard information": "Information sur le propriétaire",
+  "Last name": "Nom",
+  "Last rent notice reminder": "Dernier rappel d'avis d'échéance",
+  "late month": "{{count}} mois de retard",
+  "late month_plural": "{{count}} mois de retard",
+  "Late rent - First reminder": "Loyer en retard - 1er rappel",
+  "Late rent - Second reminder": "Loyer en retard - 2ème rappel",
+  "Late rent - Last reminder": "Loyer en retard - Dernier rappel",
+  "Lease": "Bail",
+  "Lease_plural": "Baux",
+  "Lease broken": "Rupture de bail",
+  "Lease contract": "Contrat de bail",
+  "Lease in progress": "Bail en cours",
+  "Lease terminated": "Bail résilié",
+  "Lease termination": "Résiliation de bail",
+  "Leased": "Loué",
+  "Leased_plural": "Loués",
+  "Leased by": "Loué par {{name}}",
+  "Leased properties": "Biens loués",
+  "Letterbox": "Boîte aux lettres",
+  "Letter for requesting the insurance certificate - evidence of insurance": "Demande par courrier d'une attestation d'assurance / preuve d'assurance",
+  "Letter for requesting the payment of the deposit": "Demande par courrier du règlement du dépôt de garantie",
+  "Letter that certifies the payment of deposit by the tenant": "Courrier certifiant le bon règlement du dépôt de garantie par le locataire",
+  "Level": "Etage",
+  "levy": "prélèvement",
+  "Loading": "Chargement",
+  "Location": "Emplacement",
+  "Manage documents": "Gérer les documents",
+  "Manage your real estates": "Gérer vos biens immobiliers",
+  "Month": "Mois",
+  "month": "{{count}} mois",
+  "month_plural": "{{count}} mois",
+  "Monthly CAM amount without VAT": "Charges mensuelles hors taxes",
+  "Monthly rent amount without CAM without VAT": "Loyer mensuel hors charges hors taxes",
+  "no emails defined for tenant": "pas d'email enregistré pour ce locataire",
+  "New": "Nouveau",
+  "New organization": "Nouvelle organisation",
+  "Next": "Suivant",
+  "No": "Non",
+  "No account yet?": "Pas encore de compte ?",
+  "Not paid": "Impayé",
+  "Notice letter for rent payment": "Simple demande de paiement de loyer",
+  "Only the payment of rent period are authorized. Please enter a date between": "Seul le paiement du loyer de {{period}} est autorisé. La date de règlement doit être entre le {{minDate}} et le {{maxDate}}",
+  "Order to pay or vacate": "Demande de règlement de loyer impayé sous peine d'expulsion",
+  "Organization": "Organisation",
+  "Organizations": "Organisations",
+  "Organization name": "Nom de l'organisation",
+  "Ownership equity": "Capital social",
+  "Page not found on server": "Page non trouvée sur le serveur",
+  "Paid": "Payé",
+  "Paid_plural": "Payés",
+  "Paid date": "Payé le {{date}}",
+  "Partially paid": "Paiement partiel",
+  "Password": "Mot de passe",
+  "Payment": "Règlement",
+  "Payments": "Règlements",
+  "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)": "Personnes à contacter pour toutes démarches administratives (demande de règlement des loyers, réclamation de documents...)",
+  "Period": "Période",
+  "Phone number": "Téléphone",
+  "Please enter a valid phone number": "Veuillez entrer un numéro valide",
+  "Please fill missing fields": "Veuillez remplir les champs manquants",
+  "Please set a date after the": "Veuillez indiquer une date après le {{date}}",
+  "Please set a date before the": "Veuillez indiquer une date avant le {{date}}",
+  "Previous": "Précédent",
+  "Print": "Imprimer",
+  "Print and fill this form before giving the keys to the tenant": "Imprimer et remplir ce formulaire avant de donner les clés au locataire",
+  "Problem during data decoding [JSON]": "Problème lors du décodage des données [JSON]",
+  "Properties": "Biens",
+  "Property": "Bien",
+  "Property_plural": "Biens",
+  "Property to rent": "Bien à louer",
+  "Real estate management": "Gestion de vos biens immobiliers",
+  "Reason of additional cost": "Raison du coût supplémentaire",
+  "Reason for reduction": "Raison de la réduction",
+  "Recovery of unpaid rents": "Demande de recouvrement des loyers par un huissier",
+  "Reference": "Référence",
+  "Remove": "Supprimer",
+  "Rent": "Loyer",
+  "Rent_plural": "Loyers",
+  "Rental invoice": "Quittance",
+  "Rental invoices": "Quittances",
+  "Rents": "Loyers",
+  "Rent information": "Détail du loyer",
+  "Rent notice": "Avis d'échéance",
+  "Rent notice reminder": "Rappel d'avis d'échéance",
+  "Rent to pay": "Loyer à régler",
+  "Rent with CAM fees": "Loyer C.C.",
+  "Rent with CAM fees:": "Loyer C.C. : {{amount}}",
+  "Rent with CAM fees tenanted by": "Loyer C.C. : {{amount}} occupé par {{name}}",
+  "Rent without CAM fees": "Loyer H.C.",
+  "Request cancelled on server": "Requête annulée sur le serveur",
+  "Request of certificate of deposit": "Demande de dépôt de garantie",
+  "Request of insurance certificate": "Demande d'attestation d'assurance",
+  "Request rent recovery by a third party (company, justice)": "Demande de commandement de payer par un huissier de justice",
+  "Residence lease contract 9 years": "Contrat de bail residence principale 9 ans",
+  "Room": "Local",
+  "successfuly sent to": "envoi réussi à",
+  "Save": "Enregistrer",
+  "Select properties to rent": "Séléctionner les biens à louer",
+  "Selected tenant": "Locataire sélectionné",
+  "Selected tenant_plural": "Locataires sélectionnés",
+  "Selected rent": "Loyer sélectionné",
+  "Selected rent_plural": "Loyers sélectionnés",
+  "Selected property": "Bien sélectionné",
+  "Selected property_plural": "Biens sélectionnés",
+  "Sent to": "Envoyé à {{email}} le {{datetime}}",
+  "Server access problem. Check your network connection": "Problème d'accès au serveur. Vérifiez votre connexion reseau",
+  "Server is taking too long to reply": "Réponse trop longue du serveur",
+  "Short business lease contract": "Contrat de bail précaire",
+  "Short business lease contract maximum duration 2 years": "Contrat de bail précaire durée maximum 2 ans",
+  "Sign in": "Se connecter",
+  "Sign in now": "Connectez vous maintenant",
+  "Sign out": "Deconnexion",
+  "Site data is reset every 30 minutes": "Les données du site sont réinitialisées toutes les 30 minutes",
+  "Specify the elements for the establishment of invoices and rent notices": "Préciser les éléments pour l'établissement des quittances et des avis d'échéances",
+  "Status": "Statut",
+  "Start date": "Date de début",
+  "Subject to VAT": "Soumis à la T.V.A.",
+  "Register": "S'inscrire",
+  "Tenant": "Locataire",
+  "Tenant_plural": "Locataires",
+  "Tenants": "Locataires",
+  "Tenant information": "Information sur le locataire",
+  "Term of use": "Conditions d'utilisation",
+  "Terminated lease": "Bail resilié",
+  "Terminated lease_plural": "Baux resiliés",
+  "The date is not valid (Sample date:)": "Date non valide (ex : {{date}})",
+  "The end date of contract is not compatible with contract selected": "La date de fin de bail n'est pas compatible avec le type de contrat",
+  "The form is not valid. Please check the field with error": "The Votre formulaire n'est pas valide. Veuillez vérifier le champ en erreur",
+  "The form is not valid. Please check the field with error_plural": "The Votre formulaire n'est pas valide. Veuillez vérifier les champs en erreur",
+  "This user already exists": "Cet utilisateur existe déjà",
+  "This user does not manage any real estate accounts": "Cet utilisateur ne gère aucun bien immobilier",
+  "Timeline": "Échéancier",
+  "Timeline rents": "Échéancier des loyers",
+  "There are no documents attached to the lease contract. Is the insurance certficate is missing?": "Aucun document n'est associé au contrat de bail. L'assurance du bien loué est-elle manquante ?",
+  "transfer": "virement",
+  "Total": "Total",
+  "Type": "Type",
+  "User": "Utilisateur",
+  "VAT": "T.V.A.",
+  "VAT Identification number": "Numéro de TVA Intracommunautaire",
+  "VAT ratio": "Taux T.V.A.",
+  "VAT ratio in %": "Taux T.V.A. en %",
+  "UIN": "SIRET",
+  "Uh-oh!": "houlà !",
+  "Unfriendly reminder for rent payment": "Rappel inamical pour le paiement du loyer",
+  "Unknown user": "Utilisateur inconnu",
+  "unknown": "inconnu",
+  "Unknown error": "Erreur non répertoriée",
+  "Warning": "Attention",
+  "Where the property is located": "Lieu où se trouve le bien",
+  "with share capital of": "au capital de",
+  "Year": "Année",
+  "Yes": "Oui",
+  "You already have an account?": "Vous avez déjà un compte ?",
+  "Your email address": "Votre E-mail",
+  "Your mailings": "Vos envois",
+  "Your password": "Votre mot de passe",
+  "Your session has expired, Please reconnect": "Votre session a expiré, merci de vous reconnecter",
+  "ZIP code": "Code postal"
 }
diff --git a/frontend/locales/pt.json b/frontend/locales/pt.json
index 7166495..81a04da 100644
--- a/frontend/locales/pt.json
+++ b/frontend/locales/pt.json
@@ -1,272 +1,272 @@
 {
-    "__currency_code": "BRL",
-    "__currency_symbol": "R$",
-    "[code: ]": "[code: {{code}}]",
-    "#": "#{{number}}",
-    "'s documents": "Documentos do {{name}}",
-    "ATI": "ATI",
-    "A technical issue has occurred (-_-')": "Ocorreu um erro técnico (-_-')",
-    "Account": "Conta",
-    "Accounting": "Fazendo conta",
-    "Add": "Adicionar",
-    "Add a contact": "Adicionar contato",
-    "Add a property": "Adicionar propriedade",
-    "Add a payment": "Adicionar pagamento",
-    "Address": "Endereço",
-    "Address where invoices and rent notices will be sent": "Endereço onde notificações de fatura e notificações de aluguel serão enviados",
-    "Administrator": "Administrador",
-    "All": "Todos",
-    "Amount": "Quantia",
-    "Amount of deposit refund": "Quantia de rembolso de depósito",
-    "Amount without VAT": "Quantia sem imposto",
-    "Are you sure to remove this tenant?": "Tem certeza que quer remover este inquilino?",
-    "Are you sure to remove this property?": "Tem certeza que quer remover esta propriedade?",
-    "Are you sure to send invoices by email?": "Tem certeza que quer enviar faturas por email?",
-    "Are you sure to send rent notices by email?": "Tem certeza que quer notificações de aluguel por email?",
-    "Area": "Área",
-    "Available": "Disponível",
-    "Available_plural": "Disponíveis",
-    "Bad password": "Senha ruim",
-    "Balance": "Balanço",
-    "Bank name": "Nome do Banco",
-    "Bank on which payments shall be made": "Banco nos quais os pagamentos serão efetuados",
-    "Banking establishment": "Instituto Bancário",
-    "Billing": "Faturamento",
-    "Billing address": "Endereço de faturamento",
-    "Building": "Construção",
-    "Business entity": "Entidade de negócios",
-    "Business lease contract 3,6,9 years": "Contrato de aluguel 3,6,9 anos",
-    "Business lease contract": "Contrato de aluguel",
-    "Business lease contract terminable after 3,6,9 years": "Encerramento do contrato de aluguel depois de 3,6,9 anos",
-    "By clicking Register, you accept the": "Clicando em registrar, você aceita o",
-    "CAM Fees": "Taxas CAM",
-    "Cancel": "Cancelar",
-    "Cancel selection": "Cancelar seleção",
-    "Car park": "Estacionamento",
-    "Caution": "Cuidado",
-    "Certificate of deposit": "Certificado de depósito",
-    "City": "Cidade",
-    "Comments": "Comentários",
-    "Comments that will not appear on the rent invoice": "Comentários que aparecerão nas faturas de aluguel",
-    "Company": "Compania",
-    "Company name": "Nome da companhia",
-    "Complete the fields to specify the terms of the agreement with the tenant": "Complete estes campos para especificar os termos do contrato com o inquilino",
-    "Contact person for all administrative procedures (payment of rents, claims ...)": "Pessoa de contato para todos os procedimentos administrativos (pagamento de alugueis, reclamações",
-    "Customer Identification Number": "Número de identificação do cliente",
-    "cash": "dinheiro",
-    "cheque": "cheque",
-    "Contact": "Contato",
-    "Contacts": "Contatos",
-    "Contract": "Contrato",
-    "Create your account in few clicks": "Crie sua conta com poucos cliques",
-    "Domiciliation contract": "Contrato de domiliciaçao",
-    "Domiciliation contract (mailbox rental)": "Contrato de domiciliação (aluguel de caixa postal)",
-    "Dashboard": "Painel",
-    "Date": "Data",
-    "Demonstration": "Demonstração",
-    "Deposit": "Depósito",
-    "Deposit amount": "Quantia de depósito",
-    "Deposit refund": "Quantia de reembolso",
-    "Describe the property to rent": "Descreve a propriedade à alugar",
-    "Description": "Descrição",
-    "Discount": "Desconto",
-    "Discount on current rent": "Desconto no aluguel atual",
-    "Documents": "Documentos",
-    "emails cannot be sent in demo mode": "não se pode enviar emails no modo de demonstração",
-    "E-mail": "E-mail",
-    "Edit": "Editar",
-    "Effective manager (first and last name)": "Gerente efetivo (primeiro e último nome)",
-    "End date": "Data do fim",
-    "ECAMEVAT": "ECAMEVAT",
-    "Entries": "Entradas",
-    "Entry date": "Dia de entrada",
-    "Exit date": "Dia de saída",
-    "Exits": "Saídas",
-    "Expiration date": "Data de expiração",
-    "Excluding Common Area Maintenance fees Excluding VAT": "Excluindo as taxas de manutenção da área comum excluindo IVA",
-    "EVAT": "EVAT",
-    "Excluding VAT": "Excluindo IVA",
-    "Fill the empty fields before printing the document": "Complete os campos em branco antes de imprimir o documento",
-    "First and last name": "Primeiro e último nome",
-    "First name": "Primeiro nome",
-    "Forgot password?": "Esqueceu a senha?",
-    "Free subscription": "Inscrição gratuita",
-    "Friendly reminder for rent payment" : "Lembrete amigável de pagamento de alugel",
-    "Get started - it's free.": "Comece - é de graça.",
-    "has expired": "{{document}} expirou em {{date}}",
-    "Hello": "Olá",
-    "impossible to send document to": "impossível enviar documento para",
-    "IBAN": "IBAN",
-    "ICAMEVAT": "ICAMEVAT",
-    "ICAMIVAT": "ICAMIVAT",
-    "Including Common Area Maintenance fees Excluding VAT": "Incluindo as taxas de manutenção da área comum excluindo IVA",
-    "Including Common Area Maintenance fees Including VAT": "Incluindo as taxas de manutenção da área comum incluindo IVA",
-    "Indicate the date, the amount, and the type of payment": "Indica a data, a quantia, e tipo do pagamento",
-    "Indicate the effective date of the lease termination": "Indica o dia efetivo do término da locação",
-    "Information about the monthly rent": "Informação sobre o aluguel mensal",
-    "Internal note": "Nota interna",
-    "Internal server error": "Erro no servidor interno",
-    "Individual": "Indivíduo",
-    "Inventory": "Inventório",
-    "Invoice stating the payment of the rent": "Fatura declarando o pagamento do aluguel",
-    "Issue": "Problema",
-    "It's going OK! No problems (^_^)": "Está indo OK! Sem problemas (^_^)",
-    "Label": "Etiqueta",
-    "Landloard": "Poderoso Chefão",
-    "Landloard information": "Informações do poderoso chefão",
-    "Last name": "Último nome",
-    "late month": "{{count}} mês atrasado",
-    "late month_plural": "{{count}} meses atrasado",
-    "Late rent - First reminder": "Aluguel atrasado - Primeiro lembrete",
-    "Late rent - Second reminder": "Aluguel atrasado - Segundo lembrete",
-    "Late rent - Last reminder": "Aluguel atrasado - Último lembrete",
-    "Lease": "Aluguel",
-    "Lease_plural": "Aluguéis",
-    "Lease broken": "Aluguel quebrado",
-    "Lease contract": "Contrato de aluguel",
-    "Lease in progress": "Aluguel em andamento",
-    "Lease terminated": "Aluguel terminado",
-    "Lease termination": "Recisão de aluguel",
-    "Leased": "Alugado",
-    "Leased_plural": "Alugados",
-    "Leased by": "Alugado por {{name}}",
-    "Leased properties": "Propriedades alugadas",
-    "Letterbox": "Correio",
-    "Letter for requesting the insurance certificate - evidence of insurance": "Carta para requisitar certificado de seguro - evidência de seguro",
-    "Letter for requesting the payment of the deposit": "Carta para requisitar pagamento de depósito",
-    "Letter that certifies the payment of deposit by the tenant": "Carta que certifica pagamento de depósito pelo inquilino",
-    "Level": "Andar",
-    "levy": "taxa",
-    "Loading": "Carregando",
-    "Location": "Localização",
-    "Manage documents": "Gerenciar documentos",
-    "Manage your real estates": "Gerenciar bens imóveis",
-    "Month": "Mês",
-    "month": "{{count}} mês",
-    "month_plural": "{{count}} meses",
-    "Monthly CAM amount without VAT": "Quantidade mensal de CAM sem IVA",
-    "Monthly rent amount without CAM without VAT": "Valor do aluguel mensal sem CAM sem IVA",
-    "no emails defined for tenant": "nenhum email pro inquilino",
-    "New": "Novo",
-    "New organization": "Nova organização",
-    "Next": "Próximo",
-    "No": "Não",
-    "No account yet?": "Sem conta ainda?",
-    "Not paid": "Não pago",
-    "Notice letter for rent payment": "Carta de aviso para pagamento de aluguel",
-    "Only the payment of rent period are authorized. Please enter a date between": "Somente o pagamento de {{period}} foi autorizado. A data de pagamento deve ser entre {{minDate}} e {{maxDate}}",
-    "Order to pay or vacate":  "Ordem de pagar ou desocupar",
-    "Organization": "Organização",
-    "Organizations": "Organizações",
-    "Organization name": "Nome da organização",
-    "Ownership equity": "Patrimônio líquido",
-    "Page not found on server": "Página não encontrada no servidor",
-    "Paid": "Pago",
-    "Paid_plural": "Pagos",
-    "Paid date": "Data do pagamento: {{date}}",
-    "Partially paid": "Parcialmente pago",
-    "Password": "Senha",
-    "Payment": "Pagamento",
-    "Payments": "Pagamentos",
-    "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)": "Pessoa de contato para todos os procedimentos administrativos (pedido de pagamento de aluguéis, falta de documentos necessários",
-    "Period": "Período",
-    "Phone number": "Número de telefone",
-    "Please enter a valid phone number": "Por favor digite um número de telefone válido",
-    "Please fill missing fields": "Preencha os campos faltantes",
-    "Please set a date after the": "Por favor defina a data após {{date}}",
-    "Please set a date before the": "Por favor defina a data antes de {{date}}",
-    "Previous": "Anterior",
-    "Print": "Imprimir",
-    "Print and fill this form before giving the keys to the tenant": "Imprima e preencha este formulário antes de dar as chaves ao locatário",
-    "Problem during data decoding [JSON]": "Problema na decodificação dos dados [JSON]",
-    "Properties": "Propriedades",
-    "Property": "Propriedade",
-    "Property_plural": "Propriedades",
-    "Property to rent": "Propriedade para aluguel",
-    "Real estate management": "Gerenciamento de bens imóveis",
-    "Reason for reduction": "Razão para desconto",
-    "Recovery of unpaid rents": "Reparação de aluguéis não pagos",
-    "Reference": "Referência",
-    "Remove": "Remover",
-    "Rent": "Aluguel",
-    "Rent_plural": "Aluguéis",
-    "Rental invoice": "Fatura de aluguel",
-    "Rental invoices": "Faturas de aluguéis",
-    "Rents": "Aluguéis",
-    "Rent information": "Informações de alugel",
-    "Rent notice": "Aviso de aluguel",
-    "Rent notice reminder": "Lembrete de aviso de aluguel",
-    "Rent to pay": "Aluguel a pagar",
-    "Rent with CAM fees": "Rent with CAM fees",
-    "Rent with CAM fees:": "Rent with CAM fees: {{amount}}",
-    "Rent with CAM fees tenanted by": "Rent with CAM fees: {{amount}} tenanted by {{name}}",
-    "Rent without CAM fees": "Rent without CAM fees",
-    "Request cancelled on server": "Requisiçao cancelada no servidor",
-    "Request of certificate of deposit": "Requisição de certificado de depósito",
-    "Request of insurance certificate": "Requisição de certificado de seguro",
-    "Request rent recovery by a third party (company, justice)": "Requisição de recuperação de aluguel por terceiro (compania, justiça)",
-    "Residence lease contract 9 years": "Contrato de aluguel residencial 9 anos",
-    "Room": "Sala",
-    "successfuly sent to": "enviado com sucesso para",
-    "Save": "Salvar",
-    "Select properties to rent": "Selecione propriedade para alugar",
-    "Selected tenant": "Inquilino selecionado",
-    "Selected tenant_plural": "Inquilinos selecionados",
-    "Selected rent": "Aluguel selecionado",
-    "Selected rent_plural": "Aluguéis selecionados",
-    "Selected property": "Propriedade selecionada",
-    "Selected property_plural": "Propriedades selecionadas",
-    "Server access problem. Check your network connection": "Problema de acesso ao servidor. Cheque sua conexão à rede",
-    "Server is taking too long to reply": "Servidor demorando muito a responder",
-    "Short business lease contract": "Contrato de aluguel para pequenos negócios",
-    "Short business lease contract maximum duration 2 years": "Contrato de aluguel para pequenos negócios, duração máxima 2 anos",
-    "Sign in": "Entrar",
-    "Sign in now": "Entrar agora",
-    "Sign out": "Sair",
-    "Site data is reset every 30 minutes": "Dados do site são resetados a cada 30 minutos",
-    "Specify the elements for the establishment of invoices and rent notices": "Especifique os elementos para o estabelecimento de faturas e avisos de aluguel",
-    "Status": "Status",
-    "Start date": "Dia de início",
-    "Subject to VAT": "Subject to VAT",
-    "Register": "Registrar",
-    "Tenant": "Inquilino",
-    "Tenant_plural": "Inquilinos",
-    "Tenants": "Inquilinos",
-    "Tenant information": "Informação de inquilino",
-    "Term of use": "Termos de uso",
-    "Terminated lease": "Aluguel terminado",
-    "Terminated lease_plural": "Aluguéis terminados",
-    "The date is not valid (Sample date:)": "A data não é válida (exemplo: {{date}})",
-    "The end date of contract is not compatible with contract selected": "A data do fim do contrato não é compatível com o contrato selecionado",
-    "The form is not valid. Please check the field with error": "O formulário não é válido, cheque o campo com erro",
-    "The form is not valid. Please check the field with error_plural": "O formulário não é válido, cheque os campos com erros",
-    "This user already exists": "O usuário já existe",
-    "This user does not manage any real estate accounts": "Este usuário não gerencia nenhuma conta de bens imóveis",
-    "Timeline": "Linha do tempo",
-    "Timeline rents": "Linha do tempo de aluguéis",
-    "There are no documents attached to the lease contract. Is the insurance certficate is missing?": "Não há documentos anexados a este contrato de aluguel. Está faltando o certificado de seguro?",
-    "transfer": "transferência",
-    "Total": "Total",
-    "Type": "Tipo",
-    "User": "Usuário",
-    "VAT": "VAT",
-    "VAT Identification number": "Número de identificação IVA",
-    "VAT ratio": "Relação IVA",
-    "VAT ratio in %": "Relação IVA em %",
-    "UIN": "UIN",
-    "Uh-oh!": "Uh-oh!",
-    "Unfriendly reminder for rent payment" : "Lembrete não amigável para pagamento de aluguel",
-    "Unknown user": "Usuário desconhecido",
-    "unknown": "desconhecido",
-    "Unknown error": "Erro desconhecido",
-    "Warning": "Aviso",
-    "Where the property is located": "Aonde é a propriedade",
-    "Year": "Ano",
-    "Yes": "Sim",
-    "You already have an account?": "Já tem uma conta?",
-    "Your email address": "Seu email",
-    "Your mailings": "Suas correspondências",
-    "Your password": "Sua senha",
-    "Your session has expired, Please reconnect": "Sua sessão expirou. Por favor reconecte",
-    "ZIP code": "Código postal"
+  "__currency_code": "BRL",
+  "__currency_symbol": "R$",
+  "[code: ]": "[code: {{code}}]",
+  "#": "#{{number}}",
+  "'s documents": "Documentos do {{name}}",
+  "ATI": "ATI",
+  "A technical issue has occurred (-_-')": "Ocorreu um erro técnico (-_-')",
+  "Account": "Conta",
+  "Accounting": "Fazendo conta",
+  "Add": "Adicionar",
+  "Add a contact": "Adicionar contato",
+  "Add a property": "Adicionar propriedade",
+  "Add a payment": "Adicionar pagamento",
+  "Address": "Endereço",
+  "Address where invoices and rent notices will be sent": "Endereço onde notificações de fatura e notificações de aluguel serão enviados",
+  "Administrator": "Administrador",
+  "All": "Todos",
+  "Amount": "Quantia",
+  "Amount of deposit refund": "Quantia de rembolso de depósito",
+  "Amount without VAT": "Quantia sem imposto",
+  "Are you sure to remove this tenant?": "Tem certeza que quer remover este inquilino?",
+  "Are you sure to remove this property?": "Tem certeza que quer remover esta propriedade?",
+  "Are you sure to send invoices by email?": "Tem certeza que quer enviar faturas por email?",
+  "Are you sure to send rent notices by email?": "Tem certeza que quer notificações de aluguel por email?",
+  "Area": "Área",
+  "Available": "Disponível",
+  "Available_plural": "Disponíveis",
+  "Bad password": "Senha ruim",
+  "Balance": "Balanço",
+  "Bank name": "Nome do Banco",
+  "Bank on which payments shall be made": "Banco nos quais os pagamentos serão efetuados",
+  "Banking establishment": "Instituto Bancário",
+  "Billing": "Faturamento",
+  "Billing address": "Endereço de faturamento",
+  "Building": "Construção",
+  "Business entity": "Entidade de negócios",
+  "Business lease contract 3,6,9 years": "Contrato de aluguel 3,6,9 anos",
+  "Business lease contract": "Contrato de aluguel",
+  "Business lease contract terminable after 3,6,9 years": "Encerramento do contrato de aluguel depois de 3,6,9 anos",
+  "By clicking Register, you accept the": "Clicando em registrar, você aceita o",
+  "CAM Fees": "Taxas CAM",
+  "Cancel": "Cancelar",
+  "Cancel selection": "Cancelar seleção",
+  "Car park": "Estacionamento",
+  "Caution": "Cuidado",
+  "Certificate of deposit": "Certificado de depósito",
+  "City": "Cidade",
+  "Comments": "Comentários",
+  "Comments that will not appear on the rent invoice": "Comentários que aparecerão nas faturas de aluguel",
+  "Company": "Compania",
+  "Company name": "Nome da companhia",
+  "Complete the fields to specify the terms of the agreement with the tenant": "Complete estes campos para especificar os termos do contrato com o inquilino",
+  "Contact person for all administrative procedures (payment of rents, claims ...)": "Pessoa de contato para todos os procedimentos administrativos (pagamento de alugueis, reclamações",
+  "Customer Identification Number": "Número de identificação do cliente",
+  "cash": "dinheiro",
+  "cheque": "cheque",
+  "Contact": "Contato",
+  "Contacts": "Contatos",
+  "Contract": "Contrato",
+  "Create your account in few clicks": "Crie sua conta com poucos cliques",
+  "Domiciliation contract": "Contrato de domiliciaçao",
+  "Domiciliation contract (mailbox rental)": "Contrato de domiciliação (aluguel de caixa postal)",
+  "Dashboard": "Painel",
+  "Date": "Data",
+  "Demonstration": "Demonstração",
+  "Deposit": "Depósito",
+  "Deposit amount": "Quantia de depósito",
+  "Deposit refund": "Quantia de reembolso",
+  "Describe the property to rent": "Descreve a propriedade à alugar",
+  "Description": "Descrição",
+  "Discount": "Desconto",
+  "Discount on current rent": "Desconto no aluguel atual",
+  "Documents": "Documentos",
+  "emails cannot be sent in demo mode": "não se pode enviar emails no modo de demonstração",
+  "E-mail": "E-mail",
+  "Edit": "Editar",
+  "Effective manager (first and last name)": "Gerente efetivo (primeiro e último nome)",
+  "End date": "Data do fim",
+  "ECAMEVAT": "ECAMEVAT",
+  "Entries": "Entradas",
+  "Entry date": "Dia de entrada",
+  "Exit date": "Dia de saída",
+  "Exits": "Saídas",
+  "Expiration date": "Data de expiração",
+  "Excluding Common Area Maintenance fees Excluding VAT": "Excluindo as taxas de manutenção da área comum excluindo IVA",
+  "EVAT": "EVAT",
+  "Excluding VAT": "Excluindo IVA",
+  "Fill the empty fields before printing the document": "Complete os campos em branco antes de imprimir o documento",
+  "First and last name": "Primeiro e último nome",
+  "First name": "Primeiro nome",
+  "Forgot password?": "Esqueceu a senha?",
+  "Free subscription": "Inscrição gratuita",
+  "Friendly reminder for rent payment": "Lembrete amigável de pagamento de alugel",
+  "Get started - it's free.": "Comece - é de graça.",
+  "has expired": "{{document}} expirou em {{date}}",
+  "Hello": "Olá",
+  "impossible to send document to": "impossível enviar documento para",
+  "IBAN": "IBAN",
+  "ICAMEVAT": "ICAMEVAT",
+  "ICAMIVAT": "ICAMIVAT",
+  "Including Common Area Maintenance fees Excluding VAT": "Incluindo as taxas de manutenção da área comum excluindo IVA",
+  "Including Common Area Maintenance fees Including VAT": "Incluindo as taxas de manutenção da área comum incluindo IVA",
+  "Indicate the date, the amount, and the type of payment": "Indica a data, a quantia, e tipo do pagamento",
+  "Indicate the effective date of the lease termination": "Indica o dia efetivo do término da locação",
+  "Information about the monthly rent": "Informação sobre o aluguel mensal",
+  "Internal note": "Nota interna",
+  "Internal server error": "Erro no servidor interno",
+  "Individual": "Indivíduo",
+  "Inventory": "Inventório",
+  "Invoice stating the payment of the rent": "Fatura declarando o pagamento do aluguel",
+  "Issue": "Problema",
+  "It's going OK! No problems (^_^)": "Está indo OK! Sem problemas (^_^)",
+  "Label": "Etiqueta",
+  "Landloard": "Poderoso Chefão",
+  "Landloard information": "Informações do poderoso chefão",
+  "Last name": "Último nome",
+  "late month": "{{count}} mês atrasado",
+  "late month_plural": "{{count}} meses atrasado",
+  "Late rent - First reminder": "Aluguel atrasado - Primeiro lembrete",
+  "Late rent - Second reminder": "Aluguel atrasado - Segundo lembrete",
+  "Late rent - Last reminder": "Aluguel atrasado - Último lembrete",
+  "Lease": "Aluguel",
+  "Lease_plural": "Aluguéis",
+  "Lease broken": "Aluguel quebrado",
+  "Lease contract": "Contrato de aluguel",
+  "Lease in progress": "Aluguel em andamento",
+  "Lease terminated": "Aluguel terminado",
+  "Lease termination": "Recisão de aluguel",
+  "Leased": "Alugado",
+  "Leased_plural": "Alugados",
+  "Leased by": "Alugado por {{name}}",
+  "Leased properties": "Propriedades alugadas",
+  "Letterbox": "Correio",
+  "Letter for requesting the insurance certificate - evidence of insurance": "Carta para requisitar certificado de seguro - evidência de seguro",
+  "Letter for requesting the payment of the deposit": "Carta para requisitar pagamento de depósito",
+  "Letter that certifies the payment of deposit by the tenant": "Carta que certifica pagamento de depósito pelo inquilino",
+  "Level": "Andar",
+  "levy": "taxa",
+  "Loading": "Carregando",
+  "Location": "Localização",
+  "Manage documents": "Gerenciar documentos",
+  "Manage your real estates": "Gerenciar bens imóveis",
+  "Month": "Mês",
+  "month": "{{count}} mês",
+  "month_plural": "{{count}} meses",
+  "Monthly CAM amount without VAT": "Quantidade mensal de CAM sem IVA",
+  "Monthly rent amount without CAM without VAT": "Valor do aluguel mensal sem CAM sem IVA",
+  "no emails defined for tenant": "nenhum email pro inquilino",
+  "New": "Novo",
+  "New organization": "Nova organização",
+  "Next": "Próximo",
+  "No": "Não",
+  "No account yet?": "Sem conta ainda?",
+  "Not paid": "Não pago",
+  "Notice letter for rent payment": "Carta de aviso para pagamento de aluguel",
+  "Only the payment of rent period are authorized. Please enter a date between": "Somente o pagamento de {{period}} foi autorizado. A data de pagamento deve ser entre {{minDate}} e {{maxDate}}",
+  "Order to pay or vacate": "Ordem de pagar ou desocupar",
+  "Organization": "Organização",
+  "Organizations": "Organizações",
+  "Organization name": "Nome da organização",
+  "Ownership equity": "Patrimônio líquido",
+  "Page not found on server": "Página não encontrada no servidor",
+  "Paid": "Pago",
+  "Paid_plural": "Pagos",
+  "Paid date": "Data do pagamento: {{date}}",
+  "Partially paid": "Parcialmente pago",
+  "Password": "Senha",
+  "Payment": "Pagamento",
+  "Payments": "Pagamentos",
+  "People to contact for all administrative procedures (request of unpaid rents, missing mandatory documents...)": "Pessoa de contato para todos os procedimentos administrativos (pedido de pagamento de aluguéis, falta de documentos necessários",
+  "Period": "Período",
+  "Phone number": "Número de telefone",
+  "Please enter a valid phone number": "Por favor digite um número de telefone válido",
+  "Please fill missing fields": "Preencha os campos faltantes",
+  "Please set a date after the": "Por favor defina a data após {{date}}",
+  "Please set a date before the": "Por favor defina a data antes de {{date}}",
+  "Previous": "Anterior",
+  "Print": "Imprimir",
+  "Print and fill this form before giving the keys to the tenant": "Imprima e preencha este formulário antes de dar as chaves ao locatário",
+  "Problem during data decoding [JSON]": "Problema na decodificação dos dados [JSON]",
+  "Properties": "Propriedades",
+  "Property": "Propriedade",
+  "Property_plural": "Propriedades",
+  "Property to rent": "Propriedade para aluguel",
+  "Real estate management": "Gerenciamento de bens imóveis",
+  "Reason for reduction": "Razão para desconto",
+  "Recovery of unpaid rents": "Reparação de aluguéis não pagos",
+  "Reference": "Referência",
+  "Remove": "Remover",
+  "Rent": "Aluguel",
+  "Rent_plural": "Aluguéis",
+  "Rental invoice": "Fatura de aluguel",
+  "Rental invoices": "Faturas de aluguéis",
+  "Rents": "Aluguéis",
+  "Rent information": "Informações de alugel",
+  "Rent notice": "Aviso de aluguel",
+  "Rent notice reminder": "Lembrete de aviso de aluguel",
+  "Rent to pay": "Aluguel a pagar",
+  "Rent with CAM fees": "Rent with CAM fees",
+  "Rent with CAM fees:": "Rent with CAM fees: {{amount}}",
+  "Rent with CAM fees tenanted by": "Rent with CAM fees: {{amount}} tenanted by {{name}}",
+  "Rent without CAM fees": "Rent without CAM fees",
+  "Request cancelled on server": "Requisiçao cancelada no servidor",
+  "Request of certificate of deposit": "Requisição de certificado de depósito",
+  "Request of insurance certificate": "Requisição de certificado de seguro",
+  "Request rent recovery by a third party (company, justice)": "Requisição de recuperação de aluguel por terceiro (compania, justiça)",
+  "Residence lease contract 9 years": "Contrato de aluguel residencial 9 anos",
+  "Room": "Sala",
+  "successfuly sent to": "enviado com sucesso para",
+  "Save": "Salvar",
+  "Select properties to rent": "Selecione propriedade para alugar",
+  "Selected tenant": "Inquilino selecionado",
+  "Selected tenant_plural": "Inquilinos selecionados",
+  "Selected rent": "Aluguel selecionado",
+  "Selected rent_plural": "Aluguéis selecionados",
+  "Selected property": "Propriedade selecionada",
+  "Selected property_plural": "Propriedades selecionadas",
+  "Server access problem. Check your network connection": "Problema de acesso ao servidor. Cheque sua conexão à rede",
+  "Server is taking too long to reply": "Servidor demorando muito a responder",
+  "Short business lease contract": "Contrato de aluguel para pequenos negócios",
+  "Short business lease contract maximum duration 2 years": "Contrato de aluguel para pequenos negócios, duração máxima 2 anos",
+  "Sign in": "Entrar",
+  "Sign in now": "Entrar agora",
+  "Sign out": "Sair",
+  "Site data is reset every 30 minutes": "Dados do site são resetados a cada 30 minutos",
+  "Specify the elements for the establishment of invoices and rent notices": "Especifique os elementos para o estabelecimento de faturas e avisos de aluguel",
+  "Status": "Status",
+  "Start date": "Dia de início",
+  "Subject to VAT": "Subject to VAT",
+  "Register": "Registrar",
+  "Tenant": "Inquilino",
+  "Tenant_plural": "Inquilinos",
+  "Tenants": "Inquilinos",
+  "Tenant information": "Informação de inquilino",
+  "Term of use": "Termos de uso",
+  "Terminated lease": "Aluguel terminado",
+  "Terminated lease_plural": "Aluguéis terminados",
+  "The date is not valid (Sample date:)": "A data não é válida (exemplo: {{date}})",
+  "The end date of contract is not compatible with contract selected": "A data do fim do contrato não é compatível com o contrato selecionado",
+  "The form is not valid. Please check the field with error": "O formulário não é válido, cheque o campo com erro",
+  "The form is not valid. Please check the field with error_plural": "O formulário não é válido, cheque os campos com erros",
+  "This user already exists": "O usuário já existe",
+  "This user does not manage any real estate accounts": "Este usuário não gerencia nenhuma conta de bens imóveis",
+  "Timeline": "Linha do tempo",
+  "Timeline rents": "Linha do tempo de aluguéis",
+  "There are no documents attached to the lease contract. Is the insurance certficate is missing?": "Não há documentos anexados a este contrato de aluguel. Está faltando o certificado de seguro?",
+  "transfer": "transferência",
+  "Total": "Total",
+  "Type": "Tipo",
+  "User": "Usuário",
+  "VAT": "VAT",
+  "VAT Identification number": "Número de identificação IVA",
+  "VAT ratio": "Relação IVA",
+  "VAT ratio in %": "Relação IVA em %",
+  "UIN": "UIN",
+  "Uh-oh!": "Uh-oh!",
+  "Unfriendly reminder for rent payment": "Lembrete não amigável para pagamento de aluguel",
+  "Unknown user": "Usuário desconhecido",
+  "unknown": "desconhecido",
+  "Unknown error": "Erro desconhecido",
+  "Warning": "Aviso",
+  "Where the property is located": "Aonde é a propriedade",
+  "Year": "Ano",
+  "Yes": "Sim",
+  "You already have an account?": "Já tem uma conta?",
+  "Your email address": "Seu email",
+  "Your mailings": "Suas correspondências",
+  "Your password": "Sua senha",
+  "Your session has expired, Please reconnect": "Sua sessão expirou. Por favor reconecte",
+  "ZIP code": "Código postal"
 }
diff --git a/package-lock.json b/package-lock.json
index 6033158..ad565f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -988,6 +988,49 @@
         "to-fast-properties": "^2.0.0"
       }
     },
+    "@eslint/eslintrc": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz",
+      "integrity": "sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.12.4",
+        "debug": "^4.1.1",
+        "espree": "^7.3.0",
+        "globals": "^12.1.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^3.13.1",
+        "minimatch": "^3.0.4",
+        "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "globals": {
+          "version": "12.4.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+          "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+          "dev": true,
+          "requires": {
+            "type-fest": "^0.8.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
     "@istanbuljs/load-nyc-config": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1164,6 +1207,12 @@
       "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==",
       "dev": true
     },
+    "@types/parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
+      "dev": true
+    },
     "abbrev": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -1424,9 +1473,9 @@
       "dev": true
     },
     "astral-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
-      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
       "dev": true
     },
     "async": {
@@ -2664,12 +2713,6 @@
         "strip-ansi": "~0.1.0"
       }
     },
-    "chardet": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
-      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
-      "dev": true
-    },
     "chokidar": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
@@ -2830,11 +2873,90 @@
         "restore-cursor": "^3.1.0"
       }
     },
-    "cli-width": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
-      "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
-      "dev": true
+    "cli-truncate": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+      "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+      "dev": true,
+      "requires": {
+        "slice-ansi": "^3.0.0",
+        "string-width": "^4.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "slice-ansi": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+          "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "astral-regex": "^2.0.0",
+            "is-fullwidth-code-point": "^3.0.0"
+          }
+        },
+        "string-width": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+          "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        }
+      }
     },
     "cliui": {
       "version": "5.0.0",
@@ -3142,6 +3264,39 @@
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
     },
+    "cosmiconfig": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",
+      "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==",
+      "dev": true,
+      "requires": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.2.1",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.10.0"
+      },
+      "dependencies": {
+        "parse-json": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+          "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "error-ex": "^1.3.1",
+            "json-parse-even-better-errors": "^2.3.0",
+            "lines-and-columns": "^1.1.6"
+          }
+        },
+        "path-type": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+          "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+          "dev": true
+        }
+      }
+    },
     "cp-file": {
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz",
@@ -3372,6 +3527,12 @@
         }
       }
     },
+    "dedent": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+      "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
+      "dev": true
+    },
     "deep-extend": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -3723,6 +3884,23 @@
         "has-binary2": "~1.0.2"
       }
     },
+    "enquirer": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "^4.1.1"
+      },
+      "dependencies": {
+        "ansi-colors": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+          "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+          "dev": true
+        }
+      }
+    },
     "errno": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
@@ -3818,89 +3996,108 @@
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
     },
     "eslint": {
-      "version": "6.8.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
-      "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz",
+      "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.0.0",
+        "@babel/code-frame": "7.12.11",
+        "@eslint/eslintrc": "^0.4.1",
         "ajv": "^6.10.0",
-        "chalk": "^2.1.0",
-        "cross-spawn": "^6.0.5",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
         "debug": "^4.0.1",
         "doctrine": "^3.0.0",
-        "eslint-scope": "^5.0.0",
-        "eslint-utils": "^1.4.3",
-        "eslint-visitor-keys": "^1.1.0",
-        "espree": "^6.1.2",
-        "esquery": "^1.0.1",
+        "enquirer": "^2.3.5",
+        "eslint-scope": "^5.1.1",
+        "eslint-utils": "^2.1.0",
+        "eslint-visitor-keys": "^2.0.0",
+        "espree": "^7.3.1",
+        "esquery": "^1.4.0",
         "esutils": "^2.0.2",
-        "file-entry-cache": "^5.0.1",
+        "file-entry-cache": "^6.0.1",
         "functional-red-black-tree": "^1.0.1",
         "glob-parent": "^5.0.0",
-        "globals": "^12.1.0",
+        "globals": "^13.6.0",
         "ignore": "^4.0.6",
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
-        "inquirer": "^7.0.0",
         "is-glob": "^4.0.0",
         "js-yaml": "^3.13.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.3.0",
-        "lodash": "^4.17.14",
+        "levn": "^0.4.1",
+        "lodash": "^4.17.21",
         "minimatch": "^3.0.4",
-        "mkdirp": "^0.5.1",
         "natural-compare": "^1.4.0",
-        "optionator": "^0.8.3",
+        "optionator": "^0.9.1",
         "progress": "^2.0.0",
-        "regexpp": "^2.0.1",
-        "semver": "^6.1.2",
-        "strip-ansi": "^5.2.0",
-        "strip-json-comments": "^3.0.1",
-        "table": "^5.2.3",
+        "regexpp": "^3.1.0",
+        "semver": "^7.2.1",
+        "strip-ansi": "^6.0.0",
+        "strip-json-comments": "^3.1.0",
+        "table": "^6.0.4",
         "text-table": "^0.2.0",
         "v8-compile-cache": "^2.0.3"
       },
       "dependencies": {
+        "@babel/code-frame": {
+          "version": "7.12.11",
+          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+          "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+          "dev": true,
+          "requires": {
+            "@babel/highlight": "^7.10.4"
+          }
+        },
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
         "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
           "dev": true,
           "requires": {
-            "color-convert": "^1.9.0"
+            "color-convert": "^2.0.1"
           }
         },
         "chalk": {
-          "version": "2.4.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+          "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
           "dev": true,
           "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
           }
         },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
         "cross-spawn": {
-          "version": "6.0.5",
-          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+          "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
           "dev": true,
           "requires": {
-            "nice-try": "^1.0.4",
-            "path-key": "^2.0.1",
-            "semver": "^5.5.0",
-            "shebang-command": "^1.2.0",
-            "which": "^1.2.9"
-          },
-          "dependencies": {
-            "semver": {
-              "version": "5.7.1",
-              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-              "dev": true
-            }
+            "path-key": "^3.1.0",
+            "shebang-command": "^2.0.0",
+            "which": "^2.0.1"
           }
         },
         "debug": {
@@ -3912,6 +4109,12 @@
             "ms": "2.1.2"
           }
         },
+        "eslint-visitor-keys": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+          "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+          "dev": true
+        },
         "glob-parent": {
           "version": "5.1.2",
           "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -3922,12 +4125,27 @@
           }
         },
         "globals": {
-          "version": "12.4.0",
-          "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
-          "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+          "version": "13.8.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz",
+          "integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==",
           "dev": true,
           "requires": {
-            "type-fest": "^0.8.1"
+            "type-fest": "^0.20.2"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
           }
         },
         "ms": {
@@ -3936,37 +4154,91 @@
           "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
           "dev": true
         },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+        "path-key": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
           "dev": true
         },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
           "dev": true,
           "requires": {
-            "ansi-regex": "^4.1.0"
+            "lru-cache": "^6.0.0"
           }
-        }
-      }
-    },
-    "eslint-scope": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
-      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
-      "dev": true,
-      "requires": {
+        },
+        "shebang-command": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+          "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+          "dev": true,
+          "requires": {
+            "shebang-regex": "^3.0.0"
+          }
+        },
+        "shebang-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+          "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "type-fest": {
+          "version": "0.20.2",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+          "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+          "dev": true
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "dev": true,
+          "requires": {
+            "isexe": "^2.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-scope": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+      "dev": true,
+      "requires": {
         "esrecurse": "^4.3.0",
         "estraverse": "^4.1.1"
       }
     },
     "eslint-utils": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
-      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+      "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
       "dev": true,
       "requires": {
         "eslint-visitor-keys": "^1.1.0"
@@ -3979,14 +4251,14 @@
       "dev": true
     },
     "espree": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
-      "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+      "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
       "dev": true,
       "requires": {
-        "acorn": "^7.1.1",
-        "acorn-jsx": "^5.2.0",
-        "eslint-visitor-keys": "^1.1.0"
+        "acorn": "^7.4.0",
+        "acorn-jsx": "^5.3.1",
+        "eslint-visitor-keys": "^1.3.0"
       }
     },
     "esprima": {
@@ -4257,17 +4529,6 @@
         }
       }
     },
-    "external-editor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
-      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
-      "dev": true,
-      "requires": {
-        "chardet": "^0.7.0",
-        "iconv-lite": "^0.4.24",
-        "tmp": "^0.0.33"
-      }
-    },
     "extglob": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
@@ -4392,12 +4653,12 @@
       }
     },
     "file-entry-cache": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
-      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
       "dev": true,
       "requires": {
-        "flat-cache": "^2.0.1"
+        "flat-cache": "^3.0.4"
       }
     },
     "file-type": {
@@ -4522,20 +4783,19 @@
       }
     },
     "flat-cache": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
-      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+      "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
       "dev": true,
       "requires": {
-        "flatted": "^2.0.0",
-        "rimraf": "2.6.3",
-        "write": "1.0.3"
+        "flatted": "^3.1.0",
+        "rimraf": "^3.0.2"
       },
       "dependencies": {
         "rimraf": {
-          "version": "2.6.3",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
-          "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
           "dev": true,
           "requires": {
             "glob": "^7.1.3"
@@ -4544,9 +4804,9 @@
       }
     },
     "flatted": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
-      "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
+      "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
       "dev": true
     },
     "follow-redirects": {
@@ -4751,6 +5011,12 @@
         "has-symbols": "^1.0.1"
       }
     },
+    "get-own-enumerable-property-symbols": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+      "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+      "dev": true
+    },
     "get-package-type": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -5145,6 +5411,18 @@
         "sshpk": "^1.7.0"
       }
     },
+    "human-signals": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+      "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+      "dev": true
+    },
+    "husky": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz",
+      "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==",
+      "dev": true
+    },
     "i18next": {
       "version": "14.1.1",
       "resolved": "https://registry.npmjs.org/i18next/-/i18next-14.1.1.tgz",
@@ -5374,125 +5652,6 @@
       "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
       "dev": true
     },
-    "inquirer": {
-      "version": "7.3.3",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
-      "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
-      "dev": true,
-      "requires": {
-        "ansi-escapes": "^4.2.1",
-        "chalk": "^4.1.0",
-        "cli-cursor": "^3.1.0",
-        "cli-width": "^3.0.0",
-        "external-editor": "^3.0.3",
-        "figures": "^3.0.0",
-        "lodash": "^4.17.19",
-        "mute-stream": "0.0.8",
-        "run-async": "^2.4.0",
-        "rxjs": "^6.6.0",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "through": "^2.3.6"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-          "dev": true
-        },
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-          "dev": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
-          "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true
-        },
-        "emoji-regex": {
-          "version": "8.0.0",
-          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-          "dev": true
-        },
-        "rxjs": {
-          "version": "6.6.7",
-          "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
-          "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
-          "dev": true,
-          "requires": {
-            "tslib": "^1.9.0"
-          }
-        },
-        "string-width": {
-          "version": "4.2.2",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
-          "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^8.0.0",
-            "is-fullwidth-code-point": "^3.0.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^5.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
     "intl": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/intl/-/intl-1.2.5.tgz",
@@ -5811,6 +5970,12 @@
         "has-symbols": "^1.0.2"
       }
     },
+    "is-regexp": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+      "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
+      "dev": true
+    },
     "is-retry-allowed": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz",
@@ -5842,6 +6007,12 @@
       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
       "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
     },
+    "is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+      "dev": true
+    },
     "is-utf8": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@@ -6145,6 +6316,12 @@
       "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
       "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
     },
+    "json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true
+    },
     "json-schema": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@@ -6315,21 +6492,395 @@
       }
     },
     "levn": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
-      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      }
+    },
+    "limiter": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+      "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==",
+      "dev": true
+    },
+    "lines-and-columns": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+      "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
+      "dev": true
+    },
+    "lint-staged": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.0.0.tgz",
+      "integrity": "sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.1",
+        "cli-truncate": "^2.1.0",
+        "commander": "^7.2.0",
+        "cosmiconfig": "^7.0.0",
+        "debug": "^4.3.1",
+        "dedent": "^0.7.0",
+        "enquirer": "^2.3.6",
+        "execa": "^5.0.0",
+        "listr2": "^3.8.2",
+        "log-symbols": "^4.1.0",
+        "micromatch": "^4.0.4",
+        "normalize-path": "^3.0.0",
+        "please-upgrade-node": "^3.2.0",
+        "string-argv": "0.3.1",
+        "stringify-object": "^3.3.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "braces": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+          "dev": true,
+          "requires": {
+            "fill-range": "^7.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+          "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "commander": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+          "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+          "dev": true
+        },
+        "cross-spawn": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+          "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+          "dev": true,
+          "requires": {
+            "path-key": "^3.1.0",
+            "shebang-command": "^2.0.0",
+            "which": "^2.0.1"
+          }
+        },
+        "debug": {
+          "version": "4.3.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "execa": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz",
+          "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^7.0.3",
+            "get-stream": "^6.0.0",
+            "human-signals": "^2.1.0",
+            "is-stream": "^2.0.0",
+            "merge-stream": "^2.0.0",
+            "npm-run-path": "^4.0.1",
+            "onetime": "^5.1.2",
+            "signal-exit": "^3.0.3",
+            "strip-final-newline": "^2.0.0"
+          }
+        },
+        "fill-range": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+          "dev": true,
+          "requires": {
+            "to-regex-range": "^5.0.1"
+          }
+        },
+        "get-stream": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+          "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "is-number": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+          "dev": true
+        },
+        "is-stream": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+          "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+          "dev": true
+        },
+        "log-symbols": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+          "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+          "dev": true,
+          "requires": {
+            "chalk": "^4.1.0",
+            "is-unicode-supported": "^0.1.0"
+          }
+        },
+        "micromatch": {
+          "version": "4.0.4",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+          "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+          "dev": true,
+          "requires": {
+            "braces": "^3.0.1",
+            "picomatch": "^2.2.3"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
+        "npm-run-path": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+          "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+          "dev": true,
+          "requires": {
+            "path-key": "^3.0.0"
+          }
+        },
+        "path-key": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+          "dev": true
+        },
+        "shebang-command": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+          "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+          "dev": true,
+          "requires": {
+            "shebang-regex": "^3.0.0"
+          }
+        },
+        "shebang-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+          "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "to-regex-range": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+          "dev": true,
+          "requires": {
+            "is-number": "^7.0.0"
+          }
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "dev": true,
+          "requires": {
+            "isexe": "^2.0.0"
+          }
+        }
+      }
+    },
+    "listr2": {
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.8.2.tgz",
+      "integrity": "sha512-E28Fw7Zd3HQlCJKzb9a8C8M0HtFWQeucE+S8YrSrqZObuCLPRHMRrR8gNmYt65cU9orXYHwvN5agXC36lYt7VQ==",
       "dev": true,
       "requires": {
-        "prelude-ls": "~1.1.2",
-        "type-check": "~0.3.2"
+        "chalk": "^4.1.1",
+        "cli-truncate": "^2.1.0",
+        "figures": "^3.2.0",
+        "indent-string": "^4.0.0",
+        "log-update": "^4.0.0",
+        "p-map": "^4.0.0",
+        "rxjs": "^6.6.7",
+        "through": "^2.3.8",
+        "wrap-ansi": "^7.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+          "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "indent-string": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+          "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "p-map": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+          "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+          "dev": true,
+          "requires": {
+            "aggregate-error": "^3.0.0"
+          }
+        },
+        "rxjs": {
+          "version": "6.6.7",
+          "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+          "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+          "dev": true,
+          "requires": {
+            "tslib": "^1.9.0"
+          }
+        },
+        "string-width": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+          "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+          "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "string-width": "^4.1.0",
+            "strip-ansi": "^6.0.0"
+          }
+        }
       }
     },
-    "limiter": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
-      "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==",
-      "dev": true
-    },
     "load-json-file": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
@@ -6502,6 +7053,12 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
     },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+      "dev": true
+    },
     "lodash.flattendeep": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
@@ -6554,6 +7111,12 @@
       "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
       "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
     },
+    "lodash.truncate": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+      "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=",
+      "dev": true
+    },
     "log-symbols": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@@ -6585,6 +7148,93 @@
         }
       }
     },
+    "log-update": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+      "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^4.3.0",
+        "cli-cursor": "^3.1.0",
+        "slice-ansi": "^4.0.0",
+        "wrap-ansi": "^6.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+          "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "string-width": "^4.1.0",
+            "strip-ansi": "^6.0.0"
+          }
+        }
+      }
+    },
     "logalot": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz",
@@ -7215,12 +7865,6 @@
       "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz",
       "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ=="
     },
-    "mute-stream": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
-      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
-      "dev": true
-    },
     "nan": {
       "version": "2.14.2",
       "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
@@ -8182,17 +8826,17 @@
       "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA=="
     },
     "optionator": {
-      "version": "0.8.3",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
-      "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
       "dev": true,
       "requires": {
-        "deep-is": "~0.1.3",
-        "fast-levenshtein": "~2.0.6",
-        "levn": "~0.3.0",
-        "prelude-ls": "~1.1.2",
-        "type-check": "~0.3.2",
-        "word-wrap": "~1.2.3"
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.3"
       }
     },
     "os-filter-obj": {
@@ -8254,12 +8898,6 @@
         }
       }
     },
-    "os-tmpdir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
-      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
-      "dev": true
-    },
     "p-cancelable": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
@@ -8639,6 +9277,15 @@
         "find-up": "^3.0.0"
       }
     },
+    "please-upgrade-node": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+      "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+      "dev": true,
+      "requires": {
+        "semver-compare": "^1.0.0"
+      }
+    },
     "pngquant-bin": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/pngquant-bin/-/pngquant-bin-5.0.2.tgz",
@@ -8717,9 +9364,9 @@
       "dev": true
     },
     "prelude-ls": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
-      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
       "dev": true
     },
     "prepend-http": {
@@ -8728,6 +9375,12 @@
       "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
       "dev": true
     },
+    "prettier": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz",
+      "integrity": "sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==",
+      "dev": true
+    },
     "process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -9279,9 +9932,9 @@
       }
     },
     "regexpp": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
-      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
+      "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
       "dev": true
     },
     "regexpu-core": {
@@ -9447,6 +10100,12 @@
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
     },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true
+    },
     "require-main-filename": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -9608,12 +10267,6 @@
         "estree-walker": "^0.6.1"
       }
     },
-    "run-async": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
-      "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
-      "dev": true
-    },
     "rx": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
@@ -9684,6 +10337,12 @@
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
       "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
     },
+    "semver-compare": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+      "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+      "dev": true
+    },
     "semver-diff": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
@@ -9929,24 +10588,45 @@
       "dev": true
     },
     "slice-ansi": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
-      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
       "dev": true,
       "requires": {
-        "ansi-styles": "^3.2.0",
-        "astral-regex": "^1.0.0",
-        "is-fullwidth-code-point": "^2.0.0"
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
       },
       "dependencies": {
         "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
           "dev": true,
           "requires": {
-            "color-convert": "^1.9.0"
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
           }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
         }
       }
     },
@@ -10432,6 +11112,12 @@
       "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
       "dev": true
     },
+    "string-argv": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
+      "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
+      "dev": true
+    },
     "string-width": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@@ -10491,6 +11177,25 @@
         "safe-buffer": "~5.1.0"
       }
     },
+    "stringify-object": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
+      "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+      "dev": true,
+      "requires": {
+        "get-own-enumerable-property-symbols": "^3.0.0",
+        "is-obj": "^1.0.1",
+        "is-regexp": "^1.0.0"
+      },
+      "dependencies": {
+        "is-obj": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+          "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
+          "dev": true
+        }
+      }
+    },
     "strip-ansi": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz",
@@ -10516,6 +11221,12 @@
       "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
       "dev": true
     },
+    "strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true
+    },
     "strip-indent": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
@@ -10628,15 +11339,75 @@
       "dev": true
     },
     "table": {
-      "version": "5.4.6",
-      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
-      "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+      "version": "6.7.1",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz",
+      "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==",
       "dev": true,
       "requires": {
-        "ajv": "^6.10.2",
-        "lodash": "^4.17.14",
-        "slice-ansi": "^2.1.0",
-        "string-width": "^3.0.0"
+        "ajv": "^8.0.1",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.truncate": "^4.4.2",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.4.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.4.0.tgz",
+          "integrity": "sha512-7QD2l6+KBSLwf+7MuYocbWvRPdOu63/trReTLu2KFwkgctnub1auoF+Y1WYcm09CTM7quuscrzqmASaLHC/K4Q==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-regex": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+          "dev": true
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+          "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        }
       }
     },
     "tar-stream": {
@@ -10813,15 +11584,6 @@
       "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
       "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
     },
-    "tmp": {
-      "version": "0.0.33",
-      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
-      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
-      "dev": true,
-      "requires": {
-        "os-tmpdir": "~1.0.2"
-      }
-    },
     "to-array": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
@@ -10962,12 +11724,12 @@
       "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
     },
     "type-check": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
-      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
       "dev": true,
       "requires": {
-        "prelude-ls": "~1.1.2"
+        "prelude-ls": "^1.2.1"
       }
     },
     "type-detect": {
@@ -11488,15 +12250,6 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
     },
-    "write": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
-      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
-      "dev": true,
-      "requires": {
-        "mkdirp": "^0.5.1"
-      }
-    },
     "write-file-atomic": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
@@ -11541,6 +12294,12 @@
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
     },
+    "yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "dev": true
+    },
     "yargs": {
       "version": "13.3.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
diff --git a/package.json b/package.json
index f2e2777..4aa4aaf 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,15 @@
     "watch:img": "nodemon -e png,jpg -w frontend/images scripts/build.js img",
     "builddev": "node scripts/build.js static",
     "lint": "eslint .",
+    "prepare": "husky install",
     "test": "LOCA_DEMOMODE=false LOCA_PRODUCTIVE=true mocha",
     "coverage": "LOCA_DEMOMODE=false LOCA_PRODUCTIVE=true nyc --reporter=lcov --reporter=text-summary mocha",
     "mongodump": "node scripts/mongodump.js",
     "mongorestore": "node scripts/mongorestore.js"
   },
+  "lint-staged": {
+    "**/*": "prettier --write --ignore-unknown"
+  },
   "dependencies": {
     "axios": "0.21.1",
     "bcryptjs": "2.4.3",
@@ -67,16 +71,19 @@
     "@babel/preset-env": "7.9.5",
     "babel-eslint": "10.1.0",
     "browser-sync": "2.26.14",
-    "eslint": "6.8.0",
+    "eslint": "7.26.0",
     "fs-extra": "9.0.0",
+    "husky": "6.0.0",
     "imagemin": "6.1.0",
     "imagemin-mozjpeg": "8.0.0",
     "imagemin-pngquant": "6.0.1",
     "less": "3.11.1",
+    "lint-staged": "11.0.0",
     "mocha": "6.2.3",
     "nodemon": "2.0.3",
     "npm-run-all": "4.1.5",
     "nyc": "15.0.1",
+    "prettier": "2.3.0",
     "proxyquire": "2.0.0",
     "purify-css": "1.2.6",
     "rollup": "2.6.1",
diff --git a/scripts/build.js b/scripts/build.js
index 9fd7109..7cc9eca 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -32,224 +32,229 @@ const dist_locales_directory = path.join(dist_directory, 'locales');
 const templates_pattern = path.join(view_directory, '/**/*.ejs');
 const images_pattern = path.join(image_directory, '/**/*.{jpg,png}');
 
-const buildCss = async ({inputOptions, outputOptions}) => {
-    await fs.ensureDir(dist_css_directory);
-    const code = fs.readFileSync(path.join(less_directory, `${outputOptions.name}.less`), 'utf8');
-    const output = await less.render(code, {
-        paths: [less_directory]
-    });
-    const css_file_path = path.join(dist_css_directory, `${outputOptions.name}.css`);
-    const cssmin_file_path = path.join(dist_css_directory, `${outputOptions.name}.min.css`);
-
-    fs.writeFileSync(css_file_path, output.css);
-
-    if (process.env.NODE_ENV !== 'production') {
-        return output.css;
-    }
+const buildCss = async ({ inputOptions, outputOptions }) => {
+  await fs.ensureDir(dist_css_directory);
+  const code = fs.readFileSync(
+    path.join(less_directory, `${outputOptions.name}.less`),
+    'utf8'
+  );
+  const output = await less.render(code, {
+    paths: [less_directory],
+  });
+  const css_file_path = path.join(
+    dist_css_directory,
+    `${outputOptions.name}.css`
+  );
+  const cssmin_file_path = path.join(
+    dist_css_directory,
+    `${outputOptions.name}.min.css`
+  );
 
-    const content = [outputOptions.file, templates_pattern];
-    if (inputOptions.external) {
-        content.push(...inputOptions.external);
-    }
-    return await new Promise((resolve, reject) => {
-        try {
-            purify(
-                content,
-                [css_file_path],
-                {
-                    minify: true,
-                    info: false,
-                    rejected: false
-                },
-                purified_css => {
-                    if (!purified_css) {
-                        reject('purify exited with an empty css');
-                    }
-                    fs.writeFileSync(cssmin_file_path, purified_css);
-                    resolve(purified_css);
-                }
-            );
-        } catch (error) {
-            reject(error);
+  fs.writeFileSync(css_file_path, output.css);
+
+  if (process.env.NODE_ENV !== 'production') {
+    return output.css;
+  }
+
+  const content = [outputOptions.file, templates_pattern];
+  if (inputOptions.external) {
+    content.push(...inputOptions.external);
+  }
+  return await new Promise((resolve, reject) => {
+    try {
+      purify(
+        content,
+        [css_file_path],
+        {
+          minify: true,
+          info: false,
+          rejected: false,
+        },
+        (purified_css) => {
+          if (!purified_css) {
+            reject('purify exited with an empty css');
+          }
+          fs.writeFileSync(cssmin_file_path, purified_css);
+          resolve(purified_css);
         }
-    });
+      );
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
-const buildJs = async ({inputOptions, outputOptions}) => {
-    await fs.ensureDir(dist_js_directory);
-    const bundle = await rollup(inputOptions);
-    await bundle.write(outputOptions);
+const buildJs = async ({ inputOptions, outputOptions }) => {
+  await fs.ensureDir(dist_js_directory);
+  const bundle = await rollup(inputOptions);
+  await bundle.write(outputOptions);
 };
 
 const buildImg = async () => {
-    await fs.ensureDir(dist_images_directory);
-    await imagemin([images_pattern], dist_images_directory, {
-        plugins: [
-            imageminMozjpeg(),
-            imageminPngquant({ quality: '65-80' })
-        ]
-    });
+  await fs.ensureDir(dist_images_directory);
+  await imagemin([images_pattern], dist_images_directory, {
+    plugins: [imageminMozjpeg(), imageminPngquant({ quality: '65-80' })],
+  });
 };
 
 const copyLocales = () => {
-    fs.copySync(
-        locale_directory,
-        dist_locales_directory
-    );
+  fs.copySync(locale_directory, dist_locales_directory);
 };
 
 const copyRobots = () => {
-    fs.copySync(
-        path.join(frontend_directory, 'robots.txt'),
-        path.join(dist_directory, 'robots.txt')
-    );
+  fs.copySync(
+    path.join(frontend_directory, 'robots.txt'),
+    path.join(dist_directory, 'robots.txt')
+  );
 };
 
 const copySitemap = () => {
-    fs.copySync(
-        path.join(frontend_directory, 'sitemap.xml'),
-        path.join(dist_directory, 'sitemap.xml')
-    );
+  fs.copySync(
+    path.join(frontend_directory, 'sitemap.xml'),
+    path.join(dist_directory, 'sitemap.xml')
+  );
 };
 
 const clean = (directory) => {
-    fs.emptydirSync(directory || dist_directory);
+  fs.emptydirSync(directory || dist_directory);
 };
 
 const plugins = () => {
-    const list = [
-        commonjs(),
-        babel({
-            exclude: 'node_modules/**'
-        }),
-        includePaths({
-            external: [
-                'accounting',
-                'bootbox',
-                'handlebars',
-                'historyjs',
-                'i18next',
-                'jquery',
-                'minivents',
-                'moment',
-                'sugar',
-                'frontexpress'
-            ]
-        })
-    ];
-
-    if (process.env.NODE_ENV === 'production') {
-        list.push(terser());
-    }
+  const list = [
+    commonjs(),
+    babel({
+      exclude: 'node_modules/**',
+    }),
+    includePaths({
+      external: [
+        'accounting',
+        'bootbox',
+        'handlebars',
+        'historyjs',
+        'i18next',
+        'jquery',
+        'minivents',
+        'moment',
+        'sugar',
+        'frontexpress',
+      ],
+    }),
+  ];
+
+  if (process.env.NODE_ENV === 'production') {
+    list.push(terser());
+  }
 
-    return list;
+  return list;
 };
 
-const outputFileSuffix =  (process.env.NODE_ENV === 'production' && '.min') || '';
-
-const outputOptions ={
-    format: 'umd',
-    globals: {
-        'accounting': 'accounting',
-        'bootbox': 'bootbox',
-        'handlebars': 'Handlebars',
-        'historyjs': 'History',
-        'i18next': 'i18next',
-        'jquery': '$',
-        'minivents': 'Events',
-        'moment': 'moment',
-        'sugar': 'Sugar',
-        'frontexpress': 'frontexpress'
-    },
-    sourcemap: true
+const outputFileSuffix =
+  (process.env.NODE_ENV === 'production' && '.min') || '';
+
+const outputOptions = {
+  format: 'umd',
+  globals: {
+    accounting: 'accounting',
+    bootbox: 'bootbox',
+    handlebars: 'Handlebars',
+    historyjs: 'History',
+    i18next: 'i18next',
+    jquery: '$',
+    minivents: 'Events',
+    moment: 'moment',
+    sugar: 'Sugar',
+    frontexpress: 'frontexpress',
+  },
+  sourcemap: true,
 };
 
 const print = {
-    inputOptions: {
-        input: path.join(js_directory, 'print.js'),
-        plugins: plugins()
-    },
-    outputOptions: {
-        name: 'print',
-        file: path.join(dist_js_directory, `print${outputFileSuffix}.js`),
-        ...outputOptions
-    }
+  inputOptions: {
+    input: path.join(js_directory, 'print.js'),
+    plugins: plugins(),
+  },
+  outputOptions: {
+    name: 'print',
+    file: path.join(dist_js_directory, `print${outputFileSuffix}.js`),
+    ...outputOptions,
+  },
 };
 
 const index = {
-    inputOptions: {
-        input: path.join(js_directory, 'index.js'),
-        plugins: plugins(),
-        external: [
-            path.join(node_modules_directory, 'bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js'),
-            path.join(node_modules_directory, 'bootbox/dist/bootbox.min.js')
-        ]
-    },
-    outputOptions: {
-        name: 'index',
-        file: path.join(dist_js_directory, `index${outputFileSuffix}.js`),
-        ...outputOptions
-    }
+  inputOptions: {
+    input: path.join(js_directory, 'index.js'),
+    plugins: plugins(),
+    external: [
+      path.join(
+        node_modules_directory,
+        'bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js'
+      ),
+      path.join(node_modules_directory, 'bootbox/dist/bootbox.min.js'),
+    ],
+  },
+  outputOptions: {
+    name: 'index',
+    file: path.join(dist_js_directory, `index${outputFileSuffix}.js`),
+    ...outputOptions,
+  },
 };
 
 const build = async (buildWhat) => {
-    try {
-        if (buildWhat) {
-            switch (buildWhat) {
-            case 'js':
-                clean(dist_js_directory);
-                await buildJs(print);
-                await buildJs(index);
-                console.log('js files built');
-                break;
-            case 'css':
-                clean(dist_css_directory);
-                await buildCss(print);
-                await buildCss(index);
-                console.log('less files built');
-                break;
-            case 'img':
-                clean(dist_images_directory);
-                await buildImg();
-                console.log('image files built');
-                break;
-            case 'static':
-                clean(dist_locales_directory);
-                copyLocales();
-                copyRobots();
-                copySitemap();
-                console.log('static files built');
-                break;
-            default:
-                console.error('target not found');
-                break;
-            }
-        } else {
-            // Full build
-            clean();
-            await buildJs(print);
-            await buildCss(print);
-            await buildJs(index);
-            await buildCss(index);
-            await buildImg();
-            copyLocales();
-            copyRobots();
-            copySitemap();
-            console.log('files built');
-        }
-    } catch (error) {
-        console.error(error);
+  try {
+    if (buildWhat) {
+      switch (buildWhat) {
+        case 'js':
+          clean(dist_js_directory);
+          await buildJs(print);
+          await buildJs(index);
+          console.log('js files built');
+          break;
+        case 'css':
+          clean(dist_css_directory);
+          await buildCss(print);
+          await buildCss(index);
+          console.log('less files built');
+          break;
+        case 'img':
+          clean(dist_images_directory);
+          await buildImg();
+          console.log('image files built');
+          break;
+        case 'static':
+          clean(dist_locales_directory);
+          copyLocales();
+          copyRobots();
+          copySitemap();
+          console.log('static files built');
+          break;
+        default:
+          console.error('target not found');
+          break;
+      }
+    } else {
+      // Full build
+      clean();
+      await buildJs(print);
+      await buildCss(print);
+      await buildJs(index);
+      await buildCss(index);
+      await buildImg();
+      copyLocales();
+      copyRobots();
+      copySitemap();
+      console.log('files built');
     }
+  } catch (error) {
+    console.error(error);
+  }
 };
 
 (async () => {
-    let buildWhat;
-    if (process.argv.length > 2) {
-        // Partial build
-        buildWhat = process.argv[2].toLowerCase();
-    }
+  let buildWhat;
+  if (process.argv.length > 2) {
+    // Partial build
+    buildWhat = process.argv[2].toLowerCase();
+  }
 
-    await build(buildWhat);
+  await build(buildWhat);
 })();
-
-
diff --git a/scripts/migration.js b/scripts/migration.js
index 14f913c..4595f19 100644
--- a/scripts/migration.js
+++ b/scripts/migration.js
@@ -4,293 +4,372 @@ const realmModel = require('../backend/models/realm');
 const tenantModel = require('../backend/models/occupant');
 const leaseModel = require('../backend/models/lease');
 
-const updateRealm = async realm => {
-    return await new Promise((resolve, reject) => {
-        try {
-            realmModel.update(realm, (errors, saved) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(saved);
-            });
-        } catch (error) {
-            reject(error);
+const updateRealm = async (realm) => {
+  return await new Promise((resolve, reject) => {
+    try {
+      realmModel.update(realm, (errors, saved) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(saved);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 const addLease = async (realm, lease) => {
-    return await new Promise((resolve, reject) => {
-        try {
-            leaseModel.add(realm, lease, (errors, saved) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(saved);
-            });
-        } catch (error) {
-            reject(error);
+  return await new Promise((resolve, reject) => {
+    try {
+      leaseModel.add(realm, lease, (errors, saved) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(saved);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 const updateLease = async (realm, lease) => {
-    return await new Promise((resolve, reject) => {
-        try {
-            leaseModel.update(realm, lease, (errors, saved) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(saved);
-            });
-        } catch (error) {
-            reject(error);
+  return await new Promise((resolve, reject) => {
+    try {
+      leaseModel.update(realm, lease, (errors, saved) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(saved);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 const updateTenant = async (realm, tenant) => {
-    return await new Promise((resolve, reject) => {
-        try {
-            tenantModel.update(realm, tenant, (errors, saved) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(saved);
-            });
-        } catch (error) {
-            reject(error);
+  return await new Promise((resolve, reject) => {
+    try {
+      tenantModel.update(realm, tenant, (errors, saved) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(saved);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
-
 const findAllAccounts = () => {
-    return new Promise((resolve, reject) => {
-        try {
-            accountModel.findAll((errors, accounts) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(accounts);
-            });
-        } catch (error) {
-            reject(error);
+  return new Promise((resolve, reject) => {
+    try {
+      accountModel.findAll((errors, accounts) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(accounts);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 const findAllRealms = () => {
-    return new Promise((resolve, reject) => {
-        try {
-            realmModel.findAll((errors, realms) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(realms);
-            });
-        } catch (error) {
-            reject(error);
+  return new Promise((resolve, reject) => {
+    try {
+      realmModel.findAll((errors, realms) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(realms);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 const findAllTenants = (realm) => {
-    return new Promise((resolve, reject) => {
-        try {
-            tenantModel.findAll(realm, (errors, tenants) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(tenants);
-            });
-        } catch (error) {
-            reject(error);
+  return new Promise((resolve, reject) => {
+    try {
+      tenantModel.findAll(realm, (errors, tenants) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(tenants);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 const findAllLeases = (realm) => {
-    return new Promise((resolve, reject) => {
-        try {
-            leaseModel.findAll(realm, (errors, leases) => {
-                if (errors) {
-                    return reject(errors);
-                }
-                resolve(leases);
-            });
-        } catch (error) {
-            reject(error);
+  return new Promise((resolve, reject) => {
+    try {
+      leaseModel.findAll(realm, (errors, leases) => {
+        if (errors) {
+          return reject(errors);
         }
-    });
+        resolve(leases);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
 
 // Main
 module.exports = async () => {
-    try {
-        await db.init();
-        const realms = await findAllRealms();
-        const accounts = await findAllAccounts();
-        const accountMap = accounts.reduce((acc, { email, firstname, lastname }) => {
-            acc[email] = `${firstname} ${lastname}`;
-            return acc;
-        }, { '': '' });
-        const updatedRealms = await Promise.all(realms.map(async realm => {
-            const updatedRealm = {
-                _id: realm._id,
-                name: realm.name,
-                members: realm.members || [
-                    { name: accountMap[realm.administrator], registered: true, email: realm.administrator, role: 'administrator' },
-                    { name: accountMap[realm.user1 || ''], registered: true, email: realm.user1, role: 'renter' },
-                    { name: accountMap[realm.user2 || ''], registered: true, email: realm.user2, role: 'renter' },
-                    { name: accountMap[realm.user3 || ''], registered: true, email: realm.user3, role: 'renter' },
-                    { name: accountMap[realm.user4 || ''], registered: true, email: realm.user4, role: 'renter' },
-                    { name: accountMap[realm.user5 || ''], registered: true, email: realm.user5, role: 'renter' },
-                    { name: accountMap[realm.user6 || ''], registered: true, email: realm.user6, role: 'renter' },
-                    { name: accountMap[realm.user7 || ''], registered: true, email: realm.user7, role: 'renter' },
-                    { name: accountMap[realm.user8 || ''], registered: true, email: realm.user8, role: 'renter' },
-                    { name: accountMap[realm.user9 || ''], registered: true, email: realm.user9, role: 'renter' },
-                    { name: accountMap[realm.user10 || ''], registered: true, email: realm.user10, role: 'renter' }
-                ].filter(member => !!member.email),      // [{ email, role },]
-                isCompany: realm.isCompany,
-                locale: realm.locale || 'en',
-                currency: realm.currency || 'EUR'
-            };
-            if (realm.addresses || realm.street1 || realm.street2 || realm.zipCode || realm.city) {
-                updatedRealm.addresses = realm.addresses || [
-                    {
-                        street1: realm.street1 || '',
-                        street2: realm.street2 || '',
-                        zipCode: realm.zipCode || '',
-                        city: realm.city || '',
-                        state: realm.state || '',
-                        country: realm.country || ''
-                    }
-                ];    // [{ street1, street2, zipCode, city, state, country }, ]
+  try {
+    await db.init();
+    const realms = await findAllRealms();
+    const accounts = await findAllAccounts();
+    const accountMap = accounts.reduce(
+      (acc, { email, firstname, lastname }) => {
+        acc[email] = `${firstname} ${lastname}`;
+        return acc;
+      },
+      { '': '' }
+    );
+    const updatedRealms = await Promise.all(
+      realms.map(async (realm) => {
+        const updatedRealm = {
+          _id: realm._id,
+          name: realm.name,
+          members:
+            realm.members ||
+            [
+              {
+                name: accountMap[realm.administrator],
+                registered: true,
+                email: realm.administrator,
+                role: 'administrator',
+              },
+              {
+                name: accountMap[realm.user1 || ''],
+                registered: true,
+                email: realm.user1,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user2 || ''],
+                registered: true,
+                email: realm.user2,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user3 || ''],
+                registered: true,
+                email: realm.user3,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user4 || ''],
+                registered: true,
+                email: realm.user4,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user5 || ''],
+                registered: true,
+                email: realm.user5,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user6 || ''],
+                registered: true,
+                email: realm.user6,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user7 || ''],
+                registered: true,
+                email: realm.user7,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user8 || ''],
+                registered: true,
+                email: realm.user8,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user9 || ''],
+                registered: true,
+                email: realm.user9,
+                role: 'renter',
+              },
+              {
+                name: accountMap[realm.user10 || ''],
+                registered: true,
+                email: realm.user10,
+                role: 'renter',
+              },
+            ].filter((member) => !!member.email), // [{ email, role },]
+          isCompany: realm.isCompany,
+          locale: realm.locale || 'en',
+          currency: realm.currency || 'EUR',
+        };
+        if (
+          realm.addresses ||
+          realm.street1 ||
+          realm.street2 ||
+          realm.zipCode ||
+          realm.city
+        ) {
+          updatedRealm.addresses = realm.addresses || [
+            {
+              street1: realm.street1 || '',
+              street2: realm.street2 || '',
+              zipCode: realm.zipCode || '',
+              city: realm.city || '',
+              state: realm.state || '',
+              country: realm.country || '',
+            },
+          ]; // [{ street1, street2, zipCode, city, state, country }, ]
+        }
+        if (
+          realm.contacts ||
+          realm.contact ||
+          realm.email ||
+          realm.phone1 ||
+          realm.phone2
+        ) {
+          updatedRealm.contacts = realm.contacts || [
+            {
+              name: realm.contact || '',
+              email: realm.email || '',
+              phone1: realm.phone1 || '',
+              phone2: realm.phone2 || '',
+            },
+          ]; // [{ name, email, phone1, phone2 }]
+        }
+        if (realm.bankInfo || realm.bank) {
+          updatedRealm.bankInfo = realm.bankInfo || {
+            name: realm.bank,
+            iban: realm.rib || '',
+          }; // { name, iban }
+        }
+        if (realm.isCompany) {
+          updatedRealm.companyInfo = realm.companyInfo || {
+            name: realm.company || '',
+            legalStructure: realm.legalForm || '',
+            capital: realm.capital || '',
+            ein: realm.siret || '',
+            dos: realm.rcs || '',
+            vatNumber: realm.vatNumber || '',
+            legalRepresentative: realm.manager || '',
+          };
+        }
 
-            }
-            if (realm.contacts || realm.contact || realm.email || realm.phone1 || realm.phone2) {
-                updatedRealm.contacts = realm.contacts || [
-                    {
-                        name: realm.contact || '',
-                        email: realm.email || '',
-                        phone1: realm.phone1 || '',
-                        phone2: realm.phone2 || ''
-                    }
-                ];     // [{ name, email, phone1, phone2 }]
-            }
-            if (realm.bankInfo || realm.bank) {
-                updatedRealm.bankInfo = realm.bankInfo || {
-                    name: realm.bank,
-                    iban: realm.rib || ''
-                };    // { name, iban }
-            }
-            if (realm.isCompany) {
-                updatedRealm.companyInfo = realm.companyInfo || {
-                    name: realm.company || '',
-                    legalStructure: realm.legalForm || '',
-                    capital: realm.capital || '',
-                    ein: realm.siret || '',
-                    dos: realm.rcs || '',
-                    vatNumber: realm.vatNumber || '',
-                    legalRepresentative: realm.manager || ''
-                };
-            }
+        let dbCustomLease;
+        let dbFrench369Lease;
+        const leases = await findAllLeases(realm);
+        const customLease = {
+          name: 'custom',
+          description: 'Monthly rents without time limit',
+          timeRange: 'months',
+          active: true,
+          system: true,
+          archived: false,
+        };
+        const french369Lease = {
+          name: '369',
+          description: 'French business lease limited to 9 years',
+          numberOfTerms: 108,
+          timeRange: 'months',
+          active: true,
+          system: false,
+        };
+        if (!leases || !leases.length) {
+          dbCustomLease = await addLease(realm, customLease);
+          dbFrench369Lease = await addLease(realm, french369Lease);
+        } else {
+          dbCustomLease = leases.find(({ system }) => system === true);
+          dbFrench369Lease = leases.find(
+            ({ numberOfTerms }) => numberOfTerms === 108
+          );
 
-            let dbCustomLease;
-            let dbFrench369Lease;
-            const leases = await findAllLeases(realm);
-            const customLease = {
-                name: 'custom',
-                description: 'Monthly rents without time limit',
-                timeRange: 'months',
-                active: true,
-                system: true,
-                archived: false
-            };
-            const french369Lease = {
-                name: '369',
-                description: 'French business lease limited to 9 years',
-                numberOfTerms: 108,
-                timeRange: 'months',
-                active: true,
-                system: false
-            };
-            if (!leases || !leases.length) {
-                dbCustomLease = await addLease(realm, customLease);
-                dbFrench369Lease = await addLease(realm, french369Lease);
-            }
-            else {
-                dbCustomLease = leases.find(({ system }) => system === true);
-                dbFrench369Lease = leases.find(({ numberOfTerms }) => numberOfTerms === 108);
+          if (dbCustomLease) {
+            dbCustomLease = await updateLease(realm, {
+              ...dbCustomLease,
+              ...customLease,
+            });
+          }
 
-                if (dbCustomLease) {
-                    dbCustomLease = await updateLease(realm, {
-                        ...dbCustomLease,
-                        ...customLease
-                    });
-                }
+          if (dbFrench369Lease) {
+            dbFrench369Lease = await updateLease(realm, {
+              ...dbFrench369Lease,
+              ...french369Lease,
+            });
+          }
+        }
 
-                if (dbFrench369Lease) {
-                    dbFrench369Lease = await updateLease(realm, {
-                        ...dbFrench369Lease,
-                        ...french369Lease
-                    });
-                }
+        const tenants = await findAllTenants(realm);
+        await Promise.all(
+          tenants.map(async (dbTenant) => {
+            // add the leaseId property to tenant (occupant)
+            let leaseId = dbTenant.leaseId;
+            if (dbCustomLease && dbFrench369Lease) {
+              leaseId = String(dbCustomLease._id);
+              if (dbTenant.contract === '369') {
+                leaseId = String(dbFrench369Lease._id);
+              }
             }
 
-            const tenants = await findAllTenants(realm);
-            await Promise.all(tenants.map(async (dbTenant) => {
-                // add the leaseId property to tenant (occupant)
-                let leaseId = dbTenant.leaseId;
-                if (dbCustomLease && dbFrench369Lease) {
-                    leaseId = String(dbCustomLease._id);
-                    if (dbTenant.contract === '369') {
-                        leaseId = String(dbFrench369Lease._id);
-                    }
-                }
-
-                // update the properties to add the rent and expenses of properties
-                let properties = dbTenant.properties;
-                properties.forEach(property => {
-                    if (property.expenses) {
-                        return;
-                    }
-                    property.rent = property.property.price;
-                    property.expenses = [];
+            // update the properties to add the rent and expenses of properties
+            let properties = dbTenant.properties;
+            properties.forEach((property) => {
+              if (property.expenses) {
+                return;
+              }
+              property.rent = property.property.price;
+              property.expenses = [];
 
-                    if (!property.property.expense) {
-                        return;
-                    }
+              if (!property.property.expense) {
+                return;
+              }
 
-                    property.expenses = [{
-                        title: 'general expense',
-                        amount: property.property.expense
-                    }];
-                });
+              property.expenses = [
+                {
+                  title: 'general expense',
+                  amount: property.property.expense,
+                },
+              ];
+            });
 
-                await updateTenant(realm, {
-                    ...dbTenant,
-                    leaseId,
-                    properties
-                });
-            }));
-            // const utenants = await findAllTenants(realm);
-            // console.log(utenants.map(({contract, leaseId}) => ({contract, leaseId})));
+            await updateTenant(realm, {
+              ...dbTenant,
+              leaseId,
+              properties,
+            });
+          })
+        );
+        // const utenants = await findAllTenants(realm);
+        // console.log(utenants.map(({contract, leaseId}) => ({contract, leaseId})));
 
-            // updatedRealm.tenants = tenants
-            //     .filter(({ terminationDate }) => !terminationDate)
-            //     .reduce((acc, { name, contacts }) => ([
-            //         ...acc,
-            //         ...contacts.map(contact => ({ tenant: name, ...contact }))
-            //     ]), [])
-            //     .filter(({ email }) => !!email);
+        // updatedRealm.tenants = tenants
+        //     .filter(({ terminationDate }) => !terminationDate)
+        //     .reduce((acc, { name, contacts }) => ([
+        //         ...acc,
+        //         ...contacts.map(contact => ({ tenant: name, ...contact }))
+        //     ]), [])
+        //     .filter(({ email }) => !!email);
 
-            return updatedRealm;
-        }));
+        return updatedRealm;
+      })
+    );
 
-        await Promise.all(updatedRealms.map(updatedRealm => updateRealm(updatedRealm)));
-    } catch (error) {
-        console.error(error);
-    }
+    await Promise.all(
+      updatedRealms.map((updatedRealm) => updateRealm(updatedRealm))
+    );
+  } catch (error) {
+    console.error(error);
+  }
 };
diff --git a/scripts/mongodump.js b/scripts/mongodump.js
index 0ab4088..95e1fb1 100644
--- a/scripts/mongodump.js
+++ b/scripts/mongodump.js
@@ -3,6 +3,6 @@ const mongobackup = require('mongobackup');
 const config = require('../config');
 
 mongobackup.dump({
-    db: config.database,
-    out: path.join(__dirname, '..', 'bkp')
+  db: config.database,
+  out: path.join(__dirname, '..', 'bkp'),
 });
diff --git a/scripts/mongorestore.js b/scripts/mongorestore.js
index b4baa43..b8a5300 100644
--- a/scripts/mongorestore.js
+++ b/scripts/mongorestore.js
@@ -11,32 +11,32 @@ const bkpDirectory = path.join(__dirname, '..', 'bkp');
 const bkpFile = path.join(bkpDirectory, `${db_name}.dump`);
 
 module.exports = async () => {
-    await new Promise((resolve, reject) => {
-        try {
-            let cmd;
-            if (fs.existsSync(bkpFile)) {
-                cmd = mongobackup.restore({
-                    host : db_url.hostname,
-                    drop: true,
-                    gzip: true,
-                    archive: bkpFile
-                });
-            } else {
-                cmd = mongobackup.restore({
-                    db : db_name,
-                    host : db_url.hostname,
-                    drop: true,
-                    path: path.join(bkpDirectory, db_name)
-                });
-            }
-            cmd.on('close', code => {
-                resolve(code);
-            });
-            cmd.on('error', error => {
-                reject(error);
-            });
-        } catch (error) {
-            reject(error);
-        }
-    });
+  await new Promise((resolve, reject) => {
+    try {
+      let cmd;
+      if (fs.existsSync(bkpFile)) {
+        cmd = mongobackup.restore({
+          host: db_url.hostname,
+          drop: true,
+          gzip: true,
+          archive: bkpFile,
+        });
+      } else {
+        cmd = mongobackup.restore({
+          db: db_name,
+          host: db_url.hostname,
+          drop: true,
+          path: path.join(bkpDirectory, db_name),
+        });
+      }
+      cmd.on('close', (code) => {
+        resolve(code);
+      });
+      cmd.on('error', (error) => {
+        reject(error);
+      });
+    } catch (error) {
+      reject(error);
+    }
+  });
 };
diff --git a/server.js b/server.js
index 75fa463..c11949c 100644
--- a/server.js
+++ b/server.js
@@ -4,8 +4,8 @@ const logger = require('winston');
 // configure default logger
 logger.remove(logger.transports.Console);
 logger.add(logger.transports.Console, {
-    level: config.loggerLevel,
-    colorize: true
+  level: config.loggerLevel,
+  colorize: true,
 });
 
 const restoredb = require('./scripts/mongorestore');
@@ -35,158 +35,186 @@ const ejsHelpers = require('./backend/pages/ejshelpers');
 const dist_directory = path.join(__dirname, 'dist');
 
 // Init locale
-i18next.use(LanguageDetector)
-    .use(i18nFS)
-    .use(i18nSprintf)
-    .init({
-        debug: false,
-        fallbackLng: 'en',
-        pluralSeparator: '_',
-        keySeparator: '::',
-        nsSeparator: ':::',
-        detection: {
-            order: [ /*'path', 'session', 'querystring',*/ 'cookie', 'header'],
-            lookupCookie: 'locaI18next',
-            cookieDomain: 'loca',
-            caches: ['cookie']
-        },
-        backend: {
-            loadPath: path.join(dist_directory, 'locales', '{{lng}}.json')
-        }
-    });
+i18next
+  .use(LanguageDetector)
+  .use(i18nFS)
+  .use(i18nSprintf)
+  .init({
+    debug: false,
+    fallbackLng: 'en',
+    pluralSeparator: '_',
+    keySeparator: '::',
+    nsSeparator: ':::',
+    detection: {
+      order: [/*'path', 'session', 'querystring',*/ 'cookie', 'header'],
+      lookupCookie: 'locaI18next',
+      cookieDomain: 'loca',
+      caches: ['cookie'],
+    },
+    backend: {
+      loadPath: path.join(dist_directory, 'locales', '{{lng}}.json'),
+    },
+  });
 
 // Init express
 const app = express();
 app.set('trust proxy', true);
 app.use(cookieParser());
-app.use(bodyParser.urlencoded({
-    extended: true
-}));
+app.use(
+  bodyParser.urlencoded({
+    extended: true,
+  })
+);
 app.use(bodyParser.json());
 app.use(methodOverride());
-app.use(session({
+app.use(
+  session({
     secret: 'loca-secret',
     rolling: true,
     cookie: {
-        //      min  s     ms
-        maxAge: 10 * 60 * 1000 // 10 minutes
-    }
-}));
+      //      min  s     ms
+      maxAge: 10 * 60 * 1000, // 10 minutes
+    },
+  })
+);
 app.use(passport.initialize());
 app.use(passport.session());
 app.use(i18nMiddleware.handle(i18next));
 app.use((req, res, next) => {
-    app.locals.Intl = {
-        NumberFormat: new Intl.NumberFormat(req.language, { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 }),
-        NumberFormatPercent: new Intl.NumberFormat(req.language, { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 }),
-        NumberFormatCurrency: new Intl.NumberFormat(req.language, { style: 'currency', currency: req.t('__currency_code') })
-    };
-    const splitedLanguage = req.language.split('-');
-    moment.locale(splitedLanguage[0]);
-    next();
+  app.locals.Intl = {
+    NumberFormat: new Intl.NumberFormat(req.language, {
+      style: 'decimal',
+      minimumFractionDigits: 2,
+      maximumFractionDigits: 2,
+    }),
+    NumberFormatPercent: new Intl.NumberFormat(req.language, {
+      style: 'percent',
+      minimumFractionDigits: 2,
+      maximumFractionDigits: 2,
+    }),
+    NumberFormatCurrency: new Intl.NumberFormat(req.language, {
+      style: 'currency',
+      currency: req.t('__currency_code'),
+    }),
+  };
+  const splitedLanguage = req.language.split('-');
+  moment.locale(splitedLanguage[0]);
+  next();
 });
 // Icon / static files
-app.use(favicon(path.join(dist_directory, 'images', 'favicon.png'), {
-    maxAge: 2592000000
-}));
+app.use(
+  favicon(path.join(dist_directory, 'images', 'favicon.png'), {
+    maxAge: 2592000000,
+  })
+);
 app.use('/node_modules', express.static(path.join(__dirname, '/node_modules')));
 app.use('/public', express.static(dist_directory));
 app.use('/public/image', express.static(path.join(dist_directory, 'images')));
 app.use('/public/images', express.static(path.join(dist_directory, 'images')));
-app.use('/public/fonts', express.static(path.join(__dirname, '/node_modules/bootstrap/fonts')));
+app.use(
+  '/public/fonts',
+  express.static(path.join(__dirname, '/node_modules/bootstrap/fonts'))
+);
 app.use('/public/pdf', express.static(path.join(dist_directory, 'pdf')));
 app.use('/robots.txt', express.static(path.join(dist_directory, 'robots.txt')));
-app.use('/sitemap.xml', express.static(path.join(dist_directory, 'sitemap.xml')));
+app.use(
+  '/sitemap.xml',
+  express.static(path.join(dist_directory, 'sitemap.xml'))
+);
 
 app.set('views', path.join(__dirname, 'backend', 'pages'));
 app.set('view engine', 'ejs');
 app.engine('html', require('ejs').renderFile);
 
 // Express log through out winston
-app.use(expressWinston.logger({
+app.use(
+  expressWinston.logger({
     transports: [
-        new logger.transports.Console({
-            json: false,
-            colorize: true
-        })
+      new logger.transports.Console({
+        json: false,
+        colorize: true,
+      }),
     ],
     meta: false, // optional: control whether you want to log the meta data about the request (default to true)
     msg: String, //'HTTP {{req.method}} {{req.url}}', // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}"
     expressFormat: true, // Use the default Express/morgan request formatting, with the same colors. Enabling this will override any msg and colorStatus if true. Will only output colors on transports with colorize set to true
-    colorStatus: true // Color the status code, using the Express/morgan color palette (default green, 3XX cyan, 4XX yellow, 5XX red). Will not be recognized if expressFormat is true
+    colorStatus: true, // Color the status code, using the Express/morgan color palette (default green, 3XX cyan, 4XX yellow, 5XX red). Will not be recognized if expressFormat is true
     //ignoreRoute: function( /*req, res*/ ) {
     //    return false;
     //} // optional: allows to skip some log messages based on request and/or response
-}));
-app.use(expressWinston.errorLogger({
+  })
+);
+app.use(
+  expressWinston.errorLogger({
     transports: [
-        new logger.transports.Console({
-            json: false,
-            colorize: true
-        })
-    ]
-}));
+      new logger.transports.Console({
+        json: false,
+        colorize: true,
+      }),
+    ],
+  })
+);
 
 // Init routes
-routes.forEach(route => {
-    app.use(route());
+routes.forEach((route) => {
+  app.use(route());
 });
 
 // Start web app
 if (!config.productive) {
-    // Create new middleware to handle errors and respond with content negotiation.
-    // This middleware is only intended to be used in a development environment,
-    // as the full error stack traces will be sent back to the client when an error occurs.
-    app.use(errorHandler());
+  // Create new middleware to handle errors and respond with content negotiation.
+  // This middleware is only intended to be used in a development environment,
+  // as the full error stack traces will be sent back to the client when an error occurs.
+  app.use(errorHandler());
 }
 
 // init ejs helpers
 app.locals = {
-    ...app.locals,
-    ...ejsHelpers
+  ...app.locals,
+  ...ejsHelpers,
 };
 
 db.init()
-    .then(db.exists)
-    .then(async (isDbExists) => {
-        if (config.restoreDatabase) {
-            await restoredb();
-            logger.debug('database restored');
-        }
+  .then(db.exists)
+  .then(async (/*isDbExists*/) => {
+    if (config.restoreDatabase) {
+      await restoredb();
+      logger.debug('database restored');
+    }
 
-        // migrate db to the new models
-        await migratedb();
+    // migrate db to the new models
+    await migratedb();
 
-        const appDebugHttPort = 9091;
-        const http_port = config.productive ? config.appHttpPort : appDebugHttPort;
-        app.listen(http_port, () => {
-            logger.info('Listening port ' + http_port);
-            if (config.productive) {
-                logger.info('In production mode');
-            } else {
-                logger.info('In development mode (no minify/no uglify)');
-            }
-            if (config.demoMode) {
-                logger.info('In demo mode (login disabled)');
-            }
-            logger.debug('loaded configuration from', config.configdir);
-            logger.debug(config.log());
-            if (!config.productive) {
-                const browserSync = require('browser-sync');
-                browserSync.init({
-                    port: config.appHttpPort,
-                    proxy: `localhost:${appDebugHttPort}`,
-                    socket: {
-                        domain: `localhost:${config.nginxPort}`
-                    },
-                    files: ['dist'],
-                    open: false,
-                    ui: false
-                });
-            }
+    const appDebugHttPort = 9091;
+    const http_port = config.productive ? config.appHttpPort : appDebugHttPort;
+    app.listen(http_port, () => {
+      logger.info('Listening port ' + http_port);
+      if (config.productive) {
+        logger.info('In production mode');
+      } else {
+        logger.info('In development mode (no minify/no uglify)');
+      }
+      if (config.demoMode) {
+        logger.info('In demo mode (login disabled)');
+      }
+      logger.debug('loaded configuration from', config.configdir);
+      logger.debug(config.log());
+      if (!config.productive) {
+        const browserSync = require('browser-sync');
+        browserSync.init({
+          port: config.appHttpPort,
+          proxy: `localhost:${appDebugHttPort}`,
+          socket: {
+            domain: `localhost:${config.nginxPort}`,
+          },
+          files: ['dist'],
+          open: false,
+          ui: false,
         });
-    })
-    .catch((err) => {
-        logger.error(err);
-        process.exit(1);
-    });
\ No newline at end of file
+      }
+    });
+  })
+  .catch((err) => {
+    logger.error(err);
+    process.exit(1);
+  });
diff --git a/test/api.js b/test/api.js
index 6972b81..881190a 100644
--- a/test/api.js
+++ b/test/api.js
@@ -15,364 +15,433 @@ const notificationManager = require('../backend/managers/notificationmanager');
 const accountingManager = require('../backend/managers/accountingmanager');
 
 describe('api', () => {
-    // TODO: move these tests in auth.js
-    // describe('session', () => {
-    //     it('POST   /api/signup', (done) => {
-    //         config.signup = true;
-    //         loginManager.signup = () => {};
-    //         const mocked_signup = sinon.stub(loginManager, 'signup', (req, res) => {res.json({});});
-    //         requester(apiRouter(), {httpMethod: 'post', uri: '/api/signup'})
-    //         .expect(200)
-    //         .end((err) => {
-    //             if (err) {
-    //                 throw err;
-    //             }
-    //             assert(mocked_signup.calledOnce);
-    //             config.signup = false;
-    //             loginManager.signup.restore();
-    //             delete loginManager.signup;
-    //             done();
-    //         });
-    //     });
-    //     it('POST   /api/login', (done) => {
-    //         const mocked_login = sinon.stub(loginManager, 'login', (req, res) => {res.json({});});
-    //         requester(apiRouter(), {httpMethod: 'post', uri: '/api/login'})
-    //         .expect(200)
-    //         .end((err) => {
-    //             if (err) {
-    //                 throw err;
-    //             }
-    //             assert(mocked_login.calledOnce);
-    //             loginManager.login.restore();
-    //             done();
-    //         });
-    //     });
-    //     it('POST   /api/login (demo mode)', (done) => {
-    //         config.demoMode = true;
-    //         loginManager.loginDemo = () => {};
-    //         const mocked_loginDemo = sinon.stub(loginManager, 'loginDemo', (req, res) => {res.json({});});
-    //         requester(apiRouter(), {httpMethod: 'post', uri: '/api/login'})
-    //         .expect(200)
-    //         .end((err) => {
-    //             if (err) {
-    //                 throw err;
-    //             }
-    //             assert(mocked_loginDemo.calledOnce);
-    //             config.demoMode = false;
-    //             loginManager.loginDemo.restore();
-    //             delete loginManager.loginDemo;
-    //             done();
-    //         });
-    //     });
-    //     it('GET    /api/logout', (done) => {
-    //         const mocked_logout = sinon.stub(loginManager, 'logout', (req, res) => {res.json({});});
-    //         requester(apiRouter(), {httpMethod: 'get', uri: '/api/logout'})
-    //         .expect(200)
-    //         .end((err) => {
-    //             if (err) {
-    //                 throw err;
-    //             }
-    //             assert(mocked_logout.calledOnce);
-    //             loginManager.logout.restore();
-    //             done();
-    //         });
-    //     });
-    // });
+  // TODO: move these tests in auth.js
+  // describe('session', () => {
+  //     it('POST   /api/signup', (done) => {
+  //         config.signup = true;
+  //         loginManager.signup = () => {};
+  //         const mocked_signup = sinon.stub(loginManager, 'signup', (req, res) => {res.json({});});
+  //         requester(apiRouter(), {httpMethod: 'post', uri: '/api/signup'})
+  //         .expect(200)
+  //         .end((err) => {
+  //             if (err) {
+  //                 throw err;
+  //             }
+  //             assert(mocked_signup.calledOnce);
+  //             config.signup = false;
+  //             loginManager.signup.restore();
+  //             delete loginManager.signup;
+  //             done();
+  //         });
+  //     });
+  //     it('POST   /api/login', (done) => {
+  //         const mocked_login = sinon.stub(loginManager, 'login', (req, res) => {res.json({});});
+  //         requester(apiRouter(), {httpMethod: 'post', uri: '/api/login'})
+  //         .expect(200)
+  //         .end((err) => {
+  //             if (err) {
+  //                 throw err;
+  //             }
+  //             assert(mocked_login.calledOnce);
+  //             loginManager.login.restore();
+  //             done();
+  //         });
+  //     });
+  //     it('POST   /api/login (demo mode)', (done) => {
+  //         config.demoMode = true;
+  //         loginManager.loginDemo = () => {};
+  //         const mocked_loginDemo = sinon.stub(loginManager, 'loginDemo', (req, res) => {res.json({});});
+  //         requester(apiRouter(), {httpMethod: 'post', uri: '/api/login'})
+  //         .expect(200)
+  //         .end((err) => {
+  //             if (err) {
+  //                 throw err;
+  //             }
+  //             assert(mocked_loginDemo.calledOnce);
+  //             config.demoMode = false;
+  //             loginManager.loginDemo.restore();
+  //             delete loginManager.loginDemo;
+  //             done();
+  //         });
+  //     });
+  //     it('GET    /api/logout', (done) => {
+  //         const mocked_logout = sinon.stub(loginManager, 'logout', (req, res) => {res.json({});});
+  //         requester(apiRouter(), {httpMethod: 'get', uri: '/api/logout'})
+  //         .expect(200)
+  //         .end((err) => {
+  //             if (err) {
+  //                 throw err;
+  //             }
+  //             assert(mocked_logout.calledOnce);
+  //             loginManager.logout.restore();
+  //             done();
+  //         });
+  //     });
+  // });
 
-    describe('documents', () => {
-        it('PATCH  /api/documents/:id', (done) => {
-            const mocked_update = sinon.stub(documentManager, 'update').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'patch', uri: '/api/documents/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_update.calledOnce);
-                    documentManager.update.restore();
-                    done();
-                });
+  describe('documents', () => {
+    it('PATCH  /api/documents/:id', (done) => {
+      const mocked_update = sinon
+        .stub(documentManager, 'update')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'patch',
+        uri: '/api/documents/1234',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_update.calledOnce);
+          documentManager.update.restore();
+          done();
         });
     });
+  });
 
-    describe('notifications', () => {
-        it('GET    /api/notifications', (done) => {
-            const mocked_all = sinon.stub(notificationManager, 'all').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/notifications' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_all.calledOnce);
-                    notificationManager.all.restore();
-                    done();
-                });
+  describe('notifications', () => {
+    it('GET    /api/notifications', (done) => {
+      const mocked_all = sinon
+        .stub(notificationManager, 'all')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/notifications' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_all.calledOnce);
+          notificationManager.all.restore();
+          done();
         });
     });
+  });
 
-    describe('realms', () => {
-        it('GET    /api/realms/:id', (done) => {
-            const mocked_selectRealm = sinon.stub(loginManager, 'selectRealm').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/realms/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_selectRealm.calledOnce);
-                    loginManager.selectRealm.restore();
-                    done();
-                });
+  describe('realms', () => {
+    it('GET    /api/realms/:id', (done) => {
+      const mocked_selectRealm = sinon
+        .stub(loginManager, 'selectRealm')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/realms/1234' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_selectRealm.calledOnce);
+          loginManager.selectRealm.restore();
+          done();
         });
     });
+  });
 
-    describe('occupants', () => {
-        it('POST   /api/occupants', (done) => {
-            const mocked_add = sinon.stub(occupantManager, 'add').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'post', uri: '/api/occupants' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_add.calledOnce);
-                    occupantManager.add.restore();
-                    done();
-                });
+  describe('occupants', () => {
+    it('POST   /api/occupants', (done) => {
+      const mocked_add = sinon
+        .stub(occupantManager, 'add')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'post', uri: '/api/occupants' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_add.calledOnce);
+          occupantManager.add.restore();
+          done();
         });
-        it('PATCH  /api/occupants/:id', (done) => {
-            const mocked_update = sinon.stub(occupantManager, 'update').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'patch', uri: '/api/occupants/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_update.calledOnce);
-                    occupantManager.update.restore();
-                    done();
-                });
+    });
+    it('PATCH  /api/occupants/:id', (done) => {
+      const mocked_update = sinon
+        .stub(occupantManager, 'update')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'patch',
+        uri: '/api/occupants/1234',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_update.calledOnce);
+          occupantManager.update.restore();
+          done();
         });
-        it('DELETE /api/occupants/:ids', (done) => {
-            const mocked_remove = sinon.stub(occupantManager, 'remove').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'delete', uri: '/api/occupants/1,2,3' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_remove.calledOnce);
-                    occupantManager.remove.restore();
-                    done();
-                });
+    });
+    it('DELETE /api/occupants/:ids', (done) => {
+      const mocked_remove = sinon
+        .stub(occupantManager, 'remove')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'delete',
+        uri: '/api/occupants/1,2,3',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_remove.calledOnce);
+          occupantManager.remove.restore();
+          done();
         });
-        it('GET    /api/occupants', (done) => {
-            const mocked_all = sinon.stub(occupantManager, 'all').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/occupants' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_all.calledOnce);
-                    occupantManager.all.restore();
-                    done();
-                });
+    });
+    it('GET    /api/occupants', (done) => {
+      const mocked_all = sinon
+        .stub(occupantManager, 'all')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/occupants' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_all.calledOnce);
+          occupantManager.all.restore();
+          done();
         });
-        it('GET    /api/occupants/overview', (done) => {
-            const mocked_overview = sinon.stub(occupantManager, 'overview').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/occupants/overview' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_overview.calledOnce);
-                    occupantManager.overview.restore();
-                    done();
-                });
+    });
+    it('GET    /api/occupants/overview', (done) => {
+      const mocked_overview = sinon
+        .stub(occupantManager, 'overview')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'get',
+        uri: '/api/occupants/overview',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_overview.calledOnce);
+          occupantManager.overview.restore();
+          done();
         });
     });
+  });
 
-    describe('rents', () => {
-        it('PATCH  /api/rents/:id', (done) => {
-            const mocked_update = sinon.stub(rentManager, 'update').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'patch', uri: '/api/rents/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_update.calledOnce);
-                    mocked_update.restore();
-                    done();
-                });
+  describe('rents', () => {
+    it('PATCH  /api/rents/:id', (done) => {
+      const mocked_update = sinon
+        .stub(rentManager, 'update')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'patch', uri: '/api/rents/1234' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_update.calledOnce);
+          mocked_update.restore();
+          done();
         });
-        it('GET    /api/rents/occupant/:id', (done) => {
-            const mocked_rentsOfOccupant = sinon.stub(rentManager, 'rentsOfOccupant').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/rents/occupant/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_rentsOfOccupant.calledOnce);
-                    mocked_rentsOfOccupant.restore();
-                    done();
-                });
+    });
+    it('GET    /api/rents/occupant/:id', (done) => {
+      const mocked_rentsOfOccupant = sinon
+        .stub(rentManager, 'rentsOfOccupant')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'get',
+        uri: '/api/rents/occupant/1234',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_rentsOfOccupant.calledOnce);
+          mocked_rentsOfOccupant.restore();
+          done();
         });
-        it('GET    /api/rents/:year/:month', (done) => {
-            const mocked_all = sinon.stub(rentManager, 'all').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/rents/2017/02' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_all.calledOnce);
-                    mocked_all.restore();
-                    done();
-                });
+    });
+    it('GET    /api/rents/:year/:month', (done) => {
+      const mocked_all = sinon
+        .stub(rentManager, 'all')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/rents/2017/02' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_all.calledOnce);
+          mocked_all.restore();
+          done();
         });
-        it('GET    /api/rents/overview', (done) => {
-            const mocked_overview = sinon.stub(rentManager, 'overview').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/rents/overview' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_overview.calledOnce);
-                    mocked_overview.restore();
-                    done();
-                });
+    });
+    it('GET    /api/rents/overview', (done) => {
+      const mocked_overview = sinon
+        .stub(rentManager, 'overview')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/rents/overview' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_overview.calledOnce);
+          mocked_overview.restore();
+          done();
         });
-        it('GET    /api/rents/overview/:year/:month', (done) => {
-            const mocked_overview = sinon.stub(rentManager, 'overview').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/rents/overview/2017/02' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_overview.calledOnce);
-                    mocked_overview.restore();
-                    done();
-                });
+    });
+    it('GET    /api/rents/overview/:year/:month', (done) => {
+      const mocked_overview = sinon
+        .stub(rentManager, 'overview')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'get',
+        uri: '/api/rents/overview/2017/02',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_overview.calledOnce);
+          mocked_overview.restore();
+          done();
         });
     });
+  });
 
-    describe('properties', () => {
-        it('POST   /api/properties', (done) => {
-            const mocked_add = sinon.stub(propertyManager, 'add').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'post', uri: '/api/properties' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_add.calledOnce);
-                    mocked_add.restore();
-                    done();
-                });
+  describe('properties', () => {
+    it('POST   /api/properties', (done) => {
+      const mocked_add = sinon
+        .stub(propertyManager, 'add')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'post', uri: '/api/properties' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_add.calledOnce);
+          mocked_add.restore();
+          done();
         });
-        it('PATCH  /api/properties/:id', (done) => {
-            const mocked_update = sinon.stub(propertyManager, 'update').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'patch', uri: '/api/properties/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_update.calledOnce);
-                    mocked_update.restore();
-                    done();
-                });
+    });
+    it('PATCH  /api/properties/:id', (done) => {
+      const mocked_update = sinon
+        .stub(propertyManager, 'update')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'patch',
+        uri: '/api/properties/1234',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_update.calledOnce);
+          mocked_update.restore();
+          done();
         });
-        it('DELETE /api/properties/:ids', (done) => {
-            const mocked_remove = sinon.stub(propertyManager, 'remove').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'delete', uri: '/api/properties/1,2,3,4' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_remove.calledOnce);
-                    mocked_remove.restore();
-                    done();
-                });
+    });
+    it('DELETE /api/properties/:ids', (done) => {
+      const mocked_remove = sinon
+        .stub(propertyManager, 'remove')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'delete',
+        uri: '/api/properties/1,2,3,4',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_remove.calledOnce);
+          mocked_remove.restore();
+          done();
         });
-        it('GET    /api/properties', (done) => {
-            const mocked_all = sinon.stub(propertyManager, 'all').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/properties' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_all.calledOnce);
-                    mocked_all.restore();
-                    done();
-                });
+    });
+    it('GET    /api/properties', (done) => {
+      const mocked_all = sinon
+        .stub(propertyManager, 'all')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/properties' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_all.calledOnce);
+          mocked_all.restore();
+          done();
         });
-        it('GET    /api/properties/overview', (done) => {
-            const mocked_overview = sinon.stub(propertyManager, 'overview').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/properties/overview' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_overview.calledOnce);
-                    mocked_overview.restore();
-                    done();
-                });
+    });
+    it('GET    /api/properties/overview', (done) => {
+      const mocked_overview = sinon
+        .stub(propertyManager, 'overview')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), {
+        httpMethod: 'get',
+        uri: '/api/properties/overview',
+      })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_overview.calledOnce);
+          mocked_overview.restore();
+          done();
         });
     });
+  });
 
-    describe('owner', () => {
-        it('PATCH  /api/owner/:id', (done) => {
-            const mocked_update = sinon.stub(ownerManager, 'update').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'patch', uri: '/api/owner/1234' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_update.calledOnce);
-                    mocked_update.restore();
-                    done();
-                });
+  describe('owner', () => {
+    it('PATCH  /api/owner/:id', (done) => {
+      const mocked_update = sinon
+        .stub(ownerManager, 'update')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'patch', uri: '/api/owner/1234' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_update.calledOnce);
+          mocked_update.restore();
+          done();
         });
-        it('GET    /api/owner', (done) => {
-            const mocked_all = sinon.stub(ownerManager, 'all').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/owner' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_all.calledOnce);
-                    mocked_all.restore();
-                    done();
-                });
+    });
+    it('GET    /api/owner', (done) => {
+      const mocked_all = sinon
+        .stub(ownerManager, 'all')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/owner' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_all.calledOnce);
+          mocked_all.restore();
+          done();
         });
     });
+  });
 
-    describe('accounting', () => {
-        it('GET    /api/accounting/:year', (done) => {
-            const mocked_all = sinon.stub(accountingManager, 'all').callsFake((req, res) => res.json({}));
-            requester(apiRouter(), { httpMethod: 'get', uri: '/api/accounting/2017' })
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    assert(mocked_all.calledOnce);
-                    mocked_all.restore();
-                    done();
-                });
+  describe('accounting', () => {
+    it('GET    /api/accounting/:year', (done) => {
+      const mocked_all = sinon
+        .stub(accountingManager, 'all')
+        .callsFake((req, res) => res.json({}));
+      requester(apiRouter(), { httpMethod: 'get', uri: '/api/accounting/2017' })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          assert(mocked_all.calledOnce);
+          mocked_all.restore();
+          done();
         });
     });
+  });
 });
diff --git a/test/businesslogic_FR/computeRent.js b/test/businesslogic_FR/computeRent.js
index bcc97f0..e897768 100755
--- a/test/businesslogic_FR/computeRent.js
+++ b/test/businesslogic_FR/computeRent.js
@@ -5,98 +5,120 @@ const moment = require('moment');
 const BL = require('../../backend/businesslogic');
 
 describe('business logic rent computation', () => {
-    describe('one property rented', () => {
-        const property = {
-            entryDate: '01/01/2017',
-            exitDate: '31/08/2017',
-            property: {
-                name: 'mon bureau',
-                price: 300,
-            },
-            rent: 300,
-            expenses: [{title: 'expense', amount: 10}]
-        };
-        const contract = {
-            begin: '01/01/2017',
-            end: '31/01/2017',
-            discount: 10,
-            vatRate: 0.2,
-            properties: [property]
-        };
+  describe('one property rented', () => {
+    const property = {
+      entryDate: '01/01/2017',
+      exitDate: '31/08/2017',
+      property: {
+        name: 'mon bureau',
+        price: 300,
+      },
+      rent: 300,
+      expenses: [{ title: 'expense', amount: 10 }],
+    };
+    const contract = {
+      begin: '01/01/2017',
+      end: '31/01/2017',
+      discount: 10,
+      vatRate: 0.2,
+      properties: [property],
+    };
 
-        const grandTotal = math.round((property.rent + property.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0) - contract.discount) * (1+contract.vatRate), 2);
+    const grandTotal = math.round(
+      (property.rent +
+        property.expenses.reduce((acc, { amount }) => {
+          acc += amount;
+          return acc;
+        }, 0) -
+        contract.discount) *
+        (1 + contract.vatRate),
+      2
+    );
 
-        it('check rent object structure', () => {
-            const computedRent = BL.computeRent(contract, '01/01/2017');
-            const rentMoment = moment('01/01/2017', 'DD/MM/YYYY HH:mm');
-            assert.strictEqual(computedRent.term, Number(rentMoment.format('YYYYMMDDHH')));
-            assert.strictEqual(computedRent.month, rentMoment.month()+1);
-            assert.strictEqual(computedRent.year, rentMoment.year());
-        });
-
-        it('compute one rent', () => {
-            const computedRent = BL.computeRent(contract, '01/01/2017');
-            assert.strictEqual(computedRent.total.grandTotal, grandTotal);
-        });
+    it('check rent object structure', () => {
+      const computedRent = BL.computeRent(contract, '01/01/2017');
+      const rentMoment = moment('01/01/2017', 'DD/MM/YYYY HH:mm');
+      assert.strictEqual(
+        computedRent.term,
+        Number(rentMoment.format('YYYYMMDDHH'))
+      );
+      assert.strictEqual(computedRent.month, rentMoment.month() + 1);
+      assert.strictEqual(computedRent.year, rentMoment.year());
+    });
 
-        it('compute two rents and check balance', () => {
-            const rentOne = BL.computeRent(contract, '01/01/2017');
-            const rentTwo = BL.computeRent(contract, '01/01/2017', rentOne);
-            assert.strictEqual(rentOne.total.grandTotal, grandTotal);
-            assert.strictEqual(rentTwo.total.balance, grandTotal);
-            assert.strictEqual(rentTwo.total.grandTotal, grandTotal * 2);
-        });
+    it('compute one rent', () => {
+      const computedRent = BL.computeRent(contract, '01/01/2017');
+      assert.strictEqual(computedRent.total.grandTotal, grandTotal);
     });
 
-    describe('two properties rented with one month offset', () => {
-        const property1 = {
-            entryDate: '01/01/2017',
-            exitDate: '31/08/2017',
-            property: {
-                name: 'mon bureau',
-                price: 300,
-            },
-            rent: 300,
-            expenses: [{title: 'expense', amount: 10}]
-        };
-        const property2 = {
-            entryDate: '01/02/2017',
-            exitDate: '31/08/2017',
-            property: {
-                name: 'mon parking',
-                price: 30
-            },
-            rent: 30,
-            expenses: [{title: 'expense', amount: 5}]
-        };
-        const contract = {
-            begin: '01/01/2017',
-            end: '31/01/2017',
-            discount: 10,
-            vatRate: 0.2,
-            properties: [property1, property2]
-        };
-        const grandTotal1 = math.round((property1.property.price + property1.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0) - contract.discount) * (1+contract.vatRate), 2);
-        const grandTotal2 = math.round((property2.property.price + property2.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0)) * (1+contract.vatRate), 2);
+    it('compute two rents and check balance', () => {
+      const rentOne = BL.computeRent(contract, '01/01/2017');
+      const rentTwo = BL.computeRent(contract, '01/01/2017', rentOne);
+      assert.strictEqual(rentOne.total.grandTotal, grandTotal);
+      assert.strictEqual(rentTwo.total.balance, grandTotal);
+      assert.strictEqual(rentTwo.total.grandTotal, grandTotal * 2);
+    });
+  });
 
-        it('compute one rent one property should be billed', () => {
-            const computedRent = BL.computeRent(contract, '01/01/2017');
-            assert.strictEqual(computedRent.total.grandTotal, grandTotal1);
-        });
+  describe('two properties rented with one month offset', () => {
+    const property1 = {
+      entryDate: '01/01/2017',
+      exitDate: '31/08/2017',
+      property: {
+        name: 'mon bureau',
+        price: 300,
+      },
+      rent: 300,
+      expenses: [{ title: 'expense', amount: 10 }],
+    };
+    const property2 = {
+      entryDate: '01/02/2017',
+      exitDate: '31/08/2017',
+      property: {
+        name: 'mon parking',
+        price: 30,
+      },
+      rent: 30,
+      expenses: [{ title: 'expense', amount: 5 }],
+    };
+    const contract = {
+      begin: '01/01/2017',
+      end: '31/01/2017',
+      discount: 10,
+      vatRate: 0.2,
+      properties: [property1, property2],
+    };
+    const grandTotal1 = math.round(
+      (property1.property.price +
+        property1.expenses.reduce((acc, { amount }) => {
+          acc += amount;
+          return acc;
+        }, 0) -
+        contract.discount) *
+        (1 + contract.vatRate),
+      2
+    );
+    const grandTotal2 = math.round(
+      (property2.property.price +
+        property2.expenses.reduce((acc, { amount }) => {
+          acc += amount;
+          return acc;
+        }, 0)) *
+        (1 + contract.vatRate),
+      2
+    );
 
-        it('compute one rent two properties should be billed', () => {
-            const computedRent = BL.computeRent(contract, '01/02/2017');
-            assert.strictEqual(computedRent.total.grandTotal, grandTotal1 + grandTotal2);
-        });
+    it('compute one rent one property should be billed', () => {
+      const computedRent = BL.computeRent(contract, '01/01/2017');
+      assert.strictEqual(computedRent.total.grandTotal, grandTotal1);
+    });
 
+    it('compute one rent two properties should be billed', () => {
+      const computedRent = BL.computeRent(contract, '01/02/2017');
+      assert.strictEqual(
+        computedRent.total.grandTotal,
+        grandTotal1 + grandTotal2
+      );
     });
+  });
 });
diff --git a/test/managers/contract.js b/test/managers/contract.js
index 7cdab4a..0f1b7b2 100644
--- a/test/managers/contract.js
+++ b/test/managers/contract.js
@@ -3,401 +3,692 @@ const assert = require('assert');
 const Contract = require('../../backend/managers/contract');
 
 describe('contract functionalities', () => {
-    it('create contract', () => {
-        const contract = Contract.create({
-            begin: '01/01/2017 00:00',
-            end: '31/12/2025 23:59',
-            frequency: 'months',
-            properties: [{}, {}]
-        });
-
-        assert.strictEqual(contract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(contract.rents.length, 108, 'incorrect number of rents');
-
-        assert.throws(() => {
-            Contract.create({
-                begin: '01/01/2017 00:00',
-                end: '01/01/2017 03:00',
-                frequency: 'hours'
-            });
-        });
-
-        assert.throws(() => {
-            Contract.create({
-                begin: '01/01/2017 00:00',
-                end: '01/01/2016 03:00',
-                frequency: 'hours',
-                properties: [{}, {}]
-            });
-        });
-
-        assert.throws(() => {
-            Contract.create();
-        });
-    });
-
-    it('check term frequency', () => {
-        let c1;
-        let c2;
-        let c3;
-        let c4;
-        let c5;
-
-        assert.doesNotThrow(() => {
-            c1 = Contract.create({ begin: '01/01/2017 00:00', end: '01/01/2017 03:00', frequency: 'hours', properties: [{}, {}]});
-            c2 = Contract.create({ begin: '01/01/2017 00:00', end: '31/01/2017 23:59', frequency: 'days', properties: [{}, {}]});
-            c3 = Contract.create({ begin: '01/01/2017 00:00', end: '14/01/2017 23:59', frequency: 'weeks', properties: [{}, {}]});
-            c4 = Contract.create({ begin: '01/01/2017 00:00', end: '31/12/2017 23:59', frequency: 'months', properties: [{}, {}]});
-            c5 = Contract.create({ begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'years', properties: [{}, {}]});
-        });
-
-        assert.strictEqual(c1.terms, 3, 'incorrect number of terms');
-        assert.strictEqual(c2.terms, 31, 'incorrect number of terms');
-        assert.strictEqual(c3.terms, 2, 'incorrect number of terms');
-        assert.strictEqual(c4.terms, 12, 'incorrect number of terms');
-        assert.strictEqual(c5.terms, 9, 'incorrect number of terms');
-
-        assert.throws(() => {
-            Contract.create({ begin: '01/01/2017 00:00', end: '01/01/2017 03:00', frequency: 'blabla', properties: [{}, {}]});
-        });
-    });
-
-    it('renew contract based on initial number of terms', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        const newContract = Contract.renew(contract);
-
-        assert.strictEqual(newContract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(newContract.rents.length, 108*2, 'incorrect number of rents');
-    });
-
-    it('update contract change duration', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        const newContract = Contract.update(contract, {end: '31/03/2026 23:59'});
-
-        assert.strictEqual(newContract.terms, 108 + 3, 'incorrect number of terms');
-        assert.strictEqual(newContract.rents.length, 108 + 3, 'incorrect number of rents');
-
-        const newContract2 = Contract.update(contract, {begin: '01/01/2018 00:00'});
-        assert.strictEqual(newContract2.terms, 108 - 12, 'incorrect number of terms');
-        assert.strictEqual(newContract2.rents.length, 108 - 12, 'incorrect number of rents');
-    });
-
-    it('terminate contract', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        const newContract = Contract.terminate(contract, '31/12/2017 23:59');
-
-        assert.strictEqual(newContract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(newContract.rents.length, 12, 'incorrect number of rents');
-        assert.strictEqual(newContract.begin, '01/01/2017 00:00', 'begin contract date incorrect');
-        assert.strictEqual(newContract.end, '31/12/2025 23:59', 'end contract date incorrect');
-        assert.strictEqual(newContract.termination, '31/12/2017 23:59', 'termination contract date incorrect');
-
-        // after end date of contract
-        assert.throws(() => {
-            Contract.terminate(contract, '31/12/2026 23:59');
-        });
-
-        // before begin date of contract
-        assert.throws(() => {
-            Contract.terminate(contract, '01/01/2016 00:00');
-        });
-    });
-
-    it('update termination date', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        const tmpContract = Contract.terminate(contract, '31/12/2017 23:59');
-        const longerContract = Contract.terminate(tmpContract, '31/12/2018 23:59');
-
-        assert.strictEqual(longerContract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(longerContract.rents.length, 24, 'incorrect number of rents');
-        assert.strictEqual(longerContract.begin, '01/01/2017 00:00', 'begin contract date incorrect');
-        assert.strictEqual(longerContract.end, '31/12/2025 23:59', 'end contract date incorrect');
-        assert.strictEqual(longerContract.termination, '31/12/2018 23:59', 'termination contract date incorrect');
-
-        const shorterContract = Contract.terminate(longerContract, '30/06/2017 23:59');
-        assert.strictEqual(shorterContract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(shorterContract.rents.length, 6, 'incorrect number of rents');
-        assert.strictEqual(shorterContract.begin, '01/01/2017 00:00', 'begin contract date incorrect');
-        assert.strictEqual(shorterContract.end, '31/12/2025 23:59', 'end contract date incorrect');
-        assert.strictEqual(shorterContract.termination, '30/06/2017 23:59', 'termination contract date incorrect');
+  it('create contract', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
     });
 
-    it('terminate contract and change contract duration', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        const terminateContract = Contract.terminate(contract, '31/12/2017 23:59');
-        const newContract = Contract.update(terminateContract, {end: '31/03/2026 23:59'});
+    assert.strictEqual(contract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(contract.rents.length, 108, 'incorrect number of rents');
 
-        assert.strictEqual(newContract.terms, 108 + 3, 'incorrect number of terms');
-        assert.strictEqual(newContract.rents.length, 12, 'incorrect number of rents');
-        assert.strictEqual(newContract.begin, '01/01/2017 00:00', 'begin contract date incorrect');
-        assert.strictEqual(newContract.end, '31/03/2026 23:59', 'end contract date incorrect');
-        assert.strictEqual(newContract.termination, '31/12/2017 23:59', 'termination contract date incorrect');
-
-        // termination date after end contract
-        assert.throws(() => {
-            Contract.update(terminateContract, {end: '30/12/2017 23:59'});
-        });
-
-        // termination date before begin contract
-        assert.throws(() => {
-            Contract.update(terminateContract, {begin: '01/01/2018 23:59'});
-        });
-    });
-
-    it('pay a term', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        Contract.payTerm(contract, '01/12/2025 00:00', {payments:[{amount: 200}], discounts:['discount']});
-
-        assert.strictEqual(contract.rents.filter(rent => rent.payments.length === 0).length, contract.terms - 1);
-        assert.strictEqual(contract.rents.find(rent => rent.term === 2025120100).payments[0].amount, 200);
-        assert.strictEqual(contract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(contract.rents.length, 108, 'incorrect number of rents');
-
-        assert.throws(() => {
-            Contract.payTerm(contract, '01/12/2026 00:00', {payments:[{amount: 200}], discounts:['discount']});
-        });
-
-        assert.throws(() => {
-            Contract.payTerm(contract, '01/12/2016 00:00', {payments:[{amount: 200}], discounts:['discount']});
-        });
-    });
-
-    it('pay first term', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        Contract.payTerm(contract, '01/01/2017 00:00', {payments:[{amount: 200}], discounts:['discount']});
-
-        assert.strictEqual(contract.rents.find(rent => rent.term === 2017010100).payments[0].amount, 200);
-        assert.strictEqual(contract.rents.findIndex(rent => rent.term === 2017010100), 0);
-    });
-
-    it('pay last term', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        Contract.payTerm(contract, '01/12/2025 00:00', {payments:[{amount: 200}], discounts:['discount']});
-
-        assert.strictEqual(contract.rents.find(rent => rent.term === 2025120100).payments[0].amount, 200);
-        assert.strictEqual(contract.rents.findIndex(rent => rent.term === 2025120100), contract.rents.length - 1);
-    });
-
-    it('pay a term in reverse chronological order', () => {
-        const contract = Contract.create({
-            begin: '01/01/2020 00:00',
-            end: '31/12/2020 23:59',
-            frequency: 'months',
-            vatRate: 0.2,
-            properties: [{
-                entryDate: '01/01/2020',
-                exitDate: '31/12/2020',
-                property: {
-                    name: 'office1',
-                    price: 100,
-                },
-                rent: 100,
-                expenses: [{ title: 'expense', amount: 10 }]
-            }]
-        });
-
-        let termAmount = (100 + 10) * 1.2; // VAT
-        assert.strictEqual(contract.rents.find(rent => rent.term === 2020020100).total.balance, termAmount);
-
-
-        Contract.payTerm(contract, '01/05/2020 00:00', {payments:[{amount: termAmount}]});
-        Contract.payTerm(contract, '01/04/2020 00:00', {payments:[{amount: termAmount}]});
-        Contract.payTerm(contract, '01/03/2020 00:00', {payments:[{amount: termAmount}]});
-        Contract.payTerm(contract, '01/02/2020 00:00', {payments:[{amount: termAmount}]});
-        Contract.payTerm(contract, '01/01/2020 00:00', {payments:[{amount: termAmount}]});
-
-        assert.strictEqual(contract.rents.find(rent => rent.term === 2020060100).total.balance, 0);
-    });
-
-    it('pay a term and update contract duration', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        Contract.payTerm(contract, '01/12/2025 00:00', {payments:[{amount: 200}], discounts:['discount']});
-
-        const newContract = Contract.update(contract, {begin:'01/01/2019 00:00', end: '31/12/2025 23:59'});
-        assert.strictEqual(newContract.rents.find(rent => rent.term === 2025120100).payments[0].amount, 200);
-
-        // payment out of contract time frame
-        assert.throws(() => {
-            Contract.update(contract, {begin:'01/01/2019 00:00', end: '31/12/2024 23:59'});
-        });
-    });
-
-    it('pay terms and update contract properties', () => {
-        const p1 = {
-            entryDate: '01/01/2020',
-            exitDate: '31/12/2020',
-            property: {
-                name: 'office1',
-                price: 300
-            },
-            rent: 300,
-            expenses: [{ title: 'expense', amount: 10 }]
-        };
-
-        let termP1 = p1.rent + p1.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0);
-        termP1 *= 1.2; // VAT
-
-        const contract = Contract.create({
-            begin: '01/01/2020 00:00',
-            end: '31/12/2020 23:59',
-            frequency: 'months',
-            vatRate: 0.2,
-            properties: [p1]
-        });
-        Contract.payTerm(contract, '01/01/2020 00:00', {payments:[{amount: 372}]});
-        Contract.payTerm(contract, '01/02/2020 00:00', {payments:[{amount: 372}]});
-        Contract.payTerm(contract, '01/03/2020 00:00', {payments:[{amount: 372}]});
-        Contract.payTerm(contract, '01/04/2020 00:00', {payments:[{amount: 372}]});
-        Contract.payTerm(contract, '01/05/2020 00:00', {payments:[{amount: 372}]});
-        Contract.payTerm(contract, '01/06/2020 00:00', {payments:[{amount: 372}]});
-
-        let termsGrandTotal = Array(contract.terms).fill(1).reduce((acc, value, index) => {
-            const prevTerm = index > 6 ? acc[index - 1] : 0;
-            const currentTerm = prevTerm + termP1;
-            acc.push(currentTerm);
-            return acc;
-        }, []);
-
-        assert.deepStrictEqual(contract.rents.map(r => r.total.grandTotal), termsGrandTotal);
-
-        // update contract with a new properties
-        const p2 = {
-            entryDate: '01/02/2020',
-            exitDate: '31/12/2020',
-            property: {
-                name: 'office',
-                price: 320,
-            },
-            rent: 320,
-            expenses: [{ title: 'expense', amount: 32 }]
-        };
-
-        let termP2 = p2.rent + p2.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0);
-        termP2 *= 1.2; // VAT
-
-        const newContract = Contract.update(contract, {properties: [p1, p2]});
-
-        termsGrandTotal = [
-            Math.round(termP1 * 10) / 10,          // jan
-            Math.round((termP1 + termP2) * 10) / 10,      // feb
-            Math.round((termP1 + 2 * termP2) * 10) / 10,      // mar
-            Math.round((termP1 + 3 * termP2) * 10) / 10,      // apr
-            Math.round((termP1 + 4 * termP2) * 10) / 10,      // may
-            Math.round((termP1 + 5 * termP2) * 10) / 10,      // jun
-            Math.round((termP1 + 6 * termP2) * 10) / 10,      // jul
-            Math.round((2 * termP1 + 7 * termP2) * 10) / 10,  // aou
-            Math.round((3 * termP1 + 8 * termP2) * 10) / 10,  // sep
-            Math.round((4 * termP1 + 9 * termP2) * 10) / 10, // oct
-            Math.round((5 * termP1 + 10 * termP2) * 10) / 10, // nov
-            Math.round((6 * termP1 + 11 * termP2) *10) / 10   // dec
-        ];
-
-        assert.deepStrictEqual(newContract.rents.map(r => r.total.grandTotal), termsGrandTotal);
-    });
-
-    it('pay a term and renew', () => {
-        const contract = Contract.create({begin: '01/01/2017 00:00', end: '31/12/2025 23:59', frequency: 'months', properties: [{}, {}]});
-        Contract.payTerm(contract, '01/12/2025 00:00', {payments:[{amount: 200}], discounts:['discount']});
-        const newContract = Contract.renew(contract);
-
-        assert.strictEqual(newContract.rents.filter(rent => rent.payments.length === 0).length, (contract.terms * 2) - 1);
-        assert.strictEqual(newContract.rents.find(rent => rent.term === 2025120100).payments[0].amount, 200);
-        assert.strictEqual(newContract.terms, 108, 'incorrect number of terms');
-        assert.strictEqual(newContract.rents.length, 108 * 2, 'incorrect number of rents');
-    });
-
-    it('compute terms', () => {
-        const property = {
-            entryDate: '01/01/2020',
-            exitDate: '31/08/2020',
-            property: {
-                name: 'mon bureau',
-                price: 300
-            },
-            rent: 300,
-            expenses: [{ title: 'expense', amount: 10 }]
-        };
-
-        let rentAmountProperty1 = property.rent + property.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0);
-        rentAmountProperty1 *= 1.2; // VAT
-
-        const contract = Contract.create({
-            begin: '01/01/2020 00:00',
-            end: '31/12/2020 23:59',
-            frequency: 'months',
-            vatRate: 0.2,
-            properties: [property]
-        });
-
-        const termsGrandTotal = Array(contract.terms).fill(1).reduce((acc, value, index) => {
-            const prevTerm = index > 0 ? acc[index - 1] : 0;
-            const currentTerm = index > 7 ? prevTerm : prevTerm + rentAmountProperty1; // 7 because property rented for 8 months
-            acc.push(currentTerm);
-            return acc;
-        }, []);
-
-        assert.deepStrictEqual(contract.rents.map(r => r.total.grandTotal), termsGrandTotal);
-    });
-
-    it('compute terms of two properties', () => {
-        const p1 = {
-            entryDate: '01/01/2020',
-            exitDate: '31/12/2020',
-            property: {
-                name: 'office1',
-                price: 300
-            },
-            rent: 300,
-            expenses: [{ title: 'expense', amount: 10 }]
-        };
-
-        let termP1 = p1.rent + p1.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0);
-        termP1 *= 1.2; // VAT
-
-        const p2 = {
-            entryDate: '01/07/2020',
-            exitDate: '31/12/2020',
-            property: {
-                name: 'office',
-                price: 320
-            },
-            rent: 320,
-            expenses: [{ title: 'expense', amount: 32 }]
-        };
-
-        let termP2 = p2.rent + p2.expenses.reduce((acc, { amount }) => {
-            acc += amount;
-            return acc;
-        }, 0);
-        termP2 *= 1.2; // VAT
-
-        const contract = Contract.create({
-            begin: '01/01/2020 00:00',
-            end: '31/12/2020 23:59',
-            frequency: 'months',
-            vatRate: 0.2,
-            properties: [p1, p2]
-        });
-
-        const termsGrandTotal = Array(contract.terms).fill(1).reduce((acc, value, index) => {
-            const prevTerm = index > 0 ? acc[index - 1] : 0;
-            const currentTerm = Math.round((index > 5 ? prevTerm + termP1 + termP2: prevTerm + termP1)*10) / 10; // 5 because after July 2 properties are rented
-            acc.push(currentTerm);
-            return acc;
-        }, []);
-
-        assert.deepStrictEqual(contract.rents.map(r => r.total.grandTotal), termsGrandTotal);
+    assert.throws(() => {
+      Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '01/01/2017 03:00',
+        frequency: 'hours',
+      });
     });
+
+    assert.throws(() => {
+      Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '01/01/2016 03:00',
+        frequency: 'hours',
+        properties: [{}, {}],
+      });
+    });
+
+    assert.throws(() => {
+      Contract.create();
+    });
+  });
+
+  it('check term frequency', () => {
+    let c1;
+    let c2;
+    let c3;
+    let c4;
+    let c5;
+
+    assert.doesNotThrow(() => {
+      c1 = Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '01/01/2017 03:00',
+        frequency: 'hours',
+        properties: [{}, {}],
+      });
+      c2 = Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '31/01/2017 23:59',
+        frequency: 'days',
+        properties: [{}, {}],
+      });
+      c3 = Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '14/01/2017 23:59',
+        frequency: 'weeks',
+        properties: [{}, {}],
+      });
+      c4 = Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '31/12/2017 23:59',
+        frequency: 'months',
+        properties: [{}, {}],
+      });
+      c5 = Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '31/12/2025 23:59',
+        frequency: 'years',
+        properties: [{}, {}],
+      });
+    });
+
+    assert.strictEqual(c1.terms, 3, 'incorrect number of terms');
+    assert.strictEqual(c2.terms, 31, 'incorrect number of terms');
+    assert.strictEqual(c3.terms, 2, 'incorrect number of terms');
+    assert.strictEqual(c4.terms, 12, 'incorrect number of terms');
+    assert.strictEqual(c5.terms, 9, 'incorrect number of terms');
+
+    assert.throws(() => {
+      Contract.create({
+        begin: '01/01/2017 00:00',
+        end: '01/01/2017 03:00',
+        frequency: 'blabla',
+        properties: [{}, {}],
+      });
+    });
+  });
+
+  it('renew contract based on initial number of terms', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    const newContract = Contract.renew(contract);
+
+    assert.strictEqual(newContract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(
+      newContract.rents.length,
+      108 * 2,
+      'incorrect number of rents'
+    );
+  });
+
+  it('update contract change duration', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    const newContract = Contract.update(contract, { end: '31/03/2026 23:59' });
+
+    assert.strictEqual(newContract.terms, 108 + 3, 'incorrect number of terms');
+    assert.strictEqual(
+      newContract.rents.length,
+      108 + 3,
+      'incorrect number of rents'
+    );
+
+    const newContract2 = Contract.update(contract, {
+      begin: '01/01/2018 00:00',
+    });
+    assert.strictEqual(
+      newContract2.terms,
+      108 - 12,
+      'incorrect number of terms'
+    );
+    assert.strictEqual(
+      newContract2.rents.length,
+      108 - 12,
+      'incorrect number of rents'
+    );
+  });
+
+  it('terminate contract', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    const newContract = Contract.terminate(contract, '31/12/2017 23:59');
+
+    assert.strictEqual(newContract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(
+      newContract.rents.length,
+      12,
+      'incorrect number of rents'
+    );
+    assert.strictEqual(
+      newContract.begin,
+      '01/01/2017 00:00',
+      'begin contract date incorrect'
+    );
+    assert.strictEqual(
+      newContract.end,
+      '31/12/2025 23:59',
+      'end contract date incorrect'
+    );
+    assert.strictEqual(
+      newContract.termination,
+      '31/12/2017 23:59',
+      'termination contract date incorrect'
+    );
+
+    // after end date of contract
+    assert.throws(() => {
+      Contract.terminate(contract, '31/12/2026 23:59');
+    });
+
+    // before begin date of contract
+    assert.throws(() => {
+      Contract.terminate(contract, '01/01/2016 00:00');
+    });
+  });
+
+  it('update termination date', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    const tmpContract = Contract.terminate(contract, '31/12/2017 23:59');
+    const longerContract = Contract.terminate(tmpContract, '31/12/2018 23:59');
+
+    assert.strictEqual(longerContract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(
+      longerContract.rents.length,
+      24,
+      'incorrect number of rents'
+    );
+    assert.strictEqual(
+      longerContract.begin,
+      '01/01/2017 00:00',
+      'begin contract date incorrect'
+    );
+    assert.strictEqual(
+      longerContract.end,
+      '31/12/2025 23:59',
+      'end contract date incorrect'
+    );
+    assert.strictEqual(
+      longerContract.termination,
+      '31/12/2018 23:59',
+      'termination contract date incorrect'
+    );
+
+    const shorterContract = Contract.terminate(
+      longerContract,
+      '30/06/2017 23:59'
+    );
+    assert.strictEqual(shorterContract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(
+      shorterContract.rents.length,
+      6,
+      'incorrect number of rents'
+    );
+    assert.strictEqual(
+      shorterContract.begin,
+      '01/01/2017 00:00',
+      'begin contract date incorrect'
+    );
+    assert.strictEqual(
+      shorterContract.end,
+      '31/12/2025 23:59',
+      'end contract date incorrect'
+    );
+    assert.strictEqual(
+      shorterContract.termination,
+      '30/06/2017 23:59',
+      'termination contract date incorrect'
+    );
+  });
+
+  it('terminate contract and change contract duration', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    const terminateContract = Contract.terminate(contract, '31/12/2017 23:59');
+    const newContract = Contract.update(terminateContract, {
+      end: '31/03/2026 23:59',
+    });
+
+    assert.strictEqual(newContract.terms, 108 + 3, 'incorrect number of terms');
+    assert.strictEqual(
+      newContract.rents.length,
+      12,
+      'incorrect number of rents'
+    );
+    assert.strictEqual(
+      newContract.begin,
+      '01/01/2017 00:00',
+      'begin contract date incorrect'
+    );
+    assert.strictEqual(
+      newContract.end,
+      '31/03/2026 23:59',
+      'end contract date incorrect'
+    );
+    assert.strictEqual(
+      newContract.termination,
+      '31/12/2017 23:59',
+      'termination contract date incorrect'
+    );
+
+    // termination date after end contract
+    assert.throws(() => {
+      Contract.update(terminateContract, { end: '30/12/2017 23:59' });
+    });
+
+    // termination date before begin contract
+    assert.throws(() => {
+      Contract.update(terminateContract, { begin: '01/01/2018 23:59' });
+    });
+  });
+
+  it('pay a term', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    Contract.payTerm(contract, '01/12/2025 00:00', {
+      payments: [{ amount: 200 }],
+      discounts: ['discount'],
+    });
+
+    assert.strictEqual(
+      contract.rents.filter((rent) => rent.payments.length === 0).length,
+      contract.terms - 1
+    );
+    assert.strictEqual(
+      contract.rents.find((rent) => rent.term === 2025120100).payments[0]
+        .amount,
+      200
+    );
+    assert.strictEqual(contract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(contract.rents.length, 108, 'incorrect number of rents');
+
+    assert.throws(() => {
+      Contract.payTerm(contract, '01/12/2026 00:00', {
+        payments: [{ amount: 200 }],
+        discounts: ['discount'],
+      });
+    });
+
+    assert.throws(() => {
+      Contract.payTerm(contract, '01/12/2016 00:00', {
+        payments: [{ amount: 200 }],
+        discounts: ['discount'],
+      });
+    });
+  });
+
+  it('pay first term', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    Contract.payTerm(contract, '01/01/2017 00:00', {
+      payments: [{ amount: 200 }],
+      discounts: ['discount'],
+    });
+
+    assert.strictEqual(
+      contract.rents.find((rent) => rent.term === 2017010100).payments[0]
+        .amount,
+      200
+    );
+    assert.strictEqual(
+      contract.rents.findIndex((rent) => rent.term === 2017010100),
+      0
+    );
+  });
+
+  it('pay last term', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    Contract.payTerm(contract, '01/12/2025 00:00', {
+      payments: [{ amount: 200 }],
+      discounts: ['discount'],
+    });
+
+    assert.strictEqual(
+      contract.rents.find((rent) => rent.term === 2025120100).payments[0]
+        .amount,
+      200
+    );
+    assert.strictEqual(
+      contract.rents.findIndex((rent) => rent.term === 2025120100),
+      contract.rents.length - 1
+    );
+  });
+
+  it('pay a term in reverse chronological order', () => {
+    const contract = Contract.create({
+      begin: '01/01/2020 00:00',
+      end: '31/12/2020 23:59',
+      frequency: 'months',
+      vatRate: 0.2,
+      properties: [
+        {
+          entryDate: '01/01/2020',
+          exitDate: '31/12/2020',
+          property: {
+            name: 'office1',
+            price: 100,
+          },
+          rent: 100,
+          expenses: [{ title: 'expense', amount: 10 }],
+        },
+      ],
+    });
+
+    let termAmount = (100 + 10) * 1.2; // VAT
+    assert.strictEqual(
+      contract.rents.find((rent) => rent.term === 2020020100).total.balance,
+      termAmount
+    );
+
+    Contract.payTerm(contract, '01/05/2020 00:00', {
+      payments: [{ amount: termAmount }],
+    });
+    Contract.payTerm(contract, '01/04/2020 00:00', {
+      payments: [{ amount: termAmount }],
+    });
+    Contract.payTerm(contract, '01/03/2020 00:00', {
+      payments: [{ amount: termAmount }],
+    });
+    Contract.payTerm(contract, '01/02/2020 00:00', {
+      payments: [{ amount: termAmount }],
+    });
+    Contract.payTerm(contract, '01/01/2020 00:00', {
+      payments: [{ amount: termAmount }],
+    });
+
+    assert.strictEqual(
+      contract.rents.find((rent) => rent.term === 2020060100).total.balance,
+      0
+    );
+  });
+
+  it('pay a term and update contract duration', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    Contract.payTerm(contract, '01/12/2025 00:00', {
+      payments: [{ amount: 200 }],
+      discounts: ['discount'],
+    });
+
+    const newContract = Contract.update(contract, {
+      begin: '01/01/2019 00:00',
+      end: '31/12/2025 23:59',
+    });
+    assert.strictEqual(
+      newContract.rents.find((rent) => rent.term === 2025120100).payments[0]
+        .amount,
+      200
+    );
+
+    // payment out of contract time frame
+    assert.throws(() => {
+      Contract.update(contract, {
+        begin: '01/01/2019 00:00',
+        end: '31/12/2024 23:59',
+      });
+    });
+  });
+
+  it('pay terms and update contract properties', () => {
+    const p1 = {
+      entryDate: '01/01/2020',
+      exitDate: '31/12/2020',
+      property: {
+        name: 'office1',
+        price: 300,
+      },
+      rent: 300,
+      expenses: [{ title: 'expense', amount: 10 }],
+    };
+
+    let termP1 =
+      p1.rent +
+      p1.expenses.reduce((acc, { amount }) => {
+        acc += amount;
+        return acc;
+      }, 0);
+    termP1 *= 1.2; // VAT
+
+    const contract = Contract.create({
+      begin: '01/01/2020 00:00',
+      end: '31/12/2020 23:59',
+      frequency: 'months',
+      vatRate: 0.2,
+      properties: [p1],
+    });
+    Contract.payTerm(contract, '01/01/2020 00:00', {
+      payments: [{ amount: 372 }],
+    });
+    Contract.payTerm(contract, '01/02/2020 00:00', {
+      payments: [{ amount: 372 }],
+    });
+    Contract.payTerm(contract, '01/03/2020 00:00', {
+      payments: [{ amount: 372 }],
+    });
+    Contract.payTerm(contract, '01/04/2020 00:00', {
+      payments: [{ amount: 372 }],
+    });
+    Contract.payTerm(contract, '01/05/2020 00:00', {
+      payments: [{ amount: 372 }],
+    });
+    Contract.payTerm(contract, '01/06/2020 00:00', {
+      payments: [{ amount: 372 }],
+    });
+
+    let termsGrandTotal = Array(contract.terms)
+      .fill(1)
+      .reduce((acc, value, index) => {
+        const prevTerm = index > 6 ? acc[index - 1] : 0;
+        const currentTerm = prevTerm + termP1;
+        acc.push(currentTerm);
+        return acc;
+      }, []);
+
+    assert.deepStrictEqual(
+      contract.rents.map((r) => r.total.grandTotal),
+      termsGrandTotal
+    );
+
+    // update contract with a new properties
+    const p2 = {
+      entryDate: '01/02/2020',
+      exitDate: '31/12/2020',
+      property: {
+        name: 'office',
+        price: 320,
+      },
+      rent: 320,
+      expenses: [{ title: 'expense', amount: 32 }],
+    };
+
+    let termP2 =
+      p2.rent +
+      p2.expenses.reduce((acc, { amount }) => {
+        acc += amount;
+        return acc;
+      }, 0);
+    termP2 *= 1.2; // VAT
+
+    const newContract = Contract.update(contract, { properties: [p1, p2] });
+
+    termsGrandTotal = [
+      Math.round(termP1 * 10) / 10, // jan
+      Math.round((termP1 + termP2) * 10) / 10, // feb
+      Math.round((termP1 + 2 * termP2) * 10) / 10, // mar
+      Math.round((termP1 + 3 * termP2) * 10) / 10, // apr
+      Math.round((termP1 + 4 * termP2) * 10) / 10, // may
+      Math.round((termP1 + 5 * termP2) * 10) / 10, // jun
+      Math.round((termP1 + 6 * termP2) * 10) / 10, // jul
+      Math.round((2 * termP1 + 7 * termP2) * 10) / 10, // aou
+      Math.round((3 * termP1 + 8 * termP2) * 10) / 10, // sep
+      Math.round((4 * termP1 + 9 * termP2) * 10) / 10, // oct
+      Math.round((5 * termP1 + 10 * termP2) * 10) / 10, // nov
+      Math.round((6 * termP1 + 11 * termP2) * 10) / 10, // dec
+    ];
+
+    assert.deepStrictEqual(
+      newContract.rents.map((r) => r.total.grandTotal),
+      termsGrandTotal
+    );
+  });
+
+  it('pay a term and renew', () => {
+    const contract = Contract.create({
+      begin: '01/01/2017 00:00',
+      end: '31/12/2025 23:59',
+      frequency: 'months',
+      properties: [{}, {}],
+    });
+    Contract.payTerm(contract, '01/12/2025 00:00', {
+      payments: [{ amount: 200 }],
+      discounts: ['discount'],
+    });
+    const newContract = Contract.renew(contract);
+
+    assert.strictEqual(
+      newContract.rents.filter((rent) => rent.payments.length === 0).length,
+      contract.terms * 2 - 1
+    );
+    assert.strictEqual(
+      newContract.rents.find((rent) => rent.term === 2025120100).payments[0]
+        .amount,
+      200
+    );
+    assert.strictEqual(newContract.terms, 108, 'incorrect number of terms');
+    assert.strictEqual(
+      newContract.rents.length,
+      108 * 2,
+      'incorrect number of rents'
+    );
+  });
+
+  it('compute terms', () => {
+    const property = {
+      entryDate: '01/01/2020',
+      exitDate: '31/08/2020',
+      property: {
+        name: 'mon bureau',
+        price: 300,
+      },
+      rent: 300,
+      expenses: [{ title: 'expense', amount: 10 }],
+    };
+
+    let rentAmountProperty1 =
+      property.rent +
+      property.expenses.reduce((acc, { amount }) => {
+        acc += amount;
+        return acc;
+      }, 0);
+    rentAmountProperty1 *= 1.2; // VAT
+
+    const contract = Contract.create({
+      begin: '01/01/2020 00:00',
+      end: '31/12/2020 23:59',
+      frequency: 'months',
+      vatRate: 0.2,
+      properties: [property],
+    });
+
+    const termsGrandTotal = Array(contract.terms)
+      .fill(1)
+      .reduce((acc, value, index) => {
+        const prevTerm = index > 0 ? acc[index - 1] : 0;
+        const currentTerm =
+          index > 7 ? prevTerm : prevTerm + rentAmountProperty1; // 7 because property rented for 8 months
+        acc.push(currentTerm);
+        return acc;
+      }, []);
+
+    assert.deepStrictEqual(
+      contract.rents.map((r) => r.total.grandTotal),
+      termsGrandTotal
+    );
+  });
+
+  it('compute terms of two properties', () => {
+    const p1 = {
+      entryDate: '01/01/2020',
+      exitDate: '31/12/2020',
+      property: {
+        name: 'office1',
+        price: 300,
+      },
+      rent: 300,
+      expenses: [{ title: 'expense', amount: 10 }],
+    };
+
+    let termP1 =
+      p1.rent +
+      p1.expenses.reduce((acc, { amount }) => {
+        acc += amount;
+        return acc;
+      }, 0);
+    termP1 *= 1.2; // VAT
+
+    const p2 = {
+      entryDate: '01/07/2020',
+      exitDate: '31/12/2020',
+      property: {
+        name: 'office',
+        price: 320,
+      },
+      rent: 320,
+      expenses: [{ title: 'expense', amount: 32 }],
+    };
+
+    let termP2 =
+      p2.rent +
+      p2.expenses.reduce((acc, { amount }) => {
+        acc += amount;
+        return acc;
+      }, 0);
+    termP2 *= 1.2; // VAT
+
+    const contract = Contract.create({
+      begin: '01/01/2020 00:00',
+      end: '31/12/2020 23:59',
+      frequency: 'months',
+      vatRate: 0.2,
+      properties: [p1, p2],
+    });
+
+    const termsGrandTotal = Array(contract.terms)
+      .fill(1)
+      .reduce((acc, value, index) => {
+        const prevTerm = index > 0 ? acc[index - 1] : 0;
+        const currentTerm =
+          Math.round(
+            (index > 5 ? prevTerm + termP1 + termP2 : prevTerm + termP1) * 10
+          ) / 10; // 5 because after July 2 properties are rented
+        acc.push(currentTerm);
+        return acc;
+      }, []);
+
+    assert.deepStrictEqual(
+      contract.rents.map((r) => r.total.grandTotal),
+      termsGrandTotal
+    );
+  });
 });
diff --git a/test/notificationmanager.js b/test/notificationmanager.js
index bfd129f..43252e2 100644
--- a/test/notificationmanager.js
+++ b/test/notificationmanager.js
@@ -24,7 +24,7 @@
 //                     user: {
 //                         realm: {
 //                             name: 'test'
-//                         } 
+//                         }
 //                     }
 //                 }
 //             },
@@ -78,7 +78,7 @@
 //                     user: {
 //                         realm: {
 //                             name: 'test'
-//                         } 
+//                         }
 //                     }
 //                 }
 //             },
diff --git a/test/occupantmanager.js b/test/occupantmanager.js
index f5a53e2..df3bbd0 100644
--- a/test/occupantmanager.js
+++ b/test/occupantmanager.js
@@ -5,7 +5,6 @@
 //     proxyquire = require('proxyquire'),
 //     mocks = require('./mocks');
 
-
 // describe('occupantmanager', function() {
 //     it('Check create contract 9 years', function(done) {
 //         var manager;
diff --git a/test/pages.js b/test/pages.js
index 9a59e9d..789937d 100644
--- a/test/pages.js
+++ b/test/pages.js
@@ -3,10 +3,10 @@
 'use strict';
 const proxyquire = require('proxyquire');
 proxyquire('../backend/routes/page', {
-    '../pages/print/model': (req, callback) => {
-        req.model = Object.assign(req.model, { document: 'adoc' });
-        callback();
-    }
+  '../pages/print/model': (req, callback) => {
+    req.model = Object.assign(req.model, { document: 'adoc' });
+    callback();
+  },
 });
 const assert = require('assert');
 const sinon = require('sinon');
@@ -18,193 +18,217 @@ const pageRouter = require('../backend/routes/page');
 logger.info = sinon.stub(logger, 'info').callsFake(() => {});
 
 describe('pages', () => {
-    let viewEngine;
-    beforeEach(() => {
-        viewEngine = sinon.stub();
-    });
-    describe('rendering', () => {
-        it('GET  /', (done) => {
-            config.signup = true;
-            requester(pageRouter(), { httpMethod: 'get', uri: '/' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
-                    assert(filepath.endsWith('index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'website');
-                    done();
-                });
+  let viewEngine;
+  beforeEach(() => {
+    viewEngine = sinon.stub();
+  });
+  describe('rendering', () => {
+    it('GET  /', (done) => {
+      config.signup = true;
+      requester(pageRouter(), { httpMethod: 'get', uri: '/' }, viewEngine)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
+          assert(filepath.endsWith('index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'website');
+          done();
         });
+    });
 
-        it('GET  /signup', (done) => {
-            config.signup = true;
-            requester(pageRouter(), { httpMethod: 'get', uri: '/signup' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
-                    assert(filepath.endsWith('index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'signup');
-                    done();
-                });
+    it('GET  /signup', (done) => {
+      config.signup = true;
+      requester(pageRouter(), { httpMethod: 'get', uri: '/signup' }, viewEngine)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
+          assert(filepath.endsWith('index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'signup');
+          done();
         });
+    });
 
-        it('GET  /signin', (done) => {
-            config.demoMode = false;
-            requester(pageRouter(), { httpMethod: 'get', uri: '/signin' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
-                    assert(filepath.endsWith('index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'signin');
-                    done();
-                });
+    it('GET  /signin', (done) => {
+      config.demoMode = false;
+      requester(pageRouter(), { httpMethod: 'get', uri: '/signin' }, viewEngine)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
+          assert(filepath.endsWith('index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'signin');
+          done();
         });
+    });
 
-        it('GET  /realm', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/realm' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
-                    assert(filepath.endsWith('index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'realm');
-                    done();
-                });
+    it('GET  /realm', (done) => {
+      requester(pageRouter(), { httpMethod: 'get', uri: '/realm' }, viewEngine)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
+          assert(filepath.endsWith('index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'realm');
+          done();
         });
+    });
 
-        it('GET  /:view', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/property' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
-                    assert(filepath.endsWith('index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'property');
-                    done();
-                });
+    it('GET  /:view', (done) => {
+      requester(
+        pageRouter(),
+        { httpMethod: 'get', uri: '/property' },
+        viewEngine
+      )
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
+          assert(filepath.endsWith('index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'property');
+          done();
         });
+    });
 
-        it('GET  /blabla (unknown view 404)', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/blabla' }, viewEngine)
-                .expect(404)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    done();
-                });
+    it('GET  /blabla (unknown view 404)', (done) => {
+      requester(pageRouter(), { httpMethod: 'get', uri: '/blabla' }, viewEngine)
+        .expect(404)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          done();
         });
+    });
 
-        it('GET  /view/:view', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/view/property' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
+    it('GET  /view/:view', (done) => {
+      requester(
+        pageRouter(),
+        { httpMethod: 'get', uri: '/view/property' },
+        viewEngine
+      )
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
 
-                    assert(filepath.endsWith('property/view/index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'property');
-                    done();
-                });
+          assert(filepath.endsWith('property/view/index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'property');
+          done();
         });
+    });
 
-        it('GET  /view/blabla (unknown view 404)', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/view/blabla' }, viewEngine)
-                .expect(404)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    done();
-                });
+    it('GET  /view/blabla (unknown view 404)', (done) => {
+      requester(
+        pageRouter(),
+        { httpMethod: 'get', uri: '/view/blabla' },
+        viewEngine
+      )
+        .expect(404)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          done();
         });
     });
-    describe('printable', () => {
-        it('GET  /print/:id/occupants/:ids/:year/:month', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/print/adoc/occupants/1234/2017/02' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
+  });
+  describe('printable', () => {
+    it('GET  /print/:id/occupants/:ids/:year/:month', (done) => {
+      requester(
+        pageRouter(),
+        { httpMethod: 'get', uri: '/print/adoc/occupants/1234/2017/02' },
+        viewEngine
+      )
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
 
-                    assert(filepath.endsWith('print/view/index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'print');
-                    assert.strictEqual(model.document, 'adoc');
-                    done();
-                });
+          assert(filepath.endsWith('print/view/index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'print');
+          assert.strictEqual(model.document, 'adoc');
+          done();
         });
-        it('GET  /print/:id/occupants/:ids/:year', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/print/adoc/occupants/1234/2017' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
+    });
+    it('GET  /print/:id/occupants/:ids/:year', (done) => {
+      requester(
+        pageRouter(),
+        { httpMethod: 'get', uri: '/print/adoc/occupants/1234/2017' },
+        viewEngine
+      )
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
 
-                    assert(filepath.endsWith('print/view/index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'print');
-                    assert.strictEqual(model.document, 'adoc');
-                    done();
-                });
+          assert(filepath.endsWith('print/view/index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'print');
+          assert.strictEqual(model.document, 'adoc');
+          done();
         });
-        it('GET  /print/:id/occupants/ids', (done) => {
-            requester(pageRouter(), { httpMethod: 'get', uri: '/print/adoc/occupants/1234' }, viewEngine)
-                .expect(200)
-                .end((err) => {
-                    if (err) {
-                        throw err;
-                    }
-                    const viewEngineArgs = viewEngine.args[0];
-                    assert(viewEngineArgs && viewEngineArgs.length > 1);
-                    const filepath = viewEngineArgs[0];
+    });
+    it('GET  /print/:id/occupants/ids', (done) => {
+      requester(
+        pageRouter(),
+        { httpMethod: 'get', uri: '/print/adoc/occupants/1234' },
+        viewEngine
+      )
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            throw err;
+          }
+          const viewEngineArgs = viewEngine.args[0];
+          assert(viewEngineArgs && viewEngineArgs.length > 1);
+          const filepath = viewEngineArgs[0];
 
-                    assert(filepath.endsWith('print/view/index.ejs'));
-                    const model = viewEngineArgs[1];
-                    assert.strictEqual(model.view, 'print');
-                    assert.strictEqual(model.document, 'adoc');
-                    done();
-                });
+          assert(filepath.endsWith('print/view/index.ejs'));
+          const model = viewEngineArgs[1];
+          assert.strictEqual(model.view, 'print');
+          assert.strictEqual(model.document, 'adoc');
+          done();
         });
     });
+  });
 });
diff --git a/test/requester.js b/test/requester.js
index bd0b74c..93456d7 100644
--- a/test/requester.js
+++ b/test/requester.js
@@ -3,23 +3,22 @@ const path = require('path');
 const express = require('express');
 const request = require('supertest');
 
+module.exports = function (router, { httpMethod, uri }, viewEngineStub) {
+  const app = express();
 
-module.exports = function(router, {httpMethod, uri}, viewEngineStub) {
-    const app = express();
+  if (viewEngineStub) {
+    viewEngineStub.callsArgWith(2, '');
+    app.engine('ejs', viewEngineStub);
+    app.set('views', path.join(__dirname, '..', 'backend', 'pages'));
+    app.set('view engine', 'ejs');
+  }
+  app.use((req, res, next) => {
+    // to bypass login
+    req.session = {};
+    req.user = {};
+    next();
+  });
+  app.use(router);
 
-    if (viewEngineStub) {
-        viewEngineStub.callsArgWith(2, '');
-        app.engine('ejs', viewEngineStub);
-        app.set('views', path.join(__dirname, '..', 'backend', 'pages'));
-        app.set('view engine', 'ejs');
-    }
-    app.use((req, res, next) => {
-        // to bypass login
-        req.session = {};
-        req.user = {};
-        next();
-    });
-    app.use(router);
-
-    return request(app)[httpMethod](uri);
+  return request(app)[httpMethod](uri);
 };