├── .husky
├── .gitignore
└── pre-commit
├── .prettierignore
├── test
├── mocha.opts
└── requester.js
├── backend
├── businesslogic
│ ├── FR
│ │ ├── contract
│ │ │ └── index.js
│ │ └── computeRent
│ │ │ ├── tasks
│ │ │ ├── 5_balance.js
│ │ │ ├── 6_payment.js
│ │ │ ├── 2_debts.js
│ │ │ ├── 3_discounts.js
│ │ │ ├── 7_total.js
│ │ │ ├── 4_vat.js
│ │ │ └── 1_base.js
│ │ │ └── index.js
│ └── index.js
├── pages
│ ├── print
│ │ ├── view
│ │ │ ├── index.ejs
│ │ │ ├── partials
│ │ │ │ ├── printbar.ejs
│ │ │ │ ├── placeanddate.ejs
│ │ │ │ ├── rentplaceanddate.ejs
│ │ │ │ ├── ownermanagersignature.ejs
│ │ │ │ ├── htmlhead.ejs
│ │ │ │ ├── footer.ejs
│ │ │ │ ├── customeraddress.ejs
│ │ │ │ ├── customerreference.ejs
│ │ │ │ ├── header.ejs
│ │ │ │ ├── rentcalltotal.ejs
│ │ │ │ ├── scripts.ejs
│ │ │ │ ├── paymentmodalities.ejs
│ │ │ │ ├── invoicetotal.ejs
│ │ │ │ └── invoicebody.ejs
│ │ │ ├── invoice.ejs
│ │ │ ├── rentcall.ejs
│ │ │ ├── recovery1.ejs
│ │ │ ├── recovery2.ejs
│ │ │ ├── guarantycertificate.ejs
│ │ │ ├── guarantypaybackcertificate.ejs
│ │ │ ├── paymentorder.ejs
│ │ │ ├── guarantyrequest.ejs
│ │ │ ├── insurance.ejs
│ │ │ └── recovery3.ejs
│ │ ├── index.js
│ │ └── model
│ │ │ └── index.js
│ ├── profile
│ │ ├── view
│ │ │ ├── account.ejs
│ │ │ ├── index.ejs
│ │ │ └── accountform.ejs
│ │ ├── index.js
│ │ └── model
│ │ │ └── index.js
│ ├── owner
│ │ ├── view
│ │ │ ├── owner.ejs
│ │ │ ├── index.ejs
│ │ │ └── rightmenu.ejs
│ │ ├── index.js
│ │ └── model
│ │ │ └── index.js
│ ├── realm
│ │ ├── index.js
│ │ ├── model
│ │ │ └── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ └── selectrealm.ejs
│ ├── dashboard
│ │ ├── index.js
│ │ ├── model
│ │ │ └── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ └── templates.ejs
│ ├── occupant
│ │ ├── index.js
│ │ ├── model
│ │ │ └── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ ├── invoices.ejs
│ │ │ ├── contractdocumentsform.ejs
│ │ │ ├── rentoverview.ejs
│ │ │ ├── templates.ejs
│ │ │ └── occupant.ejs
│ ├── property
│ │ ├── index.js
│ │ ├── model
│ │ │ └── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ ├── property.ejs
│ │ │ ├── templates.ejs
│ │ │ └── rightmenu.ejs
│ ├── rent
│ │ ├── model
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ └── rent.ejs
│ ├── signin
│ │ ├── model
│ │ │ └── index.js
│ │ ├── view
│ │ │ ├── index.ejs
│ │ │ └── login.ejs
│ │ └── index.js
│ ├── signup
│ │ ├── model
│ │ │ └── index.js
│ │ ├── view
│ │ │ ├── index.ejs
│ │ │ └── signup.ejs
│ │ └── index.js
│ ├── website
│ │ ├── model
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ └── website.ejs
│ ├── accounting
│ │ ├── model
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── view
│ │ │ ├── index.ejs
│ │ │ └── accounting.ejs
│ ├── footer.ejs
│ ├── layout.ejs
│ ├── index.js
│ ├── metatags.ejs
│ ├── ejshelpers.js
│ ├── index.ejs
│ └── header.ejs
├── models
│ ├── document.js
│ ├── notification.js
│ ├── rent.js
│ ├── lease.js
│ ├── account.js
│ ├── property.js
│ ├── occupant.js
│ ├── objectfilter.js
│ ├── model.js
│ └── realm.js
├── managers
│ ├── accountingmanager.js
│ ├── templatemanager.js
│ ├── documentmanager.js
│ ├── ownermanager.js
│ └── emailmanager.js
├── utils
│ └── crypto.js
└── routes
│ ├── page.js
│ ├── index.js
│ ├── auth.js
│ └── api.js
├── .eslintignore
├── .prettierrc.json
├── bkp
└── demodb
│ ├── realms.bson
│ ├── accounts.bson
│ ├── occupants.bson
│ ├── properties.bson
│ ├── realms.metadata.json
│ ├── accounts.metadata.json
│ ├── occupants.metadata.json
│ ├── properties.metadata.json
│ └── system.indexes.bson
├── frontend
├── images
│ ├── logo.png
│ ├── favicon.png
│ ├── logo-128.png
│ ├── logo-512.png
│ └── manhattan.jpg
├── robots.txt
├── less
│ ├── website.less
│ ├── card.less
│ ├── datepicker.less
│ ├── table.less
│ ├── style.less
│ ├── accounting.less
│ ├── menu.less
│ ├── form.less
│ ├── layout.less
│ ├── bootswatch.less
│ ├── index.less
│ ├── list.less
│ └── tiles.less
├── js
│ ├── website
│ │ └── middleware.js
│ ├── baseview_middleware.js
│ ├── view_middleware.js
│ ├── menu_middleware.js
│ ├── signup
│ │ ├── signupform.js
│ │ └── middleware.js
│ ├── login
│ │ ├── loginform.js
│ │ └── middleware.js
│ ├── lib
│ │ ├── objectfilter.js
│ │ └── anilayout.js
│ ├── selectrealm
│ │ └── middleware.js
│ ├── print.js
│ ├── application.js
│ ├── owner
│ │ └── middleware.js
│ ├── menu.js
│ ├── connection_middleware.js
│ ├── index.js
│ ├── property
│ │ ├── middleware.js
│ │ └── propertyform.js
│ ├── language.js
│ └── occupant
│ │ └── contractdocumentsform.js
└── sitemap.xml
├── scripts
├── mongodump.js
└── mongorestore.js
├── .gitignore
├── .travis.yml
├── dev.Dockerfile
├── .eslintrc.json
├── Dockerfile
├── LICENSE
├── documentation
├── ABOUT.md
├── README.md
└── content home page
├── config
├── website.json
└── index.js
├── README.md
├── .github
└── workflows
│ └── dockerpublish.yml
└── package.json
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | bkp
2 | dist
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --recursive
2 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/contract/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/pages/print/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%- include(document) %>
--------------------------------------------------------------------------------
/backend/pages/profile/view/account.ejs:
--------------------------------------------------------------------------------
1 | <%- include accountform.ejs %>
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | bkp
2 | coverage
3 | dist
4 | node_modules
5 | test
6 | scripts
7 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/backend/pages/owner/view/owner.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%include ownerform%>
3 |
4 |
--------------------------------------------------------------------------------
/bkp/demodb/realms.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/bkp/demodb/realms.bson
--------------------------------------------------------------------------------
/bkp/demodb/accounts.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/bkp/demodb/accounts.bson
--------------------------------------------------------------------------------
/bkp/demodb/occupants.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/bkp/demodb/occupants.bson
--------------------------------------------------------------------------------
/frontend/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/frontend/images/logo.png
--------------------------------------------------------------------------------
/bkp/demodb/properties.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/bkp/demodb/properties.bson
--------------------------------------------------------------------------------
/frontend/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/frontend/images/favicon.png
--------------------------------------------------------------------------------
/frontend/images/logo-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/frontend/images/logo-128.png
--------------------------------------------------------------------------------
/frontend/images/logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/frontend/images/logo-512.png
--------------------------------------------------------------------------------
/frontend/images/manhattan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camelaissani/loca/HEAD/frontend/images/manhattan.jpg
--------------------------------------------------------------------------------
/bkp/demodb/realms.metadata.json:
--------------------------------------------------------------------------------
1 | { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "demodb.realms" } ] }
--------------------------------------------------------------------------------
/bkp/demodb/accounts.metadata.json:
--------------------------------------------------------------------------------
1 | { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "demodb.accounts" } ] }
--------------------------------------------------------------------------------
/bkp/demodb/occupants.metadata.json:
--------------------------------------------------------------------------------
1 | { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "demodb.occupants" } ] }
--------------------------------------------------------------------------------
/backend/pages/owner/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'owner',
4 | restricted: true,
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/backend/pages/realm/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'realm',
4 | restricted: true,
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/bkp/demodb/properties.metadata.json:
--------------------------------------------------------------------------------
1 | { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "demodb.properties" } ] }
--------------------------------------------------------------------------------
/frontend/robots.txt:
--------------------------------------------------------------------------------
1 | Sitemap: http://localhost:8081/sitemap.xml
2 | User-agent: *
3 | Disallow: /public/
4 | Disallow: /bower_components/
--------------------------------------------------------------------------------
/backend/pages/dashboard/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'dashboard',
4 | restricted: true,
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/backend/pages/occupant/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'occupant',
4 | restricted: true,
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/backend/pages/profile/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'profile',
4 | restricted: true,
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/backend/pages/property/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'property',
4 | restricted: true,
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/printbar.ejs:
--------------------------------------------------------------------------------
1 |
2 | Imprimer
3 |
--------------------------------------------------------------------------------
/backend/pages/occupant/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/owner/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/profile/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/property/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/realm/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/rent/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/signin/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/signup/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/website/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/accounting/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/dashboard/model/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (req, callback) {
2 | req.model = Object.assign({}, req.model);
3 | callback();
4 | };
5 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/placeanddate.ejs:
--------------------------------------------------------------------------------
1 | <%= realm.addresses[0].city %>, le <%= today %>
2 |
--------------------------------------------------------------------------------
/backend/pages/website/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'website',
4 | public: true,
5 | restricted: true,
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/backend/pages/rent/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'rent',
4 | params: '/:year?/:month?',
5 | restricted: true,
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/rentplaceanddate.ejs:
--------------------------------------------------------------------------------
1 | <%= realm.addresses[0].city %>, le <%= rent.today %>
2 |
--------------------------------------------------------------------------------
/frontend/less/website.less:
--------------------------------------------------------------------------------
1 | .centeredbtn {
2 | margin-top: 80px;
3 | p {
4 | margin-top: 25px;
5 | }
6 | .btn {
7 | width: 220px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/backend/businesslogic/index.js:
--------------------------------------------------------------------------------
1 | const config = require('../../config');
2 |
3 | module.exports = {
4 | computeRent: require(`./${config.businesslogic}/computeRent`),
5 | };
6 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/ownermanagersignature.ejs:
--------------------------------------------------------------------------------
1 |
2 | <%=realm.companyInfo.legalRepresentative%>
3 | Gérant
4 |
--------------------------------------------------------------------------------
/backend/pages/accounting/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'accounting',
4 | params: '/:year?/:month?',
5 | restricted: true,
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/backend/pages/signin/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-login',
3 | //topMenu = '',
4 | //rightMenu = '',
5 | content = 'login';
6 | //template= '';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/signup/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-signup',
3 | //topMenu = '',
4 | //rightMenu = '',
5 | content = 'signup';
6 | //template= '';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/owner/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-owner',
3 | //topMenu = '',
4 | rightMenu = 'rightmenu',
5 | content = 'owner';
6 | //template= '';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/profile/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-account',
3 | //topMenu = '',
4 | //rightMenu = '',
5 | content = 'account';
6 | //template= '';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/website/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-website',
3 | //topMenu = '',
4 | //rightMenu = '',
5 | content = 'website';
6 | //template= '';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/print/index.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | id: 'print',
4 | params: '/:id/occupants/:ids/:year?/:month?',
5 | supportView: false,
6 | restricted: true,
7 | };
8 | };
9 |
--------------------------------------------------------------------------------
/backend/pages/realm/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-selectrealm',
3 | //topMenu = '',
4 | //rightMenu = '',
5 | content = 'selectrealm';
6 | //template= '';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/occupant/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-occupant',
3 | rightMenu = 'rightmenu',
4 | content = 'occupant',
5 | templates = 'templates';
6 | %>
7 |
8 | <%- include ../../layout.ejs %>
9 |
--------------------------------------------------------------------------------
/backend/pages/property/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-property',
3 | rightMenu = 'rightmenu',
4 | content = 'property',
5 | templates = 'templates';
6 | %>
7 |
8 | <%- include ../../layout.ejs %>
9 |
--------------------------------------------------------------------------------
/backend/pages/dashboard/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-dashboard',
3 | //topMenu = '',
4 | //rightMenu = '',
5 | content = 'dashboard',
6 | templates = 'templates';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/rent/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-rent',
3 | //topMenu = 'topmenu',
4 | rightMenu = 'rightmenu',
5 | content = 'rent',
6 | templates = 'templates';
7 | %>
8 |
9 | <%- include ../../layout.ejs %>
--------------------------------------------------------------------------------
/backend/pages/accounting/view/index.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var viewId = 'view-accounting',
3 | //rightMenu = 'rightmenu',
4 | content = 'accounting',
5 | templates = 'templates';
6 | %>
7 |
8 | <%- include ../../layout.ejs %>
9 |
--------------------------------------------------------------------------------
/backend/pages/signin/index.js:
--------------------------------------------------------------------------------
1 | const config = require('../../../config');
2 |
3 | module.exports = () => {
4 | if (!config.demoMode) {
5 | return {
6 | id: 'signin',
7 | public: true,
8 | };
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/backend/pages/signup/index.js:
--------------------------------------------------------------------------------
1 | const config = require('../../../config');
2 |
3 | module.exports = () => {
4 | if (config.signup) {
5 | return {
6 | id: 'signup',
7 | public: true,
8 | };
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/scripts/mongodump.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const mongobackup = require('mongobackup');
3 | const config = require('../config');
4 |
5 | mongobackup.dump({
6 | db: config.database,
7 | out: path.join(__dirname, '..', 'bkp'),
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/less/card.less:
--------------------------------------------------------------------------------
1 | .card {
2 | padding: 20px;
3 |
4 | background-color: @well-bg;
5 | border-color: @well-border;
6 | border-radius: @border-radius-base;
7 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 2px 3px rgba(0, 0, 0, 0.2);
8 | }
9 |
--------------------------------------------------------------------------------
/backend/pages/property/view/property.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include propertyform.ejs %>
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/htmlhead.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var min = '';
3 | if (config.productive) {
4 | min = '.min';
5 | }
6 | %>
7 |
8 |
9 | Loca
10 |
11 |
--------------------------------------------------------------------------------
/frontend/less/datepicker.less:
--------------------------------------------------------------------------------
1 | .month-picker,
2 | .year-picker {
3 | display: none;
4 | .datepicker-months,
5 | .datepicker-years,
6 | .datepicker-decades,
7 | .datepicker-centuries {
8 | background-color: #fff;
9 | border: 1px solid @well-border;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Coverage directory used by tools like istanbul
2 | .nyc_output
3 | coverage
4 |
5 | # Dependency directory
6 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
7 | node_modules
8 |
9 | dist
10 | bkp/*
11 | !bkp/demodb
12 | .vscode
--------------------------------------------------------------------------------
/backend/models/document.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const OF = require('./objectfilter');
3 |
4 | class DocumentModel {
5 | constructor() {
6 | this.schema = new OF({
7 | _id: String,
8 | documents: Array,
9 | });
10 | }
11 | }
12 |
13 | module.exports = new DocumentModel();
14 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/footer.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bkp/demodb/system.indexes.bson:
--------------------------------------------------------------------------------
1 | G v key _id name _id_ ns demodb.occupants H v key _id name _id_ ns demodb.properties D v key _id name _id_ ns demodb.realms F v key _id name _id_ ns demodb.accounts
--------------------------------------------------------------------------------
/backend/pages/realm/view/selectrealm.ejs:
--------------------------------------------------------------------------------
1 |
2 | <% for (index=0; index
3 |
4 | <%= realms[index].name %>
5 |
6 | <% } %>
7 |
8 |
--------------------------------------------------------------------------------
/frontend/js/website/middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import frontexpress from 'frontexpress';
3 |
4 | class WebsiteMiddleware extends frontexpress.Middleware {
5 | // overriden
6 | entered() {
7 | $('body').addClass('covered-body');
8 | $('body > .footer').show();
9 | }
10 | }
11 |
12 | export default WebsiteMiddleware;
13 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/5_balance.js:
--------------------------------------------------------------------------------
1 | module.exports = function (
2 | contract,
3 | rentDate,
4 | previousRent,
5 | settlements,
6 | rent
7 | ) {
8 | rent.balance = 0;
9 | if (previousRent) {
10 | rent.total.balance =
11 | previousRent.total.grandTotal - previousRent.total.payment;
12 | }
13 | return rent;
14 | };
15 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/6_payment.js:
--------------------------------------------------------------------------------
1 | module.exports = function (
2 | contract,
3 | rentDate,
4 | previousRent,
5 | settlements,
6 | rent
7 | ) {
8 | if (settlements && settlements.payments) {
9 | settlements.payments.forEach((payment) => {
10 | rent.payments.push(payment);
11 | });
12 | }
13 | return rent;
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/less/table.less:
--------------------------------------------------------------------------------
1 | .table > tbody > tr > td,
2 | .table > tbody > tr > th,
3 | .table > tfoot > tr > td,
4 | .table > tfoot > tr > th,
5 | .table > thead > tr > td,
6 | .table > thead > tr > th {
7 | padding: 4px;
8 | }
9 |
10 | .table .table {
11 | background-color: transparent;
12 | }
13 |
14 | .table .table.table-borderless td {
15 | border: none;
16 | }
17 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/customeraddress.ejs:
--------------------------------------------------------------------------------
1 |
2 |
<%= occupant.name %>
3 | <%= (occupant.name != occupant.manager)?occupant.manager:'' %>
4 | <%= occupant.street1 %>
5 | <% if (occupant.street2 && occupant.street2.length>0) { %>
6 | <%= occupant.street2 %>
7 | <% } %>
8 | <%= occupant.zipCode %> <%= occupant.city %>
9 |
--------------------------------------------------------------------------------
/frontend/js/baseview_middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import frontexpress from 'frontexpress';
3 |
4 | class BaseViewMiddleware extends frontexpress.Middleware {
5 | entered() {
6 | // hide footer and background image
7 | $('body').removeClass('covered-body');
8 | $('body > .footer').hide();
9 | }
10 | }
11 |
12 | export default BaseViewMiddleware;
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | before_install:
4 | - sudo apt-get update -qq
5 | # - sudo apt-get install -y nasm
6 |
7 | node_js:
8 | - '8.12.0'
9 |
10 | before_script:
11 | - npm install coveralls
12 |
13 | script:
14 | - npm run lint
15 | - npm run coverage
16 |
17 | after_script:
18 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
19 |
--------------------------------------------------------------------------------
/backend/models/notification.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const OF = require('./objectfilter');
3 | const Model = require('./model');
4 |
5 | class NotificationModel extends Model {
6 | constructor() {
7 | super('notifications');
8 | this.schema = new OF({
9 | id: String,
10 | status: String,
11 | });
12 | }
13 | }
14 |
15 | module.exports = new NotificationModel();
16 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/2_debts.js:
--------------------------------------------------------------------------------
1 | module.exports = function (
2 | contract,
3 | rentDate,
4 | previousRent,
5 | settlements,
6 | rent
7 | ) {
8 | if (settlements && settlements.debts) {
9 | settlements.debts.forEach((debt) => {
10 | rent.debts.push({
11 | description: debt.description,
12 | amount: debt.amount,
13 | });
14 | });
15 | }
16 | return rent;
17 | };
18 |
--------------------------------------------------------------------------------
/frontend/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | http://localhost:8081/
9 | monthly
10 |
11 |
--------------------------------------------------------------------------------
/backend/models/rent.js:
--------------------------------------------------------------------------------
1 | const OF = require('./objectfilter');
2 |
3 | class RentModel {
4 | constructor() {
5 | this.paymentSchema = new OF({
6 | _id: String,
7 | month: Number,
8 | year: Number,
9 | payments: Array,
10 | description: String,
11 | promo: Number,
12 | notepromo: String,
13 | extracharge: Number,
14 | noteextracharge: String,
15 | });
16 | }
17 | }
18 |
19 | module.exports = new RentModel();
20 |
--------------------------------------------------------------------------------
/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-stretch
2 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5
3 | RUN echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/3.6 main" | tee /etc/apt/sources.list.d/mongodb-org-3.6.list
4 | RUN apt-get update -qq && \
5 | apt-get upgrade -qqy && \
6 | apt-get install -qqy \
7 | mongodb-org-tools nasm
8 |
9 | WORKDIR /usr/app
10 |
11 | COPY . .
12 | RUN npm ci
13 |
14 | CMD npm run dev
--------------------------------------------------------------------------------
/frontend/js/view_middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import frontexpress from 'frontexpress';
3 |
4 | class ViewMiddleware extends frontexpress.Middleware {
5 | updated(req, res) {
6 | // fill view container
7 | if (res.responseText) {
8 | const $container = $('.js-view-container');
9 | $container.html(res.responseText);
10 | $container.css('visibility', 'visible');
11 | $container.css('opacity', 1);
12 | }
13 | }
14 | }
15 |
16 | export default ViewMiddleware;
17 |
--------------------------------------------------------------------------------
/backend/managers/accountingmanager.js:
--------------------------------------------------------------------------------
1 | const FD = require('./frontdata');
2 | const occupantModel = require('../models/occupant');
3 |
4 | ////////////////////////////////////////////////////////////////////////////////
5 | // Exported functions
6 | ////////////////////////////////////////////////////////////////////////////////
7 | function all(req, res) {
8 | const year = req.params.year;
9 |
10 | occupantModel.findAll(req.realm, (errors, occupants) => {
11 | res.json(FD.toAccountingData(year, occupants));
12 | });
13 | }
14 |
15 | module.exports = {
16 | all,
17 | };
18 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "parserOptions": {
4 | "ecmaVersion": 12,
5 | "sourceType": "module"
6 | },
7 | "rules": {
8 | "semi": "error",
9 | "quotes": ["error", "single", { "avoidEscape": true }],
10 | "no-unused-vars": [
11 | "error",
12 | {
13 | "vars": "all",
14 | "args": "after-used",
15 | "ignoreRestSiblings": true
16 | }
17 | ]
18 | },
19 | "env": {
20 | "node": true,
21 | "browser": true,
22 | "jquery": true,
23 | "es2021": true
24 | },
25 | "extends": ["eslint:recommended"]
26 | }
27 |
--------------------------------------------------------------------------------
/backend/pages/accounting/view/accounting.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= t('Period') %>
5 |
6 |
10 |
11 |
12 |
13 |
17 |
--------------------------------------------------------------------------------
/frontend/less/style.less:
--------------------------------------------------------------------------------
1 | .label {
2 | border-radius: 10px;
3 | color: #fff;
4 | padding-top: 5px;
5 | padding-bottom: 5px;
6 | margin-bottom: 2px;
7 |
8 | &.label-danger {
9 | background-color: lighten(@brand-danger, 20%);
10 | }
11 | &.label-warning {
12 | background-color: lighten(@brand-warning, 20%);
13 | }
14 | &.label-success {
15 | background-color: lighten(@brand-success, 10%);
16 | }
17 | }
18 |
19 | .price-content {
20 | position: relative;
21 | padding-right: 10px;
22 | }
23 | .price-symbol {
24 | font-size: ceil((@font-size-base * 0.65));
25 | position: absolute;
26 | top: 0;
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/js/menu_middleware.js:
--------------------------------------------------------------------------------
1 | import frontexpress from 'frontexpress';
2 |
3 | class MenuMiddleware extends frontexpress.Middleware {
4 | entered(req) {
5 | const menus = document.querySelectorAll('li > .js-nav-action');
6 | for (let i = 0; i < menus.length; i++) {
7 | const menu = menus[i];
8 | const parentMenu = menu.parentNode;
9 | const re = new RegExp(`^/view/${menu.dataset.id}|^/${menu.dataset.id}`);
10 | if (req.uri.match(re)) {
11 | parentMenu.className = 'active';
12 | menu.focus();
13 | } else {
14 | parentMenu.className = '';
15 | }
16 | }
17 | }
18 | }
19 |
20 | export default MenuMiddleware;
21 |
--------------------------------------------------------------------------------
/frontend/js/signup/signupform.js:
--------------------------------------------------------------------------------
1 | import Form from '../form';
2 |
3 | class SignupForm extends Form {
4 | constructor() {
5 | super({
6 | domSelector: '#signup-form',
7 | httpMethod: 'POST',
8 | uri: '/signup',
9 | manifest: {
10 | firstname: {
11 | required: true,
12 | },
13 | lastname: {
14 | required: true,
15 | },
16 | username: {
17 | required: true,
18 | email: true,
19 | },
20 | password: {
21 | required: true,
22 | },
23 | },
24 | alertOnFieldError: false,
25 | });
26 | }
27 | }
28 |
29 | export default SignupForm;
30 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/3_discounts.js:
--------------------------------------------------------------------------------
1 | module.exports = function (
2 | contract,
3 | rentDate,
4 | previousRent,
5 | settlements,
6 | rent
7 | ) {
8 | if (contract.discount) {
9 | rent.discounts.push({
10 | origin: 'contract',
11 | description: 'Remise exceptionnelle',
12 | amount: contract.discount,
13 | });
14 | }
15 |
16 | if (settlements && settlements.discounts) {
17 | settlements.discounts.forEach((discount) => {
18 | rent.discounts.push({
19 | origin: 'settlement',
20 | description: discount.description,
21 | amount: discount.amount,
22 | });
23 | });
24 | }
25 | return rent;
26 | };
27 |
--------------------------------------------------------------------------------
/backend/pages/owner/view/rightmenu.ejs:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/customerreference.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Date d'entrée
5 | <%= formatDate(occupant.beginDate) %>
6 |
7 |
8 | Référence facture
9 | <%= rent.billingReference %>
10 |
11 |
12 | Période de location
13 | <%= rent.period %>
14 |
15 |
16 | Numéro de TVA intra.
17 | <%= realm.companyInfo.vatNumber %>
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/requester.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('path');
3 | const express = require('express');
4 | const request = require('supertest');
5 |
6 | module.exports = function (router, { httpMethod, uri }, viewEngineStub) {
7 | const app = express();
8 |
9 | if (viewEngineStub) {
10 | viewEngineStub.callsArgWith(2, '');
11 | app.engine('ejs', viewEngineStub);
12 | app.set('views', path.join(__dirname, '..', 'backend', 'pages'));
13 | app.set('view engine', 'ejs');
14 | }
15 | app.use((req, res, next) => {
16 | // to bypass login
17 | req.session = {};
18 | req.user = {};
19 | next();
20 | });
21 | app.use(router);
22 |
23 | return request(app)[httpMethod](uri);
24 | };
25 |
--------------------------------------------------------------------------------
/backend/pages/footer.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/header.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
<%= realm.companyInfo.name %>
4 |
5 | <%= realm.addresses[0].street1 %>
6 | <% if (realm.addresses[0].street2 && realm.addresses[0].street2.length>0) { %>
7 | <%= realm.addresses[0].street2 %>
8 | <% } %>
9 | <%= realm.addresses[0].zipCode %> <%= realm.addresses[0].city %>
10 |
11 |
12 | Tél. : <%= realm.contacts[0].phone1 %> <%= realm.contacts[0].phone2?' - '+realm.contacts[0].phone2:'' %>
13 | <% if (realm.contacts[0].email && realm.contacts[0].email.length>0) { %>
14 | E-mail : <%= realm.contacts[0].email %>
15 | <% } %>
16 |
17 |
--------------------------------------------------------------------------------
/frontend/js/login/loginform.js:
--------------------------------------------------------------------------------
1 | import Form from '../form';
2 |
3 | class LoginForm extends Form {
4 | constructor() {
5 | super({
6 | domSelector: '#login-form',
7 | httpMethod: 'POST',
8 | uri: '/signin',
9 | manifest: {
10 | username: {
11 | required: true,
12 | email: true,
13 | },
14 | password: {
15 | required: true,
16 | },
17 | },
18 | alertOnFieldError: false,
19 | });
20 | }
21 |
22 | onGetData(data) {
23 | if (data.username) {
24 | data.email = data.username;
25 | delete data.username;
26 | }
27 | if (data.password) {
28 | data.secretword = data.password;
29 | delete data.password;
30 | }
31 | return data;
32 | }
33 | }
34 |
35 | export default LoginForm;
36 |
--------------------------------------------------------------------------------
/frontend/js/lib/objectfilter.js:
--------------------------------------------------------------------------------
1 | class ObjectFilter {
2 | static filter(schema, data) {
3 | var self = this;
4 | var filteredData = {};
5 | var key;
6 | var value;
7 | var childSchema;
8 |
9 | for (key in schema) {
10 | value = data[key];
11 | if (value !== undefined) {
12 | childSchema = schema[key];
13 | if (Array.isArray(childSchema)) {
14 | if (Array.isArray(value)) {
15 | filteredData[key] = [];
16 | value.forEach(function (data /*, index*/) {
17 | filteredData[key].push(self.filter(childSchema[0], data));
18 | });
19 | }
20 | } else {
21 | filteredData[key] = value;
22 | }
23 | }
24 | }
25 | return filteredData;
26 | }
27 | }
28 | export default ObjectFilter;
29 |
--------------------------------------------------------------------------------
/backend/pages/website/view/website.ejs:
--------------------------------------------------------------------------------
1 | <%= t(config.website.product.slogan) %>
2 | <% if (!isLogged) { %>
3 |
4 | <% if (config.demoMode) { %>
5 |
<%= t('Demonstration') %>
6 | <% } else { %>
7 | <% if (!config.signup) { %>
8 |
<%= t('Free subscription') %>
9 | <% } else { %>
10 |
<%= t('Free subscription') %>
11 | <% } %>
12 |
13 | <%= t('You already have an account?') %> <%= t('Sign in') %>
14 |
15 | <% } %>
16 |
17 | <% } %>
18 |
--------------------------------------------------------------------------------
/backend/pages/occupant/view/invoices.ejs:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-stretch
2 |
3 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5
4 | RUN echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/3.6 main" | tee /etc/apt/sources.list.d/mongodb-org-3.6.list
5 | RUN apt-get update -qq && \
6 | apt-get upgrade -qqy && \
7 | apt-get install -qqy mongodb-org-tools
8 |
9 | WORKDIR /usr/app
10 | COPY backend backend
11 | COPY bkp/demodb bkp/demodb
12 | COPY config config
13 | COPY frontend frontend
14 | COPY scripts scripts
15 | COPY LICENSE .
16 | COPY package.json .
17 | COPY package-lock.json .
18 | COPY server.js .
19 |
20 | RUN npm set progress=false && \
21 | npm config set depth 0 && \
22 | npm install forever -g --silent && \
23 | npm ci && \
24 | npm run buildprod && \
25 | NODE_ENV=production npm prune
26 |
27 | CMD forever ./server.js
28 |
--------------------------------------------------------------------------------
/frontend/js/selectrealm/middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import application from '../application';
3 | import ViewController from '../viewcontroller';
4 |
5 | class SelectRealmMiddleware extends ViewController {
6 | constructor() {
7 | super({
8 | domViewId: '#view-selectrealm',
9 | });
10 | }
11 |
12 | onInitListener() {
13 | $(document).on('click', '.js-realm-action', function () {
14 | const $action = $(this);
15 | const realmId = $action.data('id');
16 | application.httpGet(`/api/realms/${realmId}`, (req, res) => {
17 | const response = JSON.parse(res.responseText);
18 | const location = window.location;
19 | if (response.status === 'success') {
20 | window.location = `${location.origin}/dashboard`;
21 | }
22 | });
23 | return false;
24 | });
25 | }
26 | }
27 |
28 | export default SelectRealmMiddleware;
29 |
--------------------------------------------------------------------------------
/frontend/js/print.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import i18next from 'i18next';
3 | import application from './application';
4 | import language from './language';
5 |
6 | const LOCA = application.get('LOCA');
7 |
8 | language(LOCA.countryCode, (countryCode) => {
9 | LOCA.countryCode = countryCode;
10 | $('#printbutton').click(function () {
11 | let error = false;
12 | $('[contenteditable]').each(function () {
13 | if (
14 | $(this)
15 | .html()
16 | .replace(/ /g, '')
17 | .replace(' ', '').length === 0
18 | ) {
19 | $(this).addClass('error');
20 | error = true;
21 | } else {
22 | $(this).removeClass('error');
23 | }
24 | });
25 | if (error) {
26 | window.alert(
27 | i18next.t('Fill the empty fields before printing the document')
28 | );
29 | } else {
30 | window.print();
31 | }
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/backend/models/lease.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const OF = require('./objectfilter');
3 | const Model = require('./model');
4 |
5 | class LeaseModel extends Model {
6 | constructor() {
7 | super('leases');
8 | this.schema = new OF({
9 | _id: String,
10 | name: String,
11 | description: String,
12 | numberOfTerms: Number,
13 | timeRange: String, // days, weeks, months, years
14 | active: Boolean,
15 | system: Boolean,
16 | templateId: String,
17 | });
18 | }
19 |
20 | findAll(realm, callback) {
21 | super.findAll(realm, (errors, leases) => {
22 | if (errors && errors.length > 0) {
23 | callback(errors);
24 | return;
25 | }
26 |
27 | callback(
28 | null,
29 | leases.sort((p1, p2) => {
30 | return p1.name.localeCompare(p2.name);
31 | })
32 | );
33 | });
34 | }
35 | }
36 |
37 | module.exports = new LeaseModel();
38 |
--------------------------------------------------------------------------------
/backend/pages/print/view/invoice.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant) {
8 | occupant.rents.forEach(function(rent) {
9 | %>
10 |
11 |
12 | <%- include partials/header.ejs %>
13 |
14 | <%- include partials/customeraddress.ejs %>
15 |
16 | <%- include partials/rentplaceanddate.ejs %>
17 |
18 |
Quittance
19 |
20 | <%- include partials/customerreference.ejs %>
21 |
22 |
23 |
24 | <%- include partials/invoicebody %>
25 |
26 | <%- include partials/invoicetotal %>
27 |
28 |
29 | <%- include partials/footer %>
30 |
31 | <%
32 | });
33 | });
34 | %>
35 | <%- include partials/printbar %>
36 |
37 | <%- include partials/scripts %>
38 |
39 |
--------------------------------------------------------------------------------
/frontend/less/accounting.less:
--------------------------------------------------------------------------------
1 | #view-accounting {
2 | .table {
3 | width: inherit;
4 | }
5 |
6 | tbody tr td {
7 | height: 95px;
8 | min-width: 90px;
9 | max-width: 90px;
10 | width: 90px;
11 | &.inactive {
12 | background-color: lighten(@gray-base, 80%);
13 | }
14 | }
15 |
16 | tbody tr td:first-child {
17 | max-width: none;
18 | white-space: nowrap;
19 | }
20 |
21 | #accounting-payments-table-top-hscroll {
22 | overflow-x: scroll;
23 | overflow-y: hidden;
24 | #accounting-payments-fake-table {
25 | height: 1px; // hack to display the h scrollbar
26 | }
27 | }
28 | #accounting-payments-per-year-table.table {
29 | margin-bottom: 0;
30 | }
31 |
32 | #accounting-payments-table tbody tr td:first-child {
33 | max-width: none;
34 | white-space: nowrap;
35 | }
36 |
37 | small {
38 | font-size: ceil((@font-size-base * 0.9));
39 | color: @gray-light;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/backend/pages/print/model/index.js:
--------------------------------------------------------------------------------
1 | const FD = require('../../../managers/frontdata');
2 | const occupantModel = require('../../../models/occupant');
3 |
4 | module.exports = function (req, callback) {
5 | const realm = req.realm;
6 | const doc = req.params.id;
7 | const month = req.params.month;
8 | const fromMonth = req.params.fromMonth;
9 | const year = req.params.year;
10 | const occupantIds = req.params.ids ? req.params.ids.split(',') : [];
11 |
12 | occupantModel.findFilter(
13 | realm,
14 | { $query: { _id: { $in: occupantIds } } },
15 | (errors, occupants) => {
16 | if (errors && errors.length > 0) {
17 | callback(errors);
18 | return;
19 | }
20 | const data = FD.toPrintData(
21 | realm,
22 | doc,
23 | fromMonth,
24 | month,
25 | year,
26 | occupants
27 | );
28 | req.model = Object.assign(req.model, data);
29 | callback();
30 | }
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/less/menu.less:
--------------------------------------------------------------------------------
1 | .side-menu {
2 | display: none;
3 | position: fixed;
4 |
5 | .row {
6 | margin: 0;
7 | }
8 | .page-header {
9 | margin-top: 0;
10 | }
11 | .btn {
12 | text-align: left;
13 | }
14 |
15 | .list-group {
16 | max-height: 350px;
17 | overflow-y: auto;
18 | margin-bottom: 0;
19 | }
20 |
21 | .list-group.list-group-selection {
22 | .list-group-item {
23 | padding: 2px 10px 0 10px;
24 | .fa-times {
25 | cursor: pointer;
26 | }
27 | }
28 | }
29 |
30 | .list-group.list-group-selection {
31 | &.fixed .list-group-item {
32 | .fa-times {
33 | display: none;
34 | }
35 | }
36 | }
37 |
38 | .panel:last-of-type {
39 | margin-bottom: 0;
40 | }
41 | }
42 |
43 | @media (max-width: @screen-md) {
44 | .navbar-fixed-top {
45 | position: relative;
46 | width: @container-desktop;
47 | }
48 | .side-menu {
49 | position: relative;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/rentcalltotal.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sous total H.T.
4 | <%= formatMoney(rent.total.subTotal) %>
5 |
6 | <% if (rent.vats.length) { %>
7 |
8 |
9 | TVA <%= formatPercent(rent.vats[0].rate) %>
10 | <%= formatMoney(rent.total.vat) %>
11 |
12 | <% } %>
13 |
14 |
15 | Solde antérieur
16 | <%= formatMoney(rent.total.balance) %>
17 |
18 |
19 |
20 | Total T.T.C.
21 | <%= formatMoney(rent.total.grandTotal) %>
22 |
23 |
--------------------------------------------------------------------------------
/backend/pages/layout.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if (typeof topMenu != 'undefined') { %>
5 |
6 | <%- include(topMenu) %>
7 |
8 | <% } %>
9 |
10 | <% if (typeof rightMenu != 'undefined') { %>
11 |
12 | <%- include(content) %>
13 |
14 |
15 |
18 | <% } else { %>
19 |
20 | <%- include(content) %>
21 |
22 | <% } %>
23 |
24 |
25 |
26 |
27 |
28 | <% if (typeof templates != 'undefined') { %>
29 | <%- include(templates) %>
30 | <% } %>
31 |
32 |
--------------------------------------------------------------------------------
/backend/utils/crypto.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const config = require('../../config');
3 |
4 | const key = config.CIPHER_KEY;
5 | const iv_key = config.CIPHER_IV_KEY;
6 |
7 | const iv = crypto.createHash('sha256').update(iv_key).digest();
8 | const bufferedIV = Buffer.allocUnsafe(16);
9 | iv.copy(bufferedIV);
10 |
11 | function encrypt(text) {
12 | const hashedKey = crypto.createHash('sha256').update(key).digest();
13 | const cipher = crypto.createCipheriv('aes-256-cbc', hashedKey, bufferedIV);
14 | return [cipher.update(text, 'binary', 'hex'), cipher.final('hex')].join('');
15 | }
16 |
17 | function decrypt(encryptedText) {
18 | const hashedKey = crypto.createHash('sha256').update(key).digest();
19 | const decipher = crypto.createDecipheriv(
20 | 'aes-256-cbc',
21 | hashedKey,
22 | bufferedIV
23 | );
24 | return [
25 | decipher.update(encryptedText, 'hex', 'binary'),
26 | decipher.final('binary'),
27 | ].join('');
28 | }
29 |
30 | module.exports = {
31 | encrypt,
32 | decrypt,
33 | };
34 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/scripts.ejs:
--------------------------------------------------------------------------------
1 | <%
2 | var min = '';
3 | if (config.productive) {
4 | min = '.min';
5 | }
6 | %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/paymentmodalities.ejs:
--------------------------------------------------------------------------------
1 |
2 |
MODALITES DE PAIEMENT
3 |
4 |
5 | Par virement bancaire
6 | Nos coordonnées bancaires
7 | <%= realm.bankInfo.name %>
8 | IBAN : <%= realm.bankInfo.iban %>
9 |
10 | Par espèce
11 | Contactez <%= realm.contacts[0].name %>
12 | Tél : <%= realm.contacts[0].phone1 %> <%= realm.contacts[0].phone2?' - '+realm.contacts[0].phone2:'' %>
13 | E-mail : <%= realm.contacts[0].email %>
14 |
15 |
16 | Par chèque bancaire ou postal
17 | Etablissez votre chèque à l'ordre de la <%= realm.companyInfo.legalStructure+' '+realm.companyInfo.name %> et à envoyer à l'adresse suivante :
18 | <%= realm.addresses[0].street1 %>
19 | <% if (realm.addresses[0].street2 && realm.addresses[0].street2.length>0) { %>
20 | <%= realm.addresses[0].street2 %>
21 | <% } %>
22 | <%= realm.addresses[0].zipCode %> <%= realm.addresses[0].city %>
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Camel Aissani
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/backend/pages/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const logger = require('winston');
4 |
5 | const restrictedList = []; // list of {id, params}
6 | const publicList = []; // list of {id, params}
7 | const publicRestrictedList = []; // list of {id, params}
8 |
9 | const root_pages_dir = path.join(__dirname);
10 |
11 | fs.readdirSync(root_pages_dir)
12 | .filter((page) => fs.lstatSync(path.join(root_pages_dir, page)).isDirectory())
13 | .forEach((page) => {
14 | const pageDesc = Object.assign(
15 | {
16 | public: false,
17 | restricted: false,
18 | supportView: true,
19 | },
20 | require(`./${page}`)()
21 | );
22 |
23 | if (pageDesc.public && pageDesc.restricted) {
24 | publicRestrictedList.push(pageDesc);
25 | } else if (pageDesc.public) {
26 | publicList.push(pageDesc);
27 | } else {
28 | restrictedList.push(pageDesc);
29 | }
30 | logger.debug(`loaded page ${page}`);
31 | });
32 |
33 | module.exports = {
34 | list: [...publicList, ...restrictedList, ...publicRestrictedList],
35 | publicList,
36 | restrictedList,
37 | publicRestrictedList,
38 | };
39 |
--------------------------------------------------------------------------------
/backend/models/account.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Model = require('./model');
4 | const OF = require('./objectfilter');
5 |
6 | class AccountModel extends Model {
7 | constructor() {
8 | super('accounts');
9 | this.schema = new OF({
10 | email: String,
11 | password: String,
12 | firstname: String,
13 | lastname: String,
14 | creation: String,
15 | });
16 | }
17 |
18 | findOne(email, callback) {
19 | super.findFilter(
20 | null,
21 | {
22 | email: email.toLowerCase(),
23 | },
24 | (errors, accounts) => {
25 | if (errors) {
26 | callback(errors);
27 | } else if (!accounts || accounts.length === 0) {
28 | callback(null, null);
29 | } else {
30 | callback(null, accounts[0]);
31 | }
32 | }
33 | );
34 | }
35 |
36 | add(item, callback) {
37 | super.add(null, item, callback);
38 | }
39 |
40 | findAll(callback) {
41 | super.findAll(null, function (errors, accounts) {
42 | if (errors) {
43 | return callback(errors);
44 | }
45 | callback(null, accounts);
46 | });
47 | }
48 | }
49 |
50 | module.exports = new AccountModel();
51 |
--------------------------------------------------------------------------------
/scripts/mongorestore.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { URL } = require('url');
4 | const mongobackup = require('mongobackup');
5 | const config = require('../config');
6 |
7 | const db_url = new URL(config.database);
8 | const db_name = db_url.pathname.slice(1);
9 |
10 | const bkpDirectory = path.join(__dirname, '..', 'bkp');
11 | const bkpFile = path.join(bkpDirectory, `${db_name}.dump`);
12 |
13 | module.exports = async () => {
14 | await new Promise((resolve, reject) => {
15 | try {
16 | let cmd;
17 | if (fs.existsSync(bkpFile)) {
18 | cmd = mongobackup.restore({
19 | host: db_url.hostname,
20 | drop: true,
21 | gzip: true,
22 | archive: bkpFile,
23 | });
24 | } else {
25 | cmd = mongobackup.restore({
26 | db: db_name,
27 | host: db_url.hostname,
28 | drop: true,
29 | path: path.join(bkpDirectory, db_name),
30 | });
31 | }
32 | cmd.on('close', (code) => {
33 | resolve(code);
34 | });
35 | cmd.on('error', (error) => {
36 | reject(error);
37 | });
38 | } catch (error) {
39 | reject(error);
40 | }
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/7_total.js:
--------------------------------------------------------------------------------
1 | const math = require('mathjs');
2 |
3 | module.exports = function (
4 | contract,
5 | rentDate,
6 | previousRent,
7 | settlements,
8 | rent
9 | ) {
10 | const preTaxAmount = rent.preTaxAmounts.reduce(
11 | (total, preTaxAmount) => total + preTaxAmount.amount,
12 | 0
13 | );
14 | const charges = rent.charges.reduce(
15 | (total, charges) => total + charges.amount,
16 | 0
17 | );
18 | const debts = rent.debts.reduce((total, debt) => total + debt.amount, 0);
19 | const discount = rent.discounts.reduce(
20 | (total, discount) => total + discount.amount,
21 | 0
22 | );
23 | const vat = math.round(
24 | rent.vats.reduce((total, vat) => total + vat.amount, 0),
25 | 2
26 | );
27 | const payment = rent.payments.reduce(
28 | (total, payment) => total + payment.amount,
29 | 0
30 | );
31 |
32 | rent.total.preTaxAmount = preTaxAmount;
33 | rent.total.charges = charges;
34 | rent.total.debts = debts;
35 | rent.total.discount = discount;
36 | rent.total.vat = vat;
37 | rent.total.grandTotal = math.round(
38 | preTaxAmount + charges + debts - discount + vat + rent.total.balance,
39 | 2
40 | );
41 | rent.total.payment = payment;
42 |
43 | return rent;
44 | };
45 |
--------------------------------------------------------------------------------
/backend/pages/print/view/rentcall.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant) {
8 | occupant.rents.forEach(function(rent) {
9 | %>
10 |
11 |
12 | <%- include partials/header.ejs %>
13 |
14 | <%- include partials/header.ejs %>
15 |
16 | <%- include partials/customeraddress.ejs %>
17 |
18 | <%- include partials/customerreference.ejs %>
19 |
20 | <%- include partials/rentplaceanddate.ejs %>
21 |
22 |
Avis d'échéance
23 |
24 |
25 | Montant total de <%= formatMoney(rent.total.grandTotal) %> à nous faire parvenir avant le <%= rent.dueDate %> .
26 |
27 |
28 |
29 |
30 |
31 | <%- include partials/invoicebody %>
32 |
33 | <%- include partials/rentcalltotal %>
34 |
35 |
36 |
37 |
38 | <%- include partials/paymentmodalities %>
39 |
40 | <%- include partials/footer %>
41 |
42 | <%
43 | });
44 | });
45 | %>
46 | <%- include partials/printbar %>
47 |
48 | <%- include partials/scripts %>
49 |
50 |
--------------------------------------------------------------------------------
/backend/models/property.js:
--------------------------------------------------------------------------------
1 | const OF = require('./objectfilter');
2 | const Model = require('./model');
3 |
4 | class PropertyModel extends Model {
5 | constructor() {
6 | super('properties');
7 | this.schema = new OF({
8 | _id: String,
9 | type: String,
10 | name: String,
11 | description: String,
12 | surface: Number,
13 | phone: String,
14 | digicode: String,
15 | address: Object, // { street1, street2, zipCode, city, state, country }
16 |
17 | price: Number,
18 |
19 | // TODO moved in Occupant.properties model
20 | expense: Number,
21 |
22 | // TODO to remove, replaced by address
23 | building: String,
24 | level: String,
25 | location: String,
26 | });
27 | }
28 |
29 | findAll(realm, callback) {
30 | super.findAll(realm, (errors, properties) => {
31 | if (errors && errors.length > 0) {
32 | callback(errors);
33 | return;
34 | }
35 |
36 | callback(
37 | null,
38 | properties.sort((p1, p2) => {
39 | if (p1.type === p2.type) {
40 | return p1.name.localeCompare(p2.name);
41 | }
42 | return p1.type.localeCompare(p2.type);
43 | })
44 | );
45 | });
46 | }
47 | }
48 |
49 | module.exports = new PropertyModel();
50 |
--------------------------------------------------------------------------------
/backend/pages/print/view/recovery1.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant) {
8 | occupant.rents.forEach(function(rent) {
9 | %>
10 |
11 |
12 | <%- include partials/header.ejs %>
13 |
14 | <%- include partials/customeraddress.ejs %>
15 |
16 | <%- include partials/customerreference.ejs %>
17 |
18 | <%- include partials/rentplaceanddate.ejs %>
19 |
20 |
Avis d'échéance
21 |
22 |
23 | Montant total de <%= formatMoney(rent.total.grandTotal) %> était à nous faire parvenir avant le <%= rent.dueDate %> .
24 |
25 |
26 |
27 |
28 |
29 | <%- include partials/invoicebody %>
30 |
31 | <%- include partials/rentcalltotal %>
32 |
33 |
34 |
35 |
36 | <%- include partials/paymentmodalities %>
37 |
38 | <%- include partials/footer %>
39 |
40 | Rappel Loyer impayé
41 |
42 |
43 | <%
44 | });
45 | });
46 | %>
47 | <%- include partials/printbar %>
48 |
49 | <%- include partials/scripts %>
50 |
51 |
--------------------------------------------------------------------------------
/documentation/ABOUT.md:
--------------------------------------------------------------------------------
1 | # About Loca
2 |
3 | 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,
4 | without any of the complex or complicated features… and best of all, absolutely free to use anytime, anywhere?
5 |
6 | This is the underlying philosophy behind the creation and development of Loca by its Lead Developer, Camel Aissani.
7 | A rental property manager by profession who also enjoys Software Development and Coding as a part time hobby.
8 |
9 | From the initial plan of creating Loca to help himself and his fellow rental property manager cum friend in their businesses, Loca has evolved
10 | into a user-friendly, durable and no-cost solution for rental property management.
11 |
12 | Loca is ideal for small, independent landlords and property managers to effectively manage their businesses, keep accurate and secure rental records,
13 | 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.
14 |
15 | Our promise to you our esteemed clients is that Loca will be a welcome solution to your rental management needs,
16 | not an additional problem.
17 |
18 | Thank You For Using Loca Today.
19 |
--------------------------------------------------------------------------------
/backend/pages/print/view/recovery2.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant) {
8 | occupant.rents.forEach(function(rent) {
9 | %>
10 |
11 |
12 | <%- include partials/header.ejs %>
13 |
14 | <%- include partials/customeraddress.ejs %>
15 |
16 | <%- include partials/customerreference.ejs %>
17 |
18 | <%- include partials/rentplaceanddate.ejs %>
19 |
20 |
Avis d'échéance
21 |
22 |
23 | Montant total de <%= formatMoney(rent.total.grandTotal) %> était à nous faire parvenir avant le <%= rent.dueDate %> .
24 |
25 |
26 |
27 |
28 |
29 | <%- include partials/invoicebody %>
30 |
31 | <%- include partials/rentcalltotal %>
32 |
33 |
34 |
35 |
36 | <%- include partials/paymentmodalities %>
37 |
38 | <%- include partials/footer %>
39 |
40 | Dernier rappel avant huissier
41 |
42 |
43 | <%
44 | });
45 | });
46 | %>
47 | <%- include partials/printbar %>
48 |
49 | <%- include partials/scripts %>
50 |
51 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/4_vat.js:
--------------------------------------------------------------------------------
1 | module.exports = function (
2 | contract,
3 | rentDate,
4 | previousRent,
5 | settlements,
6 | rent
7 | ) {
8 | if (contract.vatRate) {
9 | const rate = contract.vatRate || 0;
10 |
11 | rent.preTaxAmounts.forEach((preTaxAmount) => {
12 | rent.vats.push({
13 | origin: 'contract',
14 | description: `${preTaxAmount.description} T.V.A. (${rate * 100}%)`,
15 | amount: preTaxAmount.amount * rate,
16 | rate,
17 | });
18 | });
19 |
20 | rent.charges.forEach((charges) => {
21 | rent.vats.push({
22 | origin: 'contract',
23 | description: `${charges.description} T.V.A. (${rate * 100}%)`,
24 | amount: charges.amount * rate,
25 | rate,
26 | });
27 | });
28 |
29 | rent.debts.forEach((debt) => {
30 | rent.vats.push({
31 | origin: 'debts',
32 | description: `${debt.description} T.V.A. (${rate * 100}%)`,
33 | amount: debt.amount * rate,
34 | rate,
35 | });
36 | });
37 |
38 | rent.discounts.forEach((discount) => {
39 | rent.vats.push({
40 | origin: discount.origin,
41 | description: `${discount.description} T.V.A. (${rate * 100}%)`,
42 | amount: discount.amount * rate * -1,
43 | rate,
44 | });
45 | });
46 | }
47 |
48 | return rent;
49 | };
50 |
--------------------------------------------------------------------------------
/config/website.json:
--------------------------------------------------------------------------------
1 | {
2 | "website": {
3 | "company": {
4 | "name": "",
5 | "website": ""
6 | },
7 | "product": {
8 | "name": "Loca",
9 | "slogan": "Real estate management",
10 | "website": "https://demo.nuageprive.fr/",
11 | "imageUrl": "http://demo.nuageprive.fr/public/images/1.jpg"
12 | },
13 | "contact": {
14 | "phone": "01.99.99.99.99",
15 | "email": "camel.aissani@nuageprive.fr",
16 | "address": {
17 | "street1": "",
18 | "street2": "",
19 | "zipCode": "",
20 | "city": ""
21 | }
22 | },
23 | "metatags": {
24 | "type": "website",
25 | "title": "Open source real estate management",
26 | "description": "",
27 | "keywords": "open source software, free software, real estate management, online management, rent management, invoices, rent notices",
28 | "location": "Paris, France",
29 | "rating": "General",
30 | "author": {
31 | "name": "Camel Aissani",
32 | "twitter": "@camelaissani"
33 | }
34 | },
35 | "author": {
36 | "name": "Camel Aissani",
37 | "website": "http://www.nuageprive.fr",
38 | "twitter": {
39 | "url": "https://twitter.com/camelaissani",
40 | "id": "@camelaissani"
41 | },
42 | "github": {
43 | "url": "https://github.com/camelaissani",
44 | "id": "camelaissani"
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/js/signup/middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import i18next from 'i18next';
3 | import SignupForm from './signupform.js';
4 | import frontexpress from 'frontexpress';
5 |
6 | class SignupMiddleware extends frontexpress.Middleware {
7 | constructor() {
8 | super();
9 | this.form = new SignupForm();
10 | }
11 |
12 | // overriden
13 | entered() {
14 | $('body').addClass('covered-body');
15 | $('body > .footer').show();
16 | }
17 |
18 | updated() {
19 | this.form.bindForm();
20 | $('#signup-send').click(() => {
21 | this.form.submit((response) => {
22 | let message;
23 |
24 | if (response.status === 'success') {
25 | $('#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
26 | return;
27 | }
28 |
29 | if (response.status === 'missing-field') {
30 | message = i18next.t('Please fill missing fields');
31 | } else if (response.status === 'signup-email-taken') {
32 | message = i18next.t('This user already exists');
33 | } else {
34 | message = i18next.t("A technical issue has occurred (-_-')");
35 | }
36 |
37 | this.form.showErrorMessage(message);
38 | });
39 | return false;
40 | });
41 | }
42 |
43 | // overriden
44 | exited() {
45 | this.form.unbindForm();
46 | }
47 | }
48 |
49 | export default SignupMiddleware;
50 |
--------------------------------------------------------------------------------
/backend/pages/signin/view/login.ejs:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/backend/pages/print/view/guarantycertificate.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 | <%
6 | occupants.forEach(function(occupant, index) {
7 | %>
8 |
9 |
10 | <%- include partials/header.ejs %>
11 |
12 | <%- include partials/customeraddress.ejs %>
13 |
14 | <%- include partials/placeanddate.ejs %>
15 |
16 |
17 | Reçu du dépôt de garantie
18 |
19 |
20 |
21 |
22 | Reçu de Madame/Monsieur <%=occupant.manager%>
23 |
24 |
25 | La somme de <%=occupant.guaranty.toFixed(2)%> euros.
26 |
27 |
28 | Pour le montant d'un dépôt de garantie correspondant aux locaux occupés par <%=occupant.name%> dans l'immeuble situé au :
29 |
30 |
31 | <%= occupant.street1 %>
32 | <% if (occupant.street2 && occupant.street2.length>0) { %>
33 | <%= occupant.street2 %>
34 | <% } %>
35 | <%= occupant.zipCode %> <%= occupant.city %>
36 |
37 | <%- include partials/ownermanagersignature %>
38 |
39 |
40 | <%- include partials/footer %>
41 |
42 | <% }); %>
43 | <%- include partials/printbar %>
44 |
45 | <%- include partials/scripts %>
46 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/invoicetotal.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sous total H.T.
4 | <%= formatMoney(rent.total.subTotal) %>
5 |
6 | <% if (rent.vats.length) { %>
7 |
8 |
9 | TVA <%= formatPercent(rent.vats[0].rate) %>
10 | <%= formatMoney(rent.total.vat) %>
11 |
12 | <% } %>
13 |
14 |
15 | Solde antérieur
16 | <%= formatMoney(rent.total.balance) %>
17 |
18 |
19 |
20 | Total T.T.C.
21 | <%= formatMoney(rent.total.grandTotal) %>
22 |
23 |
24 |
25 |
26 | Montant payé
27 | <%= formatMoney(rent.total.payment) %>
28 |
29 |
30 |
31 |
32 | <%= rent.total.newBalance < 0 ? "Solde en votre faveur" : "Montant restant à payer" %>
33 | <%= formatMoney(Math.abs(rent.total.newBalance)) %>
34 |
35 |
--------------------------------------------------------------------------------
/backend/pages/dashboard/view/templates.ejs:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
--------------------------------------------------------------------------------
/frontend/less/form.less:
--------------------------------------------------------------------------------
1 | .huge {
2 | font-size: @jumbotron-font-size;
3 | height: @jumbotron-font-size * 2;
4 | }
5 |
6 | .form-control[readonly='readonly'] {
7 | color: @text-color;
8 | background-color: @body-bg;
9 | border: none;
10 | box-shadow: none;
11 | cursor: default;
12 | -webkit-appearance: none;
13 | -moz-appearance: none;
14 | &::-ms-expand {
15 | display: none;
16 | }
17 | }
18 |
19 | form {
20 | .form-error {
21 | display: none;
22 | margin-bottom: 15px;
23 | .fa {
24 | color: @brand-warning;
25 | font-size: 20px;
26 | }
27 | }
28 | }
29 |
30 | .form-card {
31 | background-color: @well-bg;
32 | max-width: 500px;
33 | margin: auto;
34 | margin-top: 40px;
35 | .btn {
36 | width: 100%;
37 | }
38 | p {
39 | margin-top: 10px;
40 | }
41 | padding: 10px 0;
42 | box-shadow: none;
43 | .form-group {
44 | margin-bottom: 5px;
45 | &:last-of-type {
46 | margin-bottom: 15px;
47 | }
48 | }
49 | .help-block {
50 | margin-top: 10px;
51 | margin-bottom: 0;
52 | }
53 |
54 | .page-header {
55 | padding: 0 40px 20px 40px;
56 | margin: 0;
57 | h1,
58 | h2,
59 | h3,
60 | h4,
61 | h5,
62 | h6,
63 | .h1,
64 | .h2,
65 | .h3,
66 | .h4,
67 | .h5,
68 | .h6 {
69 | color: @text-color;
70 | }
71 | }
72 | form {
73 | padding: 0 50px;
74 | }
75 | }
76 |
77 | body.covered-body {
78 | .form-card {
79 | color: @text-color;
80 | a {
81 | color: @text-color;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/backend/pages/metatags.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/backend/routes/page.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const express = require('express');
5 | const config = require('../../config');
6 | const pages = require('../pages');
7 |
8 | function buildModel(pageId, req, callback) {
9 | req.model = {
10 | config,
11 | view: pageId,
12 | isLogged: req.user ? true : false,
13 | isRealmSelected: req.realm ? true : false,
14 | isDefaultRealmSelected: req.realm && req.realm.name === '__default_',
15 | isMultipleRealmsAvailable: req.realms && req.realms.length > 1,
16 | user: req.user,
17 | realm: req.realm,
18 | realms: req.realms,
19 | errors: null,
20 | };
21 | const modelFn = require(path.join('..', 'pages', pageId, 'model'));
22 | modelFn(req, callback);
23 | }
24 |
25 | function renderPage(pageId, req, res, pageWithHeaders = true) {
26 | const page = pageWithHeaders ? 'index' : `${pageId}/view/index`;
27 | res.render(page, req.model);
28 | }
29 |
30 | module.exports = function () {
31 | const router = express.Router();
32 |
33 | pages.list.forEach((page) => {
34 | const params = page.params || '';
35 | const path = page.id === 'website' ? params || '/' : `/${page.id}${params}`;
36 | router.get(path, (req, res) => {
37 | buildModel(page.id, req, () =>
38 | renderPage(page.id, req, res, page.supportView)
39 | );
40 | });
41 | if (page.supportView) {
42 | router.get(`/view/${page.id}${params}`, (req, res) => {
43 | buildModel(page.id, req, () => renderPage(page.id, req, res, false));
44 | });
45 | }
46 | });
47 |
48 | return router;
49 | };
50 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | module.exports = function (contract, rentDate, previousRent, settlements) {
5 | const rent = {
6 | term: 0,
7 | month: 0,
8 | year: 0,
9 | preTaxAmounts: [
10 | // {
11 | // description: '',
12 | // amount: ''
13 | // }
14 | ],
15 | charges: [
16 | // {
17 | // description: '',
18 | // amount: ''
19 | // }
20 | ],
21 | discounts: [
22 | // {
23 | // origin: '', // 'contract', 'settlement'
24 | // description: '',
25 | // amount: ''
26 | // }
27 | ],
28 | debts: [
29 | // {
30 | // description: '',
31 | // amount: ''
32 | // }
33 | ],
34 | vats: [
35 | // {
36 | // origin: '', // 'contract', 'settlement'
37 | // description: '',
38 | // rate: 0,
39 | // amount: 0
40 | // }
41 | ],
42 | payments: [
43 | // {
44 | // date: '',
45 | // amount: 0,
46 | // type: '',
47 | // reference: ''
48 | // }
49 | ],
50 | description: '',
51 | total: {
52 | balance: 0,
53 | preTaxAmount: 0,
54 | charges: 0,
55 | discount: 0,
56 | vat: 0,
57 | grandTotal: 0,
58 | payment: 0,
59 | },
60 | };
61 | const tasks_dir = path.join(__dirname, 'tasks');
62 | const taskFiles = fs.readdirSync(tasks_dir);
63 | return taskFiles.reduce((rent, taskFile) => {
64 | const task = require(path.join(tasks_dir, taskFile));
65 | return task(contract, rentDate, previousRent, settlements, rent);
66 | }, rent);
67 | };
68 |
--------------------------------------------------------------------------------
/backend/models/occupant.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const OF = require('./objectfilter');
3 | const Model = require('./model');
4 |
5 | class OccupantModel extends Model {
6 | constructor() {
7 | super('occupants');
8 | this.schema = new OF({
9 | _id: String,
10 | isCompany: Boolean,
11 | company: String,
12 | legalForm: String,
13 | siret: String,
14 | capital: Number,
15 | manager: String,
16 | name: String,
17 | street1: String,
18 | street2: String,
19 | zipCode: String,
20 | city: String,
21 | state: String,
22 | country: String,
23 | contacts: Array,
24 | contract: String,
25 | leaseId: String,
26 | beginDate: String,
27 | endDate: String,
28 | frequency: String,
29 | terminationDate: String,
30 | guarantyPayback: Number,
31 | properties: Array, // [{ propertyId, property: { ... }, entryDate, exitDate, rent, expenses: [{title, amount}] }]
32 | guaranty: Number,
33 | reference: String,
34 | isVat: Boolean,
35 | vatRatio: Number,
36 | discount: Number,
37 | rents: Array,
38 | });
39 | }
40 |
41 | findAll(realm, callback) {
42 | super.findAll(realm, (errors, occupants) => {
43 | if (errors && errors.length > 0) {
44 | callback(errors);
45 | return;
46 | }
47 |
48 | callback(
49 | null,
50 | occupants.sort((o1, o2) => {
51 | const name1 = (o1.isCompany ? o1.company : o1.name) || '';
52 | const name2 = (o2.isCompany ? o2.company : o2.name) || '';
53 |
54 | return name1.localeCompare(name2);
55 | })
56 | );
57 | });
58 | }
59 | }
60 |
61 | module.exports = new OccupantModel();
62 |
--------------------------------------------------------------------------------
/frontend/js/application.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import frontexpress from 'frontexpress';
3 |
4 | const application = frontexpress();
5 |
6 | const now = new Date();
7 |
8 | application.set('LOCA', {
9 | currentMonth: now.getMonth() + 1,
10 | currentYear: now.getFullYear(),
11 | countryCode: 'en-US',
12 | });
13 |
14 | const httpPostPatchTransformer = {
15 | data({ data }) {
16 | if (!data) {
17 | return data;
18 | }
19 |
20 | return $.param(data);
21 | },
22 | headers({ headers, data }) {
23 | if (!data) {
24 | return headers;
25 | }
26 | const updatedHeaders = headers || {};
27 | if (!updatedHeaders['Content-Type']) {
28 | updatedHeaders['Content-Type'] =
29 | 'application/x-www-form-urlencoded; charset=UTF-8';
30 | }
31 | return updatedHeaders;
32 | },
33 | };
34 |
35 | application.set('http POST transformer', httpPostPatchTransformer);
36 | application.set('http PATCH transformer', httpPostPatchTransformer);
37 |
38 | application.openPrintPreview = (url) => {
39 | window.open(
40 | url,
41 | '_blank',
42 | 'location=no,menubar=yes,status=no,titlebar=yes,toolbar=yes,scrollbars=yes,resizable=yes,width=1000,height=700'
43 | );
44 | };
45 |
46 | application.sendEmail = (
47 | tenantIds,
48 | document,
49 | year,
50 | month,
51 | callback = () => {}
52 | ) => {
53 | if (!tenantIds) {
54 | callback();
55 | }
56 | application.httpPost(
57 | {
58 | uri: '/api/emails',
59 | data: {
60 | document,
61 | tenantIds,
62 | year,
63 | month,
64 | },
65 | },
66 | (req, res) => {
67 | callback(JSON.parse(res.responseText));
68 | }
69 | );
70 | };
71 |
72 | export default application;
73 |
--------------------------------------------------------------------------------
/frontend/less/layout.less:
--------------------------------------------------------------------------------
1 | html {
2 | position: relative;
3 | min-height: 100%;
4 | }
5 |
6 | body {
7 | overflow-x: hidden;
8 | background-color: @body-bg;
9 | padding-top: 70px;
10 | padding-bottom: 15px;
11 | margin-bottom: @footer-height;
12 | }
13 | .covered-body {
14 | background: url('/public/images/manhattan.jpg') no-repeat center center fixed;
15 | background-size: cover;
16 |
17 | font-family: 'Open Sans', Arial, sans-serif;
18 | .view {
19 | color: #fff;
20 | a {
21 | color: #fff;
22 | }
23 | h1,
24 | h2,
25 | h3,
26 | h4,
27 | h5,
28 | h6,
29 | .h1,
30 | .h2,
31 | .h3,
32 | .h4,
33 | .h5,
34 | .h6 {
35 | color: #fff;
36 | }
37 | }
38 | }
39 |
40 | .footer {
41 | position: absolute;
42 | bottom: 0;
43 | width: 100%;
44 | height: @footer-height;
45 | color: @navbar-default-color;
46 | background: @navbar-default-bg;
47 | font-size: 11px;
48 | padding: 15px 0;
49 |
50 | a {
51 | color: @navbar-default-link-color;
52 | padding-right: 5px;
53 | &:focus {
54 | outline: 0;
55 | }
56 | }
57 | }
58 |
59 | .sheet {
60 | display: none;
61 | }
62 |
63 | .modal {
64 | z-index: 9999;
65 | }
66 |
67 | // SPECIFIC
68 | #waitwindow {
69 | position: fixed;
70 | height: 60px;
71 | width: 230px;
72 | top: 50%;
73 | left: 50%;
74 | margin-top: -60px/2;
75 | margin-left: -230px/2;
76 | color: #ffffff;
77 | background-color: #f0ad4e;
78 | border-color: #eea236;
79 |
80 | .loading {
81 | padding-left: 10px;
82 | font-size: 18px;
83 | }
84 | }
85 |
86 | @media (max-width: @screen-md) {
87 | body {
88 | padding-top: 0;
89 | overflow-x: auto;
90 | }
91 | .footer {
92 | width: @container-desktop;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/frontend/js/login/middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import i18next from 'i18next';
3 | import LoginForm from './loginform';
4 | import frontexpress from 'frontexpress';
5 |
6 | class LoginMiddleware extends frontexpress.Middleware {
7 | constructor() {
8 | super();
9 | this.form = new LoginForm();
10 | }
11 |
12 | // overriden
13 | entered() {
14 | $('body').addClass('covered-body');
15 | $('body > .footer').show();
16 | }
17 |
18 | updated(req, res) {
19 | super.updated(req, res);
20 |
21 | this.form.bindForm();
22 | $('#login-send').click(() => {
23 | this.form.submit((response) => {
24 | let message;
25 |
26 | if (response.status === 'success') {
27 | $('#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
28 | return;
29 | }
30 |
31 | if (response.status === 'login-user-not-found') {
32 | message = i18next.t('Unknown user');
33 | } else if (response.status === 'login-invalid-password') {
34 | message = i18next.t('Bad password');
35 | } else if (response.status === 'login-realm-not-found') {
36 | message = i18next.t(
37 | 'This user does not manage any real estate accounts'
38 | );
39 | } else if (response.status === 'missing-field') {
40 | message = i18next.t('Please fill missing fields');
41 | } else {
42 | message = i18next.t("A technical issue has occurred (-_-')");
43 | }
44 | this.form.showErrorMessage(message);
45 | });
46 | return false;
47 | });
48 | }
49 |
50 | // overriden
51 | exited() {
52 | this.form.unbindForm();
53 | }
54 | }
55 |
56 | export default LoginMiddleware;
57 |
--------------------------------------------------------------------------------
/backend/pages/profile/view/accountform.ejs:
--------------------------------------------------------------------------------
1 |
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :warning: DEPRECATED :warning:
2 |
3 | This repository is deprecated in favor of [MicroRealEstate](https://github.com/microrealestate/microrealestate).
4 |
5 | The new version uses latest stack on the front-end (NextJS, ReactJS, MobX). The split between front and back-end code is more obvious and the UI has been fully rewritten.
6 |
7 | Also, additional functionalities were added in order to customize the application to the landlords needs.
8 |
9 | Developer wise [MicroRealEstate](https://github.com/microrealestate/microrealestate) should be easier to enhance and to maintain.
10 |
11 | ## Loca
12 |
13 | ### What is Loca?
14 |
15 | 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...
16 |
17 | The idea is to make easy the management of properties and occupants by proposing many services:
18 |
19 | - Gather all information of your properties and occupants in one place
20 | - Create rent contract from templates available in the system
21 | - Follow the rent payments month by month
22 | - Template letters for recovery of not paid rents
23 |
24 | 
25 |
26 | ### Getting started
27 |
28 | Follow instructions from [here](https://github.com/microrealestate/microrealestate#getting-started)
29 |
30 | ### Technical Stack
31 |
32 | Back-end:
33 |
34 | Node, Express, MongoDB, EJS (templates), PassportJS (authentication)
35 |
36 | Front-End:
37 |
38 | JQuery, Bootstrap, Handlebars, and [frontexpress](https://github.com/camelaissani/frontexpress)
39 |
40 | Build system based on RollupJS
41 |
42 | ### Why do I created this application?
43 |
44 | Simply to help my best friend and I to manage properties that we rent.
45 |
46 | Above all, to have a good reason to play with node and javascript :-)
47 |
--------------------------------------------------------------------------------
/backend/pages/print/view/guarantypaybackcertificate.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <% occupants.forEach(function(occupant, index) { %>
7 |
8 |
9 | <%- include partials/header.ejs %>
10 |
11 | <%- include partials/customeraddress.ejs %>
12 |
13 | <%- include partials/placeanddate.ejs %>
14 |
15 |
16 | Remboursement du dépôt de garantie
17 |
18 |
19 |
20 |
21 | Je soussigné Madame/Monsieur <%=occupant.manager%>
22 |
23 |
24 | Avoir reçu la somme de <%=occupant.guaranty.toFixed(2)%> euros par chèque numéro du bailleur <%= realm.companyInfo.name %> .
25 |
26 |
27 | Ce montant correspond au remboursement du dépôt de garantie de locaux occupés par <%=occupant.name%> dans l'immeuble situé au :
28 |
29 |
30 | <%= occupant.street1 %>
31 | <% if (occupant.street2 && occupant.street2.length>0) { %>
32 | <%= occupant.street2 %>
33 | <% } %>
34 | <%= occupant.zipCode %> <%= occupant.city %>
35 |
36 |
37 |
Signature du locataire
38 | précédée de la mention (lu et approuvé) :
39 |
40 |
41 |
42 | <%- include partials/footer %>
43 |
44 | <% }); %>
45 | <%- include partials/printbar %>
46 |
47 | <%- include partials/scripts %>
48 |
--------------------------------------------------------------------------------
/backend/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const logger = require('winston');
3 | const apiV1 = require('./api');
4 | const auth = require('./auth');
5 | const page = require('./page');
6 | const pages = require('../pages');
7 |
8 | function _shouldBeLogged(req, res, next) {
9 | if (!req.session || !req.user) {
10 | return res.sendStatus(401);
11 | }
12 | next();
13 | }
14 |
15 | function _shouldBeLoggedThenRedirect(req, res, next) {
16 | if (!req.session || !req.user) {
17 | logger.info('redirect to /signin');
18 | return res.redirect('/signin');
19 | }
20 | next();
21 | }
22 |
23 | function _shouldNotBeLoggedThenRedirect(req, res, next) {
24 | if (req.session && req.user) {
25 | // TODO remove harcoded page dashboard
26 | logger.info('redirect to /dashboard');
27 | return res.redirect('/dashboard');
28 | }
29 | next();
30 | }
31 |
32 | module.exports = [
33 | // control route access
34 | () =>
35 | pages.restrictedList.reduce((router, pageDesc) => {
36 | const path = `/${pageDesc.id}${pageDesc.params || ''}`;
37 | router.use(path, _shouldBeLoggedThenRedirect);
38 | if (pageDesc.supportView) {
39 | router.use(`/view${path}`, _shouldBeLogged);
40 | }
41 | return router;
42 | }, express.Router()),
43 | () =>
44 | pages.publicList.reduce((router, pageDesc) => {
45 | const path = `/${pageDesc.id}${pageDesc.params || ''}`;
46 | router.use(path, _shouldNotBeLoggedThenRedirect);
47 | if (pageDesc.supportView) {
48 | router.use(`/view${path}`, _shouldNotBeLoggedThenRedirect);
49 | }
50 | return router;
51 | }, express.Router()),
52 | () => express.Router().use('/signedin', _shouldBeLogged),
53 | () => express.Router().use('/signedup', _shouldNotBeLoggedThenRedirect),
54 | () => express.Router().use('/signout', _shouldBeLogged),
55 | // add routes
56 | auth,
57 | apiV1,
58 | page,
59 | ];
60 |
--------------------------------------------------------------------------------
/backend/pages/signup/view/signup.ejs:
--------------------------------------------------------------------------------
1 |
37 |
--------------------------------------------------------------------------------
/backend/pages/occupant/view/contractdocumentsform.ejs:
--------------------------------------------------------------------------------
1 |
36 |
--------------------------------------------------------------------------------
/documentation/README.md:
--------------------------------------------------------------------------------
1 | # Loca
2 |
3 | Loca is a free and open-source web-based application that offers a veritable and user-friendly platform for owners and
4 | managers of buildings, flats, offices, meeting rooms, car parks, letter boxes...to make accurate financial and business decisions.
5 |
6 | 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.
7 |
8 | The idea is to make easy the management of properties and occupants with feature such as
9 |
10 | • Collect and keep all information of your properties and tenants securely in one place.
11 |
12 | • Create downloadable rental contracts and leasing agreements from templates available in the system.
13 |
14 | • Follow all rental payments month by month
15 |
16 | • Generate rental debt recovery letters from Templates for recovery unpaid rents by defaulting tenants… and so much more.
17 |
18 | ## How to contribute
19 |
20 | If the issue you report is not yet reported and related to the Loca core code, please report it on GitHub.
21 | Provide as much details as needed and include screenshots if possible..
22 |
23 | Fork the repository, edit and submit a pull request to branch that has latest version in development.
24 | Please be very clear on your commit messages and pull request, empty pull request messages are not accepted.
25 |
26 | Important!
27 |
28 | Issues that are not related to the core code (such as a third party extension or your server configuration) might be closed
29 | without explanation. You need to contact extension developer, use the forum or find a third partner to resolve a custom code issue.
30 |
31 | ## Making a suggestion
32 |
33 | We like improvements, but improvements are not bugs or issue. Please do not create an issue report if you think something needs
34 | improving (such as features or change to code standards etc).
35 |
--------------------------------------------------------------------------------
/backend/pages/occupant/view/rentoverview.ejs:
--------------------------------------------------------------------------------
1 |
2 |
<%= t('Rent information') %>
3 |
4 |
5 |
<%= t('Deposit') %>
6 |
-
7 |
8 |
9 |
13 |
17 |
18 |
22 |
26 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/backend/pages/print/view/paymentorder.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant) {
8 | occupant.rents.forEach(function(rent) {
9 | %>
10 |
11 |
12 | <%- include partials/header.ejs %>
13 |
14 |
15 |
Huissier
16 | Adresse
17 | Code Postal Ville
18 |
19 |
20 |
21 | <%- include partials/rentplaceanddate.ejs %>
22 |
25 |
26 | Objet : Demande de commandement de payer
27 |
28 |
29 |
30 |
31 | Madame, Monsieur,
32 |
33 |
34 |
35 |
36 | Suite à plusieurs impayés de loyer de la société <%=occupant.name%> , je vous joins les éléments suivants pour la rédaction d'un commandement de payer :
37 |
38 |
39 |
40 | Dernier avis d'échéance
41 | Copie du bail commercial
42 | Copie de la carte d'identité du gérant
43 | Extrait kbis de notre société
44 |
45 |
46 |
47 |
Nous vous prions d'agréer, Madame, Monsieur, l’expression de nos sincères salutations.
48 | <%- include partials/ownermanagersignature %>
49 |
50 |
51 | <%- include partials/footer %>
52 |
53 | <%
54 | });
55 | });
56 | %>
57 | <%- include partials/printbar %>
58 |
59 | <%- include partials/scripts %>
60 |
61 |
--------------------------------------------------------------------------------
/backend/pages/ejshelpers.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 |
3 | const _textToNumber = (text) => {
4 | let value = parseFloat(text);
5 | if (isNaN(value)) {
6 | value = 0;
7 | }
8 | return value;
9 | };
10 |
11 | module.exports = {
12 | formatSurface(text, hideUnit, emptyForZero) {
13 | const value = _textToNumber(text);
14 |
15 | if (value === 0 && emptyForZero) {
16 | return '';
17 | }
18 |
19 | if (hideUnit) {
20 | return this.Intl.NumberFormat.format(value);
21 | }
22 |
23 | return `${this.Intl.NumberFormat.format(value)} m2 `;
24 | },
25 |
26 | formatNumber(text, emptyForZero) {
27 | const value = _textToNumber(text);
28 |
29 | if (value === 0 && emptyForZero) {
30 | return '';
31 | }
32 |
33 | return this.Intl.NumberFormat.format(value);
34 | },
35 |
36 | formatMoney(text, hideCurrency, emptyForZero) {
37 | const value = _textToNumber(text);
38 |
39 | if (value === 0 && emptyForZero) {
40 | return '';
41 | }
42 |
43 | if (hideCurrency) {
44 | return this.Intl.NumberFormat.format(value);
45 | }
46 |
47 | return this.Intl.NumberFormatCurrency.format(value);
48 | },
49 |
50 | formatPercent(text, hidePercent, emptyForZero) {
51 | const value = _textToNumber(text);
52 |
53 | if (value === 0 && emptyForZero) {
54 | return '';
55 | }
56 |
57 | if (hidePercent) {
58 | return this.Intl.NumberFormat.format(value);
59 | }
60 |
61 | return this.Intl.NumberFormatPercent.format(value);
62 | },
63 |
64 | formatMonth(text) {
65 | return moment.months()[parseInt(text, 10) - 1];
66 | },
67 |
68 | formatMonthYear(month, year) {
69 | return moment.monthsShort()[parseInt(month, 10) - 1] + ' ' + year;
70 | },
71 |
72 | formatDate(text) {
73 | return moment(text, 'DD/MM/YYYY').format('L');
74 | },
75 |
76 | formatDateText(text) {
77 | return moment(text, 'DD/MM/YYYY').format('LL');
78 | },
79 |
80 | formatDateTime(text) {
81 | return moment(text, 'DD/MM/YYYY HH:MM').format('L LTS');
82 | },
83 | };
84 |
--------------------------------------------------------------------------------
/backend/businesslogic/FR/computeRent/tasks/1_base.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 |
3 | module.exports = function (
4 | contract,
5 | rentDate,
6 | previousRent,
7 | settlements,
8 | rent
9 | ) {
10 | const currentMoment = moment(rentDate, 'DD/MM/YYYY HH:mm');
11 | rent.term = Number(currentMoment.format('YYYYMMDDHH'));
12 | if (contract.frequency === 'months') {
13 | rent.term = Number(
14 | moment(currentMoment).startOf('month').format('YYYYMMDDHH')
15 | );
16 | }
17 | if (contract.frequency === 'days') {
18 | rent.term = Number(
19 | moment(currentMoment).startOf('day').format('YYYYMMDDHH')
20 | );
21 | }
22 | if (contract.frequency === 'hours') {
23 | rent.term = Number(
24 | moment(currentMoment).startOf('hour').format('YYYYMMDDHH')
25 | );
26 | }
27 | rent.month = currentMoment.month() + 1; // 0 based
28 | rent.year = currentMoment.year();
29 |
30 | contract.properties
31 | .filter((property) => {
32 | const entryMoment = moment(property.entryDate, 'DD/MM/YYYY').startOf(
33 | 'day'
34 | );
35 | const exitMoment = moment(property.exitDate, 'DD/MM/YYYY').endOf('day');
36 |
37 | return currentMoment.isBetween(
38 | entryMoment,
39 | exitMoment,
40 | contract.frequency,
41 | '[]'
42 | );
43 | })
44 | .forEach(function (property) {
45 | if (property.property) {
46 | const name = property.property.name || '';
47 | const preTaxAmount = property.rent || 0;
48 | const expenses = property.expenses || [];
49 |
50 | rent.preTaxAmounts.push({
51 | description: name,
52 | amount: preTaxAmount,
53 | });
54 |
55 | if (expenses.length) {
56 | rent.charges.push(
57 | ...expenses.map(({ title, amount }) => ({
58 | description: title,
59 | amount,
60 | }))
61 | );
62 | }
63 | }
64 | });
65 | if (settlements) {
66 | rent.description = settlements.description || '';
67 | }
68 | return rent;
69 | };
70 |
--------------------------------------------------------------------------------
/backend/pages/print/view/partials/invoicebody.ejs:
--------------------------------------------------------------------------------
1 |
2 | Description
3 | Quantité
4 | Prix Unitaire H.T.
5 | Total H.T.
6 |
7 |
8 | <% rent.preTaxAmounts.forEach(function(preTaxAmount) { %>
9 |
10 |
11 | <%= preTaxAmount.description %>
12 |
13 | 1
14 | <%= formatNumber(preTaxAmount.amount) %>
15 | <%= formatNumber(preTaxAmount.amount) %>
16 |
17 | <% }); %>
18 |
19 | <% rent.charges.forEach(function(charge) { %>
20 |
21 |
22 | <%= charge.description %>
23 |
24 | 1
25 | <%= formatNumber(charge.amount) %>
26 | <%= formatNumber(charge.amount) %>
27 |
28 | <% }); %>
29 |
30 | <% rent.discounts.forEach(function(discount) { %>
31 |
32 |
33 | <%= discount.description %>
34 |
35 | 1
36 | - <%= formatNumber(discount.amount) %>
37 | - <%= formatNumber(discount.amount) %>
38 |
39 | <% }); %>
40 |
41 | <% rent.debts.forEach(function(debt) { %>
42 |
43 |
44 | <%= debt.description %>
45 |
46 | 1
47 | <%= formatNumber(debt.amount) %>
48 | <%= formatNumber(debt.amount) %>
49 |
50 | <% }); %>
51 |
52 | <%
53 | var emptyRowCount = 9 - rent.preTaxAmounts.length - rent.charges.length - rent.discounts.length - rent.debts.length;
54 | for (var idx=0; idx
56 |
57 |
58 |
59 |
60 |
61 |
62 | <% } %>
--------------------------------------------------------------------------------
/backend/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const passport = require('passport');
3 | const passportLocal = require('passport-local');
4 | const logger = require('winston');
5 | const config = require('../../config');
6 | const loginManager = require('../managers/loginmanager');
7 |
8 | module.exports = function () {
9 | ////////////////////////////////////////////////////////////////////////////////
10 | // Set up passport
11 | ////////////////////////////////////////////////////////////////////////////////
12 | passport.use(
13 | new passportLocal.Strategy(
14 | {
15 | usernameField: 'email',
16 | passwordField: 'secretword',
17 | },
18 | (email, password, done) => {
19 | loginManager.authenticate(email, password, (err, user) => {
20 | if (err) {
21 | return done(null, false, { message: err });
22 | }
23 | return done(null, user);
24 | });
25 | }
26 | )
27 | );
28 |
29 | passport.serializeUser((user, done) => done(null, user.email));
30 | passport.deserializeUser(loginManager.getUserByEmail);
31 |
32 | ////////////////////////////////////////////////////////////////////////////////
33 | // Session routes
34 | ////////////////////////////////////////////////////////////////////////////////
35 | const router = express.Router();
36 | router.use(loginManager.updateRequestWithRealmsOfUser);
37 |
38 | if (config.signup) {
39 | router.post('/signup', loginManager.signup);
40 | router.post('/signedup', (req, res) => {
41 | res.redirect('/signin');
42 | });
43 | }
44 |
45 | if (config.demoMode) {
46 | router.get('/signin', loginManager.loginDemo);
47 | } else {
48 | router.post('/signin', loginManager.login);
49 | }
50 |
51 | router.all('/signedin', (req, res) => {
52 | // TODO remove harcoded page dashboard
53 | res.redirect('/dashboard');
54 | });
55 |
56 | router.get('/signout', (req, res) => {
57 | logger.info('sign out and redirect to /');
58 | req.session.destroy(() => res.redirect('/'));
59 | });
60 |
61 | return router;
62 | };
63 |
--------------------------------------------------------------------------------
/frontend/js/owner/middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import application from '../application';
3 | import ViewController from '../viewcontroller';
4 | import anilayout from '../lib/anilayout';
5 | import OwnerForm from './ownerform';
6 |
7 | class OwnerMiddleware extends ViewController {
8 | constructor() {
9 | super({
10 | domViewId: '#view-owner',
11 | });
12 | this.form = new OwnerForm();
13 | this.edited = false;
14 | }
15 |
16 | onDataChanged() {
17 | var data;
18 | this.form.bindForm();
19 | application.httpGet('/api/owner', (req, res) => {
20 | const owner = JSON.parse(res.responseText);
21 | this.form.setData(owner);
22 | data = this.form.getData();
23 | if (!this.edited) {
24 | if (data._id && data._id !== '') {
25 | this.closeForm();
26 | } else {
27 | this.openForm();
28 | }
29 | } else {
30 | this.openForm();
31 | }
32 | });
33 | }
34 |
35 | openForm() {
36 | this.edited = true;
37 | $('#owner-form select')
38 | .attr('readonly', false)
39 | .attr('disabled', false)
40 | .removeClass('uneditable-input');
41 | $('#owner-form input')
42 | .attr('readonly', false)
43 | .attr('disabled', false)
44 | .removeClass('uneditable-input');
45 | anilayout.showMenu('owner-form-menu');
46 | }
47 |
48 | closeForm() {
49 | this.edited = false;
50 | $('#owner-form select')
51 | .attr('readonly', true)
52 | .attr('disabled', true)
53 | .addClass('uneditable-input');
54 | $('#owner-form input')
55 | .attr('readonly', true)
56 | .attr('disabled', true)
57 | .addClass('uneditable-input');
58 | anilayout.showMenu('owner-menu');
59 | }
60 |
61 | onUserAction($action, actionId) {
62 | if (actionId === 'edit-owner') {
63 | this.openForm();
64 | } else if (actionId === 'save-form') {
65 | this.form.submit((/*errors*/) => {
66 | this.closeForm();
67 | });
68 | }
69 | }
70 |
71 | // overriden
72 | exited() {
73 | this.form.unbindForm();
74 | }
75 | }
76 |
77 | export default OwnerMiddleware;
78 |
--------------------------------------------------------------------------------
/frontend/js/lib/anilayout.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | const TRANSITION_DURATION_MENU = 200;
4 |
5 | class Anilayout {
6 | isMenuVisible(dataId) {
7 | dataId = dataId.startsWith('#') ? dataId.slice(1, dataId.length) : dataId;
8 | return $('.js-side-menu[data-id="' + dataId + '"]').hasClass('active');
9 | }
10 |
11 | showMenu(dataId, callback) {
12 | var $cardToSelect;
13 |
14 | function callbackEx() {
15 | if (callback) {
16 | callback();
17 | }
18 | }
19 |
20 | dataId = dataId.startsWith('#') ? dataId.slice(1, dataId.length) : dataId;
21 | $cardToSelect = $('.js-side-menu[data-id="' + dataId + '"]:hidden');
22 |
23 | if ($cardToSelect.length > 0) {
24 | this.hideMenu(function () {
25 | $cardToSelect.trigger('before-show-card');
26 | $cardToSelect.addClass('active').velocity('transition.bounceRightIn', {
27 | duration: TRANSITION_DURATION_MENU,
28 | complete: function () {
29 | callbackEx();
30 | $cardToSelect.trigger('after-show-card');
31 | },
32 | });
33 | });
34 | } else {
35 | callbackEx();
36 | }
37 | }
38 |
39 | hideMenu(callback) {
40 | var $activeCard = $('.js-side-menu.active').not(':hidden');
41 |
42 | function callbackEx() {
43 | if (callback) {
44 | callback();
45 | }
46 | }
47 |
48 | if ($activeCard.length > 0) {
49 | $activeCard.trigger('before-hide-card');
50 | $activeCard.removeClass('active');
51 | $activeCard.velocity('transition.bounceRightOut', {
52 | duration: TRANSITION_DURATION_MENU,
53 | complete: function () {
54 | callbackEx();
55 | $activeCard.trigger('after-hide-card');
56 | },
57 | });
58 | } else {
59 | callbackEx();
60 | }
61 | }
62 |
63 | showSheet(dataId) {
64 | dataId = dataId.startsWith('#') ? dataId.slice(1, dataId.length) : dataId;
65 | this.hideSheet();
66 | $('.js-sheet[data-id="' + dataId + '"]')
67 | .addClass('active')
68 | .show();
69 | }
70 |
71 | hideSheet() {
72 | $('.js-sheet.active').removeClass('active').hide();
73 | }
74 | }
75 |
76 | export default new Anilayout();
77 |
--------------------------------------------------------------------------------
/frontend/js/menu.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import application from './application';
3 |
4 | function uriFromId(id) {
5 | if (id === 'website') {
6 | return { nav: `/view/${id}`, history: '/' };
7 | }
8 | return { nav: `/view/${id}`, history: `/${id}` };
9 | }
10 |
11 | function resizeRightMenus() {
12 | const parentSize = $('#right-menu-pane').width();
13 | $('.js-side-menu').each(function () {
14 | const $activeMenu = $(this);
15 | const affixPadding = $activeMenu.innerWidth() - $activeMenu.width();
16 | $activeMenu.width(parentSize - affixPadding);
17 | });
18 | }
19 |
20 | export default () => {
21 | $(document).on('click', '.js-nav-action', function () {
22 | const viewId = $(this).data('id');
23 | const uri = uriFromId(viewId);
24 | application.httpGet({
25 | uri: uri.nav,
26 | history: {
27 | state: { viewId },
28 | title: viewId,
29 | uri: uri.history,
30 | },
31 | });
32 | $('.dropdown.open .dropdown-toggle').dropdown('toggle');
33 | return false;
34 | });
35 | $(document).on(
36 | 'click',
37 | '.navbar-collapse.collapse.in a:not(.dropdown-toggle)',
38 | function () {
39 | $(this).closest('.navbar-collapse').collapse('hide');
40 | }
41 | );
42 | $(document).on(
43 | 'click',
44 | '.navbar-collapse.collapse.in button:not(.navbar-toggle)',
45 | function () {
46 | $(this).closest('.navbar-collapse').collapse('hide');
47 | }
48 | );
49 |
50 | // affix management for js-side-menu
51 | $(document).on('before-show-card', '.js-side-menu', function () {
52 | resizeRightMenus();
53 | $(this).affix({ offset: { top: 0 } });
54 | });
55 |
56 | $(document).on('after-show-card', '.js-side-menu', function () {
57 | $(this).affix('checkPosition');
58 | });
59 |
60 | $(document).on('before-hide-card', '.js-side-menu', function () {
61 | $(window).off('.affix');
62 | $(this).removeData('bs.affix').removeClass('affix affix-top affix-bottom');
63 | });
64 |
65 | $(document).on('affix.bs.affix', '.js-side-menu', function () {
66 | const $menu = $(this);
67 | $menu.width($menu.width());
68 | });
69 |
70 | $(window).resize(() => resizeRightMenus());
71 | };
72 |
--------------------------------------------------------------------------------
/frontend/js/connection_middleware.js:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 | import frontexpress from 'frontexpress';
3 | import bootbox from 'bootbox';
4 |
5 | class ConnectionMiddleware extends frontexpress.Middleware {
6 | entered() {
7 | $('#waitwindow').show();
8 | }
9 |
10 | updated() {
11 | $('#waitwindow').hide();
12 | }
13 |
14 | exited() {
15 | $('#waitwindow').hide();
16 | }
17 |
18 | failed(request, response) {
19 | const errors = [];
20 | let needAuthentication = false;
21 | if (response.status === 0) {
22 | errors.push(
23 | i18next.t('Server access problem. Check your network connection')
24 | );
25 | } else if (response.status == 401) {
26 | needAuthentication = true;
27 | errors.push(i18next.t('Your session has expired, Please reconnect'));
28 | errors.push(i18next.t('[code: ]', { code: 401 }));
29 | } else if (response.status == 404) {
30 | errors.push(i18next.t('Page not found on server'));
31 | errors.push(i18next.t('[code: ]', { code: 404 }));
32 | } else if (response.status == 500) {
33 | errors.push(i18next.t('Internal server error'));
34 | errors.push(i18next.t('[code: ]', { code: 500 }));
35 | } else if (response.errorThrown === 'parsererror') {
36 | errors.push(i18next.t('Problem during data decoding [JSON]'));
37 | } else if (response.errorThrown === 'timeout') {
38 | errors.push(i18next.t('Server is taking too long to reply'));
39 | } else if (response.errorThrown === 'abort') {
40 | errors.push(i18next.t('Request cancelled on server'));
41 | } else {
42 | errors.push(i18next.t('Unknown error'));
43 | errors.push(response.statusText);
44 | }
45 |
46 | bootbox.hideAll();
47 | $('#waitwindow').hide();
48 | bootbox.alert({ title: i18next.t('Uh-oh!'), message: errors });
49 |
50 | if (needAuthentication) {
51 | setTimeout(function () {
52 | window.location.replace(
53 | location.protocol +
54 | '//' +
55 | location.hostname +
56 | (location.port ? ':' + location.port : '') +
57 | '/signin'
58 | );
59 | }, 2000);
60 | }
61 | }
62 | }
63 |
64 | export default ConnectionMiddleware;
65 |
--------------------------------------------------------------------------------
/backend/managers/templatemanager.js:
--------------------------------------------------------------------------------
1 | const config = require('../../config');
2 | const axios = require('axios');
3 |
4 | //TODO: if no added value to have the api in loca then move that in nginx config
5 |
6 | const pdfGeneratorUrl = `${config.PDFGENERATOR_URL}/templates`;
7 |
8 | ////////////////////////////////////////////////////////////////////////////////
9 | // Exported functions
10 | ////////////////////////////////////////////////////////////////////////////////
11 | const all = async (req, res) => {
12 | const { language } = req;
13 |
14 | const response = await axios.get(pdfGeneratorUrl, {
15 | headers: {
16 | organizationId: req.headers.organizationid,
17 | 'Accept-Language': language,
18 | },
19 | });
20 |
21 | res.json(response.data);
22 | };
23 |
24 | const one = async (req, res) => {
25 | const { language } = req;
26 | const { id } = req.params;
27 |
28 | const response = await axios.get(`${pdfGeneratorUrl}/${id}`, {
29 | headers: {
30 | organizationId: req.headers.organizationid,
31 | 'Accept-Language': language,
32 | },
33 | });
34 |
35 | res.json(response.data);
36 | };
37 |
38 | const add = async (req, res) => {
39 | const { language } = req;
40 |
41 | const response = await axios.post(pdfGeneratorUrl, req.body, {
42 | headers: {
43 | organizationId: req.headers.organizationid,
44 | 'Accept-Language': language,
45 | },
46 | });
47 |
48 | res.json(response.data);
49 | };
50 |
51 | const update = async (req, res) => {
52 | const { language } = req;
53 |
54 | const response = await axios.put(pdfGeneratorUrl, req.body, {
55 | headers: {
56 | organizationId: req.headers.organizationid,
57 | 'Accept-Language': language,
58 | },
59 | });
60 |
61 | res.json(response.data);
62 | };
63 |
64 | const remove = async (req, res) => {
65 | const { language } = req;
66 | const { id } = req.params;
67 |
68 | const response = await axios.delete(`${pdfGeneratorUrl}/${id}`, {
69 | headers: {
70 | organizationId: req.headers.organizationid,
71 | 'Accept-Language': language,
72 | },
73 | });
74 |
75 | res.json(response.data);
76 | };
77 |
78 | module.exports = {
79 | all,
80 | one,
81 | add,
82 | update,
83 | remove,
84 | };
85 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const sugar = require('sugar');
3 |
4 | const toBoolean = (value) => {
5 | if (value && typeof value !== 'boolean') {
6 | value = value.toLowerCase() === 'true';
7 | }
8 | return value;
9 | };
10 |
11 | const loggerLevel =
12 | process.env.LOCA_LOGGER_LEVEL || process.env.LOGGER_LEVEL || 'debug';
13 | const nginxPort = process.env.NGINX_PORT || 8080;
14 | const appHttpPort = process.env.LOCA_NODEJS_PORT || process.env.PORT || 8080;
15 | const configDir =
16 | process.env.LOCA_CONFIG_DIR ||
17 | process.env.CONFIG_DIR ||
18 | path.join(__dirname, '..', 'config');
19 | const demoMode = toBoolean(
20 | process.env.LOCA_DEMOMODE || process.env.DEMO_MODE || true
21 | );
22 | const restoreDatabase = toBoolean(process.env.RESTORE_DB || true);
23 | const signup = toBoolean(
24 | process.env.LOCA_PRODUCTIVE || process.env.SIGNUP || false
25 | );
26 |
27 | const website = require(path.join(configDir, 'website.json'));
28 |
29 | const config = {
30 | ...website,
31 | loggerLevel,
32 | nginxPort,
33 | appHttpPort,
34 | configDir,
35 | businesslogic: 'FR',
36 | productive: process.env.NODE_ENV === 'production',
37 | signup,
38 | restoreDatabase,
39 | demoMode,
40 | database:
41 | process.env.MONGO_URL ||
42 | process.env.LOCA_DBNAME ||
43 | process.env.BASE_DB_URL ||
44 | 'mongodb://localhost/demodb',
45 | EMAILER_URL: process.env.EMAILER_URL || 'http://localhost:8083/emailer',
46 | PDFGENERATOR_URL:
47 | process.env.PDFGENERATOR_URL || 'http://localhost:8082/pdfgenerator',
48 | ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET || 'access_token_secret',
49 | REFRESH_TOKEN_SECRET:
50 | process.env.REFRESH_TOKEN_SECRET || 'refresh_token_secret',
51 | CIPHER_KEY: process.env.CIPHER_KEY || 'cipher_key_secret',
52 | CIPHER_IV_KEY: process.env.CIPHER_IV_KEY || 'cipher_iv_key_secret',
53 | };
54 |
55 | module.exports = {
56 | ...config,
57 | log: () => {
58 | const escapedConfig = sugar.Object.clone(config);
59 | escapedConfig.ACCESS_TOKEN_SECRET = '****';
60 | escapedConfig.REFRESH_TOKEN_SECRET = '****';
61 | escapedConfig.CIPHER_KEY = '****';
62 | escapedConfig.CIPHER_IV_KEY = '****';
63 | return JSON.stringify(escapedConfig, null, 1);
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/.github/workflows/dockerpublish.yml:
--------------------------------------------------------------------------------
1 | name: Docker
2 |
3 | on:
4 | push:
5 | # Publish `master` as Docker `latest` image.
6 | branches:
7 | - master
8 |
9 | # Publish `v1.2.3` tags as releases.
10 | tags:
11 | - v*
12 |
13 | # Run tests for any PRs.
14 | pull_request:
15 |
16 | env:
17 | IMAGE_NAME: loca
18 |
19 | jobs:
20 | # Run tests.
21 | # See also https://docs.docker.com/docker-hub/builds/automated-testing/
22 | test:
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 | - uses: actions/checkout@v2
27 |
28 | - name: Run tests
29 | run: |
30 | if [ -f docker-compose.test.yml ]; then
31 | docker-compose --file docker-compose.test.yml build
32 | docker-compose --file docker-compose.test.yml run sut
33 | else
34 | docker build . --file Dockerfile
35 | fi
36 |
37 | # Push image to GitHub Packages.
38 | # See also https://docs.docker.com/docker-hub/builds/
39 | push:
40 | # Ensure test job passes before pushing image.
41 | needs: test
42 |
43 | runs-on: ubuntu-latest
44 | if: github.event_name == 'push'
45 |
46 | steps:
47 | - uses: actions/checkout@v2
48 |
49 | - name: Build image
50 | run: docker build . --file Dockerfile --tag image
51 |
52 | - name: Log into registry
53 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
54 |
55 | - name: Push image
56 | run: |
57 | IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
58 |
59 | # Change all uppercase to lowercase
60 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
61 |
62 | # Strip git ref prefix from version
63 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
64 |
65 | # Strip "v" prefix from tag name
66 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
67 |
68 | # Use Docker `latest` tag convention
69 | [ "$VERSION" == "master" ] && VERSION=latest
70 |
71 | echo IMAGE_ID=$IMAGE_ID
72 | echo VERSION=$VERSION
73 |
74 | docker tag image $IMAGE_ID:$VERSION
75 | docker push $IMAGE_ID:$VERSION
76 |
--------------------------------------------------------------------------------
/backend/pages/print/view/guarantyrequest.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant, index) {
8 | %>
9 |
10 |
11 | <%- include partials/header.ejs %>
12 |
13 | <%- include partials/customeraddress.ejs %>
14 |
15 |
16 | <%- include partials/placeanddate.ejs %>
17 |
18 |
19 | Lettre RAR n°
20 | Copie lettre simple
21 |
22 |
23 |
24 | Objet : Paiement de dépôt de garantie
25 |
26 |
27 |
28 |
29 | Madame, Monsieur,
30 |
31 |
32 |
33 |
34 | Nous avons conclu ensemble un contrat de bail portant sur la location de locaux situés :
35 |
36 |
37 | <%= occupant.street1 %>
38 | <% if (occupant.street2 && occupant.street2.length>0) { %>
39 | <%= occupant.street2 %>
40 | <% } %>
41 | <%= occupant.zipCode %> <%= occupant.city %>
42 |
43 |
44 | Conformément au paragraphe "DEPOT DE GARANTIE" de ce contrat, nous vous demandons de bien vouloir nous faire parvenir par retour de courrier le réglement de votre dếpôt de garantie correspondant au montant de euros.
45 |
46 |
47 |
48 | Dans cette attente, veuillez agréer, Madame, Monsieur , l'assurance de nos sentiments distingués.
49 |
50 | <%- include partials/ownermanagersignature %>
51 |
52 |
53 | <%- include partials/footer %>
54 |
55 |
56 | <% }); %>
57 | <%- include partials/printbar %>
58 |
59 | <%- include partials/scripts %>
60 |
--------------------------------------------------------------------------------
/backend/models/objectfilter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const sugar = require('sugar');
3 | const logger = require('winston');
4 | sugar.extend();
5 |
6 | module.exports = class ObjectFilter {
7 | constructor(schema) {
8 | this.schema = schema;
9 | }
10 |
11 | filter(data) {
12 | return Object.keys(this.schema).reduce((filteredData, key) => {
13 | const type = this.schema[key];
14 | const value = data[key];
15 |
16 | if (typeof value != 'undefined') {
17 | if (type === Boolean) {
18 | if (
19 | typeof value == 'string' &&
20 | (value === 'true' || value === 'false')
21 | ) {
22 | filteredData[key] = value === 'true';
23 | } else if (typeof value == 'boolean') {
24 | filteredData[key] = value;
25 | }
26 | } else if (type === Number) {
27 | let number = value;
28 | if (typeof value == 'string') {
29 | number = Number(value.replace(',', '.'));
30 | }
31 | if (!isNaN(number)) {
32 | filteredData[key] = number;
33 | } else {
34 | filteredData[key] = 0;
35 | }
36 | } else if (type === Array) {
37 | if (Array.isArray(value)) {
38 | filteredData[key] = value;
39 | }
40 | } else if (type === Object) {
41 | if (typeof value == 'object') {
42 | filteredData[key] = value;
43 | }
44 | } else if (type === String) {
45 | if (key === '_id' && typeof value == 'object') {
46 | filteredData[key] = value.toString();
47 | } else if (typeof value == 'string') {
48 | filteredData[key] = value;
49 | }
50 | } else {
51 | logger.error(
52 | 'type unsupported ' +
53 | type +
54 | ' for schema ' +
55 | JSON.stringify(this.schema)
56 | );
57 | //throw new Error('Cannot valid schema type unsupported ' + type);
58 | }
59 | } else {
60 | logger.silly(
61 | 'undefined value for key ' +
62 | key +
63 | ' of schema ' +
64 | JSON.stringify(this.schema)
65 | );
66 | }
67 | return filteredData;
68 | }, {});
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/backend/managers/documentmanager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const moment = require('moment');
4 | const config = require('../../config');
5 | const FD = require('./frontdata');
6 | const occupantModel = require('../models/occupant');
7 | const documentModel = require('../models/document');
8 | const axios = require('axios');
9 |
10 | ////////////////////////////////////////////////////////////////////////////////
11 | // Exported functions
12 | ////////////////////////////////////////////////////////////////////////////////
13 | const get = async (req, res) => {
14 | const { language } = req;
15 | const { document, id, term } = req.params;
16 | let url = `${config.PDFGENERATOR_URL}/${document}/${id}`;
17 | if (term) {
18 | url = `${url}/${term}`;
19 | }
20 |
21 | const response = await axios.get(url, {
22 | responseType: 'stream',
23 | headers: {
24 | organizationId: req.headers.organizationid || String(req.realm._id),
25 | 'Accept-Language': language,
26 | },
27 | });
28 |
29 | response.data.pipe(res);
30 | };
31 |
32 | const update = (req, res) => {
33 | const realm = req.realm;
34 | const occupant = documentModel.schema.filter(req.body);
35 |
36 | if (!occupant.documents) {
37 | occupant.documents = [];
38 | }
39 |
40 | occupantModel.findOne(realm, occupant._id, (errors, dbOccupant) => {
41 | if (errors) {
42 | res.json({
43 | errors: errors,
44 | });
45 | return;
46 | }
47 |
48 | dbOccupant.documents = [];
49 |
50 | occupant.documents.forEach((document) => {
51 | const momentExpirationDate = moment(
52 | document.expirationDate,
53 | 'DD/MM/YYYY'
54 | ).endOf('day');
55 | if (
56 | document.name &&
57 | document.name.trim() !== '' &&
58 | momentExpirationDate.isValid()
59 | ) {
60 | document.expirationDate = momentExpirationDate.toDate();
61 | dbOccupant.documents.push(document);
62 | }
63 | });
64 |
65 | occupantModel.update(realm, dbOccupant, (errors) => {
66 | if (errors) {
67 | res.json({
68 | errors: errors,
69 | });
70 | return;
71 | }
72 | res.json(FD.toOccupantData(dbOccupant));
73 | });
74 | });
75 | };
76 |
77 | module.exports = {
78 | get,
79 | update,
80 | };
81 |
--------------------------------------------------------------------------------
/backend/pages/occupant/view/templates.ejs:
--------------------------------------------------------------------------------
1 |
2 |
41 |
42 |
47 |
48 |
56 |
57 |
--------------------------------------------------------------------------------
/backend/pages/print/view/insurance.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <% occupants.forEach(function(occupant, index) { %>
7 |
8 |
9 | <%- include partials/header.ejs %>
10 |
11 | <%- include partials/customeraddress.ejs %>
12 |
13 | <%- include partials/placeanddate.ejs %>
14 |
15 |
16 | Lettre RAR n°
17 | Copie lettre simple
18 |
19 |
20 |
21 | Objet : Demande d’attestation d’assurance pour l'année <%= year %>
22 |
23 |
24 |
25 |
26 | Madame, Monsieur,
27 |
28 |
29 |
30 |
31 | Nous avons conclu ensemble un contrat de bail portant sur la location de locaux situés :
32 |
33 |
34 | <%= occupant.street1 %>
35 | <% if (occupant.street2 && occupant.street2.length>0) { %>
36 | <%= occupant.street2 %>
37 | <% } %>
38 | <%= occupant.zipCode %> <%= occupant.city %>
39 |
40 |
41 | Conformément à l’article 19 du paragraphe "CHARGES ET CONDITIONS" de ce contrat, nous vous demandons de bien vouloir nous faire parvenir par retour de courrier une attestation d’assurances couvrant les risques locatifs de vos locaux pour l’année <%= year %> .
42 |
43 |
44 | Nous vous rappelons que vous êtes dans l'obligation légale de vous couvrir contre les risques dont vous avez à répondre en qualité de locataire, aussi nous vous remercions de bien vouloir nous communiquer dans les meilleurs délais les pièces justifiant de cette assurance.
45 |
46 |
47 |
48 | Dans cette attente, veuillez agréer, Madame, Monsieur , l'assurance de nos sentiments distingués.
49 |
50 | <%- include partials/ownermanagersignature %>
51 |
52 |
53 | <%- include partials/footer %>
54 |
55 | <% }); %>
56 | <%- include partials/printbar %>
57 |
58 | <%- include partials/scripts %>
59 |
--------------------------------------------------------------------------------
/documentation/content home page:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | For Property Managers, By Property Managers …
5 |
6 | Welcome To The World Of Loca
7 |
8 | A Simple Web-based, Open Source Management Application Created Specifically for Rental Property Owners, Landlords and Managers
9 |
10 | Try out Our Demo Here
11 |
12 | Loca is a simple but professional, user-friendly and eye-catching Rental Property Management Application.
13 |
14 | Rental Property Management, The Stress-Free Way
15 | It is designed to help you make sound rental property management decisions and control your rental property expenses effectively.
16 |
17 | Best Of All, It’s Totally Free To Use… No Hidden Charges, No Cutthroat upfront fees, Nothing!
18 |
19 | Growing Your Rental Businesses Has Never Been This Easy…
20 | Whether it’s a single property or a multiple properties, Loca helps you get it done.
21 |
22 | Try out Our Demo Here
23 |
24 | So What Make Loca So Special Anyway?
25 | • Secure Online Cloud based Solution.
26 | • No need for any software download, backup or update.
27 | • Smart, user-friendly layout. No need to waste too much time learning the system.
28 | • It is open-source… therefore it is totally free to use for all properties. No upfront costs.
29 | • Created by experienced rental property managers, so your rental property management needs are in good hands.
30 |
31 | Try Out Our Demo Here
32 |
33 | Switch Over To Loca Now And Enjoy The Endless Loca Advantages…
34 |
35 | Rent
36 | • Track rent payments as they come in.
37 | • Collect rent payments online.
38 | • Set up late payment remainder emails for defaulting tenants.
39 | • Get notified immediately the rents get paid.
40 | • Get a detailed tenant timeline and payment history.
41 | • So much more…
42 |
43 | Documentation
44 | • Create beautiful, professional rental invoices or lease contracts in a few minutes.
45 | • Keep accurate records of your tenants repair requests or complaints’
46 | • Notify them immediately by email or sms when the issue is resolved.
47 | • Store all crucial rental documents, tenancy info and other important files in our cloud-based digital archives and view or retrieve them via downloads at any time.
48 | • So much more…
49 |
50 | Accounting
51 | • Easily setup, read and update accounting reports, especially for taxation purposes.
52 | • Create monitoring reports for businesses alongside maintenance expenses.
53 | • Reduce the stressful need for endless and costly phone calls or paperwork.
54 | • So much more…
55 |
56 | Starting Now, Be In Absolute Control Of Your Rental Property Cashflow… No Matter Where You Are.
57 | Try Out Our Demo First.
58 |
--------------------------------------------------------------------------------
/backend/pages/index.ejs:
--------------------------------------------------------------------------------
1 | <% var min = config.productive ? '.min' : ''; %>
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%=config.website.productName%> <%=config.website.productSlogan%>
11 |
12 | <%- include metatags.ejs %>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | <%- include header.ejs %>
22 |
23 |
24 | <%- include(view+'/view/index') %>
25 |
26 |
27 |
28 | <%= t('Loading') %>...
29 |
30 |
31 | <%- include footer.ejs %>
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/backend/managers/ownermanager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const realmModel = require('../models/realm');
4 |
5 | ////////////////////////////////////////////////////////////////////////////////
6 | // Exported functions
7 | ////////////////////////////////////////////////////////////////////////////////
8 | function all(req, res) {
9 | const realm = req.realm;
10 | realmModel.findOne(realm._id, (errors, dbRealm) => {
11 | if (errors && errors.length > 0) {
12 | res.json({
13 | errors: errors,
14 | });
15 | return;
16 | }
17 | res.json({
18 | _id: dbRealm._id,
19 | isCompany: dbRealm.isCompany,
20 | company: dbRealm.companyInfo.name,
21 | legalForm: dbRealm.companyInfo.legalStructure,
22 | capital: dbRealm.companyInfo.capital,
23 | siret: dbRealm.companyInfo.ein,
24 | dos: dbRealm.companyInfo.dos,
25 | vatNumber: dbRealm.companyInfo.vatNumber,
26 | manager: dbRealm.companyInfo.legalRepresentative,
27 | street1: dbRealm.addresses[0].street1,
28 | street2: dbRealm.addresses[0].street2,
29 | zipCode: dbRealm.addresses[0].zipCode,
30 | city: dbRealm.addresses[0].city,
31 | state: dbRealm.addresses[0].state,
32 | country: dbRealm.addresses[0].country,
33 | contact: dbRealm.contacts[0].name,
34 | email: dbRealm.contacts[0].email,
35 | phone1: dbRealm.contacts[0].phone1,
36 | phone2: dbRealm.contacts[0].phone2,
37 | bank: dbRealm.bankInfo.name,
38 | rib: dbRealm.bankInfo.iban,
39 | });
40 | });
41 | }
42 |
43 | function update(req, res) {
44 | const realm = req.realm;
45 | const owner = req.body;
46 |
47 | realm.isCompany = owner.isCompany;
48 | realm.companyInfo = {
49 | name: owner.company,
50 | legalStructure: owner.legalForm,
51 | capital: owner.capital,
52 | ein: owner.siret,
53 | dos: owner.dos,
54 | vatNumber: owner.vatNumber,
55 | legalRepresentative: owner.manager,
56 | };
57 |
58 | realm.addresses = [
59 | {
60 | street1: owner.street1,
61 | street2: owner.street2,
62 | zipCode: owner.zipCode,
63 | city: owner.city,
64 | state: owner.state,
65 | country: owner.country,
66 | },
67 | ];
68 |
69 | realm.contacts = [
70 | {
71 | name: owner.contact,
72 | email: owner.email,
73 | phone1: owner.phone1,
74 | phone2: owner.phone2,
75 | },
76 | ];
77 |
78 | realm.bankInfo = {
79 | name: owner.bank,
80 | iban: owner.rib,
81 | };
82 |
83 | realmModel.update(realmModel.schema.filter(realm), (errors) => {
84 | res.json({ errors: errors });
85 | });
86 | }
87 |
88 | module.exports = {
89 | all,
90 | update,
91 | };
92 |
--------------------------------------------------------------------------------
/backend/pages/property/view/templates.ejs:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
44 |
45 |
60 |
61 |
--------------------------------------------------------------------------------
/backend/pages/header.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | <% if (isLogged) { %>
7 |
15 |
37 | <% } %>
38 |
39 |
40 |
--------------------------------------------------------------------------------
/backend/pages/print/view/recovery3.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include partials/htmlhead.ejs %>
4 |
5 |
6 | <%
7 | occupants.forEach(function(occupant) {
8 | occupant.rents.forEach(function(rent) {
9 | %>
10 |
11 |
12 | <%- include partials/header.ejs %>
13 |
14 | <%- include partials/customeraddress.ejs %>
15 |
16 | <%- include partials/rentplaceanddate.ejs %>
17 |
18 |
19 | Lettre RAR n°
20 | Copie lettre simple
21 |
22 |
23 | Objet : Demande de règlement sous peine de rupture du bail commercial
24 |
25 |
26 |
27 |
28 | Madame, Monsieur,
29 |
30 |
31 |
32 |
33 | Nous vous rappelons que nos relations commerciales avec votre société sont liées par un bail signé le <%= formatDateText(occupant.beginDate) %>.
34 |
35 |
36 | Dans ce bail, il est explicitement indiqué que <%=occupant.name%> s'oblige à payer d'avance, la location d'un bureau, à <%= realm.companyInfo.name + ' ' + realm.companyInfo.legalStructure %> tous les mois, le premier jour de chaque mois (chapitre Loyers - article II)
37 |
38 | Or, à ce jour, plusieurs loyers n'ont pas été réglés représentant un montant de <%= formatMoney(rent.total.grandTotal) %> TTC.
39 |
40 |
41 | Nous vous informons que votre dossier sera transmit à notre huissier de justice afin de faire respecter la clause résolutoire (« Il est convenu qu'à défaut par le preneur d'exécuter une seule des charges et conditions du bail - qui sont toutes de rigueur - ou de payer exactement un seul terme de loyer et charges ou accessoires à son échéance, le présent bail sera, si bon semble au Bailleur, résilié de plein droit et sans aucune formalité judiciaire, un mois après une simple mise en demeure d'exécuter ou un simple commandement de payer signifié à personne ou à domicile, contenant déclaration par le Bailleur de son intention d'user de la présente clause et demeuré sans effet pendant ce délai. » ).
42 |
43 |
44 |
Nous vous prions d'agréer, Madame, Monsieur, l’expression de nos sentiments les meilleurs.
45 |
46 | <%- include partials/ownermanagersignature %>
47 |
48 |
49 | <%- include partials/footer %>
50 |
51 | <%
52 | });
53 | });
54 | %>
55 | <%- include partials/printbar %>
56 |
57 | <%- include partials/scripts %>
58 |
59 |
--------------------------------------------------------------------------------
/frontend/js/index.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import i18next from 'i18next';
3 | import bootbox from 'bootbox';
4 | import application from './application';
5 | import FV from './formvalidators';
6 | import language from './language';
7 | import menu from './menu';
8 | import ConnectionMiddleware from './connection_middleware';
9 | import MenuMiddleware from './menu_middleware';
10 | import WebsiteMiddleware from './website/middleware';
11 | import SignupMiddleware from './signup/middleware';
12 | import LoginMiddleware from './login/middleware';
13 | import ViewMiddleware from './view_middleware';
14 | import DashboardMiddleware from './dashboard/middleware';
15 | import RentMiddleware from './rent/middleware';
16 | import OccupantMiddleware from './occupant/middleware';
17 | import PropertyMiddleware from './property/middleware';
18 | import AccountingMiddleware from './accounting/middleware';
19 | import OwnerMiddleware from './owner/middleware';
20 | import SelectRealmMiddleware from './selectrealm/middleware';
21 |
22 | const LOCA = application.get('LOCA');
23 |
24 | ///////////////////////////////////////////////////////////////////////////////
25 | // routes
26 | ///////////////////////////////////////////////////////////////////////////////
27 | const connectionMiddleware = new ConnectionMiddleware();
28 | application.get(connectionMiddleware);
29 | application.post(connectionMiddleware);
30 | application.get(new MenuMiddleware());
31 | application.get(/^\/view\//, new ViewMiddleware());
32 | [
33 | { id: 'website', Middleware: WebsiteMiddleware },
34 | { id: 'signup', Middleware: SignupMiddleware },
35 | { id: 'signin', Middleware: LoginMiddleware },
36 | { id: 'dashboard', Middleware: DashboardMiddleware },
37 | { id: 'rent', Middleware: RentMiddleware },
38 | { id: 'occupant', Middleware: OccupantMiddleware },
39 | { id: 'property', Middleware: PropertyMiddleware },
40 | { id: 'accounting', Middleware: AccountingMiddleware },
41 | { id: 'selectrealm', Middleware: SelectRealmMiddleware },
42 | { id: 'owner', Middleware: OwnerMiddleware },
43 | ].forEach((page) => {
44 | const middleware = new page.Middleware();
45 | application.get(`/${page.id !== 'website' ? page.id : ''}`, middleware);
46 | application.get(`/view/${page.id}`, middleware);
47 | });
48 |
49 | ///////////////////////////////////////////////////////////////////////////////
50 | // launch application
51 | ///////////////////////////////////////////////////////////////////////////////
52 | language(LOCA.countryCode, (countryCode) => {
53 | LOCA.countryCode = countryCode;
54 | application.listen();
55 |
56 | // init form validators
57 | FV();
58 |
59 | // init menu
60 | menu();
61 |
62 | // display reset data message
63 | const $demoPopover = $('#demo-popover');
64 | if ($demoPopover.length) {
65 | bootbox.alert({
66 | message: i18next.t('Site data is reset every 30 minutes'),
67 | });
68 | }
69 | });
70 |
--------------------------------------------------------------------------------
/backend/models/model.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const logger = require('winston');
3 | const db = require('./db');
4 |
5 | module.exports = class Model {
6 | constructor(collection) {
7 | this.collection = collection;
8 | db.addCollection(collection);
9 | }
10 |
11 | findOne(realm, id, callback) {
12 | db.findItemById(realm, this.collection, id, (errors, dbItems) => {
13 | if (errors && errors.length > 0) {
14 | callback(errors);
15 | return;
16 | }
17 |
18 | const item = dbItems && dbItems.length > 0 ? dbItems[0] : null;
19 | callback(null, this.schema ? this.schema.filter(item) : item);
20 | });
21 | }
22 |
23 | findAll(realm, callback) {
24 | this.findFilter(realm, {}, callback);
25 | }
26 |
27 | findFilter(realm, filter, callback) {
28 | db.listWithFilter(realm, this.collection, filter, (errors, dbItems) => {
29 | if (errors && errors.length > 0) {
30 | callback(errors);
31 | return;
32 | }
33 | const items = dbItems || [];
34 | if (this.schema) {
35 | items.forEach((item, index) => {
36 | items[index] = this.schema.filter(item);
37 | });
38 | }
39 | callback(null, items);
40 | });
41 | }
42 |
43 | upsert(realm, query, fieldsToSet, fieldsToSetOnInsert, callback) {
44 | const updateSchema = this.updateSchema || this.schema;
45 |
46 | if (!updateSchema.exists(fieldsToSet)) {
47 | logger.error('cannot update', this.collection, fieldsToSet, 'not valid');
48 | callback(['cannot update database fields not valid']);
49 | return;
50 | }
51 |
52 | db.upsert(
53 | realm,
54 | this.collection,
55 | query,
56 | fieldsToSet,
57 | this.schema.filter(fieldsToSetOnInsert),
58 | (errors) => {
59 | if (errors && errors.length > 0) {
60 | callback(errors);
61 | return;
62 | }
63 | callback(null);
64 | }
65 | );
66 | }
67 |
68 | update(realm, item, callback) {
69 | const updateSchema = this.updateSchema || this.schema;
70 | const itemToUpdate = updateSchema.filter(item);
71 | db.update(realm, this.collection, itemToUpdate, (errors, dbItem) => {
72 | if (errors && errors.length > 0) {
73 | return callback(errors);
74 | }
75 | callback(null, this.schema.filter(dbItem));
76 | });
77 | }
78 |
79 | add(realm, item, callback) {
80 | const addSchema = this.addSchema || this.schema;
81 | const itemToAdd = addSchema.filter(item);
82 | db.add(realm, this.collection, itemToAdd, (errors, dbItem) => {
83 | if (errors && errors.length > 0) {
84 | callback(errors);
85 | return;
86 | }
87 | callback(null, this.schema.filter(dbItem));
88 | });
89 | }
90 |
91 | remove(realm, ids, callback) {
92 | db.remove(realm, this.collection, ids, (errors) => {
93 | callback(errors && errors.length > 0 ? errors : null);
94 | });
95 | }
96 | };
97 |
--------------------------------------------------------------------------------
/frontend/less/bootswatch.less:
--------------------------------------------------------------------------------
1 | // Cerulean 3.3.2
2 | // Bootswatch
3 | // -----------------------------------------------------
4 |
5 | .btn-shadow(@color) {
6 | #gradient > .vertical-three-colors(lighten(@color, 8%), @color, 60%, darken(@color, 4%));;
7 | filter: none;
8 | border-bottom: 1px solid darken(@color, 10%);
9 | }
10 |
11 | // Navbar =====================================================================
12 |
13 | .navbar {
14 | .btn-shadow(@navbar-default-bg);
15 | filter: none;
16 | .box-shadow(0 1px 10px rgba(0, 0, 0, 0.1));
17 |
18 | &-default {
19 | .badge {
20 | background-color: #fff;
21 | color: @navbar-default-bg;
22 | }
23 | }
24 |
25 | &-inverse {
26 | #gradient > .vertical-three-colors(lighten(@navbar-inverse-bg, 8%), lighten(@navbar-inverse-bg, 4%), 60%, darken(@navbar-inverse-bg, 2%));;
27 | filter: none;
28 | border-bottom: 1px solid darken(@navbar-inverse-bg, 10%);
29 |
30 | .badge {
31 | background-color: #fff;
32 | color: @navbar-inverse-bg;
33 | }
34 | }
35 |
36 | .navbar-nav > li > a,
37 | &-brand {
38 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
39 | }
40 | }
41 |
42 | @media (max-width: @grid-float-breakpoint-max) {
43 | .navbar {
44 | .dropdown-header {
45 | color: #fff;
46 | }
47 | }
48 | }
49 |
50 | // Buttons ====================================================================
51 |
52 | .btn {
53 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
54 |
55 | .caret {
56 | border-top-color: #fff;
57 | }
58 | }
59 |
60 | .btn-default {
61 | .btn-shadow(@btn-default-bg);
62 |
63 | &:hover {
64 | color: @btn-default-color;
65 | }
66 |
67 | .caret {
68 | border-top-color: @text-color;
69 | }
70 | }
71 |
72 | .btn-default {
73 | .btn-shadow(@btn-default-bg);
74 | }
75 |
76 | .btn-primary {
77 | .btn-shadow(@btn-primary-bg);
78 | }
79 |
80 | .btn-success {
81 | .btn-shadow(@btn-success-bg);
82 | }
83 |
84 | .btn-info {
85 | .btn-shadow(@btn-info-bg);
86 | }
87 |
88 | .btn-warning {
89 | .btn-shadow(@btn-warning-bg);
90 | }
91 |
92 | .btn-danger {
93 | .btn-shadow(@btn-danger-bg);
94 | }
95 |
96 | // Typography =================================================================
97 |
98 | // Tables =====================================================================
99 |
100 | // Forms ======================================================================
101 |
102 | // Navs =======================================================================
103 |
104 | // Indicators =================================================================
105 |
106 | // Progress bars ==============================================================
107 |
108 | // Containers =================================================================
109 |
110 | .panel-primary,
111 | .panel-success,
112 | .panel-warning,
113 | .panel-danger,
114 | .panel-info {
115 | .panel-heading,
116 | .panel-title {
117 | color: #fff;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/backend/routes/api.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const loginManager = require('../managers/loginmanager');
5 | const rentManager = require('../managers/rentmanager');
6 | const occupantManager = require('../managers/occupantmanager');
7 | const documentManager = require('../managers/documentmanager');
8 | const propertyManager = require('../managers/propertymanager');
9 | const ownerManager = require('../managers/ownermanager');
10 | const notificationManager = require('../managers/notificationmanager');
11 | const accountingManager = require('../managers/accountingmanager');
12 | const emailManager = require('../managers/emailmanager');
13 |
14 | module.exports = function () {
15 | const router = express.Router();
16 |
17 | router.use((req, res, next) => {
18 | if (!req.session || !req.user) {
19 | return res.sendStatus(401);
20 | }
21 | next();
22 | });
23 |
24 | const realmsRouter = express.Router();
25 | realmsRouter.get('/:id', loginManager.selectRealm);
26 | router.use('/realms', realmsRouter);
27 |
28 | const occupantsRouter = express.Router();
29 | occupantsRouter.post('/', occupantManager.add);
30 | occupantsRouter.patch('/:id', occupantManager.update);
31 | occupantsRouter.delete('/:ids', occupantManager.remove);
32 | occupantsRouter.get('/', occupantManager.all);
33 | occupantsRouter.get('/overview', occupantManager.overview);
34 | router.use('/occupants', occupantsRouter);
35 |
36 | const documentsRouter = express.Router();
37 | documentsRouter.patch('/:id', documentManager.update);
38 | router.use('/documents', documentsRouter);
39 |
40 | const notificationsRouter = express.Router();
41 | notificationsRouter.get('/', notificationManager.all);
42 | router.use('/notifications', notificationsRouter);
43 |
44 | const rentsRouter = express.Router();
45 | rentsRouter.patch('/:id', rentManager.update);
46 | rentsRouter.get('/occupant/:id', rentManager.rentsOfOccupant);
47 | rentsRouter.get('/:year/:month', rentManager.all);
48 | rentsRouter.get('/overview', rentManager.overview);
49 | rentsRouter.get('/overview/:year/:month', rentManager.overview);
50 | router.use('/rents', rentsRouter);
51 |
52 | const propertiesRouter = express.Router();
53 | propertiesRouter.post('/', propertyManager.add);
54 | propertiesRouter.patch('/:id', propertyManager.update);
55 | propertiesRouter.delete('/:ids', propertyManager.remove);
56 | propertiesRouter.get('/', propertyManager.all);
57 | propertiesRouter.get('/overview', propertyManager.overview);
58 | router.use('/properties', propertiesRouter);
59 |
60 | router.get('/accounting/:year', accountingManager.all);
61 |
62 | const ownerRouter = express.Router();
63 | ownerRouter.get('/', ownerManager.all);
64 | ownerRouter.patch('/:id', ownerManager.update);
65 | router.use('/owner', ownerRouter);
66 |
67 | const emailRouter = express.Router();
68 | emailRouter.post('/', emailManager.send);
69 | router.use('/emails', emailRouter);
70 |
71 | const apiRouter = express.Router();
72 | apiRouter.use('/api', router);
73 | return apiRouter;
74 | };
75 |
--------------------------------------------------------------------------------
/backend/pages/property/view/rightmenu.ejs:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
16 |
35 |
--------------------------------------------------------------------------------
/frontend/js/property/middleware.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import Handlebars from 'handlebars';
3 | import bootbox from 'bootbox';
4 | import i18next from 'i18next';
5 | import application from '../application';
6 | import ViewController from '../viewcontroller';
7 | import PropertyForm from './propertyform';
8 |
9 | class PropertyMiddleware extends ViewController {
10 | // PropertyCtrl extends Controller
11 | constructor() {
12 | super({
13 | domViewId: '#view-property',
14 | domListId: '#properties',
15 | defaultMenuId: 'properties-menu',
16 | listSelectionLabel: 'Selected property',
17 | listSelectionMenuId: 'properties-selection-menu',
18 | urls: {
19 | overview: '/api/properties/overview',
20 | items: '/api/properties',
21 | },
22 | });
23 | this.form = new PropertyForm();
24 | }
25 |
26 | onInitTemplate() {
27 | // Handlebars templates
28 | var $propertiesSelected = $('#view-property-selected-list-template');
29 | if ($propertiesSelected.length > 0) {
30 | this.templateSelectedRow = Handlebars.compile($propertiesSelected.html());
31 | }
32 | }
33 |
34 | onUserAction($action, actionId) {
35 | var selection = [];
36 | var selectionIds;
37 |
38 | selection = this.list.getSelectedData();
39 |
40 | if (actionId === 'list-action-edit-property') {
41 | this.form.setData(selection[0]);
42 | this.openForm('property-form');
43 | } else if (actionId === 'list-action-add-property') {
44 | this.list.unselectAll();
45 | this.form.setData(null);
46 | this.openForm('property-form');
47 | } else if (actionId === 'list-action-remove-property') {
48 | bootbox.confirm(
49 | i18next.t('Are you sure to remove this property'),
50 | (result) => {
51 | if (!result) {
52 | return;
53 | }
54 | selectionIds = [];
55 | for (var index = 0; index < selection.length; ++index) {
56 | selectionIds.push(selection[index]._id);
57 | }
58 | application.httpDelete(
59 | `/api/properties/${selectionIds.join()}`,
60 | (req, res) => {
61 | const response = JSON.parse(res.responseText);
62 | if (!response.errors || response.errors.length === 0) {
63 | this.list.unselectAll();
64 | this.loadList(() => {
65 | this.closeForm();
66 | });
67 | }
68 | }
69 | );
70 | }
71 | );
72 | } else if (actionId === 'list-action-save-form') {
73 | this.form.submit((data) => {
74 | this.closeForm(() => {
75 | this.loadList(() => {
76 | this.list.select($('.js-list-row#' + data._id), true);
77 | });
78 | });
79 | });
80 | }
81 | }
82 |
83 | onDataChanged(callback) {
84 | this.form.bindForm();
85 | if (callback) {
86 | callback();
87 | }
88 | }
89 |
90 | // overriden
91 | exited() {
92 | this.form.unbindForm();
93 | }
94 | }
95 |
96 | export default PropertyMiddleware;
97 |
--------------------------------------------------------------------------------
/frontend/less/index.less:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,700);
2 | // Core variables and mixins
3 | @import '../../node_modules/bootstrap/less/variables.less';
4 | @import '../../node_modules/bootstrap/less/mixins.less';
5 |
6 | // Reset and dependencies
7 | @import '../../node_modules/bootstrap/less/normalize.less';
8 | //@import "../../node_modules/bootstrap/less/print.less";
9 | //@import "../../node_modules/bootstrap/less/glyphicons.less";
10 |
11 | // Core CSS
12 | @import '../../node_modules/bootstrap/less/scaffolding.less';
13 | @import '../../node_modules/bootstrap/less/type.less';
14 | //@import "../../node_modules/bootstrap/less/code.less";
15 | @import '../../node_modules/bootstrap/less/grid.less';
16 | @import '../../node_modules/bootstrap/less/tables.less';
17 | @import '../../node_modules/bootstrap/less/forms.less';
18 | @import '../../node_modules/bootstrap/less/buttons.less';
19 |
20 | // Components
21 | //@import "../../node_modules/bootstrap/less/component-animations.less";
22 | @import '../../node_modules/bootstrap/less/dropdowns.less';
23 | @import '../../node_modules/bootstrap/less/button-groups.less';
24 | @import '../../node_modules/bootstrap/less/input-groups.less';
25 | @import '../../node_modules/bootstrap/less/navs.less';
26 | @import '../../node_modules/bootstrap/less/navbar.less';
27 | //@import "../../node_modules/bootstrap/less/breadcrumbs.less";
28 | //@import "../../node_modules/bootstrap/less/pagination.less";
29 | //@import "../../node_modules/bootstrap/less/pager.less";
30 | @import '../../node_modules/bootstrap/less/labels.less';
31 | //@import "../../node_modules/bootstrap/less/badges.less";
32 | //@import "../../node_modules/bootstrap/less/jumbotron.less";
33 | //@import "../../node_modules/bootstrap/less/thumbnails.less";
34 | @import '../../node_modules/bootstrap/less/alerts.less';
35 | //@import "../../node_modules/bootstrap/less/progress-bars.less";
36 | //@import "../../node_modules/bootstrap/less/media.less";
37 | @import '../../node_modules/bootstrap/less/list-group.less';
38 | @import '../../node_modules/bootstrap/less/panels.less';
39 | // @import "../../node_modules/bootstrap/less/responsive-embed.less";
40 | @import '../../node_modules/bootstrap/less/wells.less';
41 | @import '../../node_modules/bootstrap/less/close.less';
42 |
43 | // Components w/ JavaScript
44 | @import '../../node_modules/bootstrap/less/modals.less';
45 | @import '../../node_modules/bootstrap/less/tooltip.less';
46 | //@import "../../node_modules/bootstrap/less/popovers.less";
47 | @import '../../node_modules/bootstrap/less/carousel.less';
48 |
49 | // Utility classes
50 | @import '../../node_modules/bootstrap/less/utilities.less';
51 | // @import "../../node_modules/bootstrap/less/responsive-utilities.less";
52 |
53 | @import 'variables.less';
54 | @import 'bootswatch.less';
55 | @import '../../node_modules/bootstrap-datepicker/less/datepicker3.less';
56 | @import 'list.less';
57 | @import 'table.less';
58 | @import 'style.less';
59 | @import 'layout.less';
60 | @import 'menu.less';
61 | @import 'tiles.less';
62 | @import 'form.less';
63 | @import 'card.less';
64 | @import 'datepicker.less';
65 | @import 'accounting.less';
66 | @import 'website.less';
67 |
--------------------------------------------------------------------------------
/backend/pages/occupant/view/occupant.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include occupantform.ejs %>
4 |
5 |
6 |
7 | <%- include contractdocumentsform.ejs %>
8 |
9 |
10 |
11 |
12 | <%- include invoices.ejs %>
13 |
14 |
15 |
16 | <%- include ../../rent/view/rentshistory.ejs %>
17 |
18 |
19 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/frontend/less/list.less:
--------------------------------------------------------------------------------
1 | .list {
2 | position: relative;
3 | }
4 |
5 | .list-row {
6 | display: table;
7 | position: relative;
8 |
9 | width: 100%;
10 |
11 | padding: 5px 20px;
12 | white-space: nowrap;
13 |
14 | .list-col {
15 | display: table-cell;
16 | vertical-align: middle;
17 | width: 33.3333%;
18 | }
19 |
20 | .list-col:first-child {
21 | width: 66.6666%;
22 | }
23 |
24 | .label {
25 | font-size: ceil((@font-size-small * 0.8));
26 | }
27 |
28 | &:not(.fixed) {
29 | cursor: pointer;
30 | }
31 |
32 | &:not(:last-child) {
33 | margin-bottom: 20px;
34 | }
35 |
36 | .list-selection-overlay {
37 | position: absolute;
38 | display: none;
39 | top: 0;
40 | left: 0;
41 | height: 100%;
42 | width: 100%;
43 | font-size: 40px;
44 | border-radius: @border-radius-base;
45 | border: solid 1px @well-border;
46 |
47 | &::before {
48 | content: ' ';
49 | display: block;
50 | height: 100%;
51 | width: 100%;
52 | }
53 |
54 | .fa {
55 | display: none;
56 | position: absolute;
57 | color: @link-color;
58 | height: 40px;
59 | top: 50%;
60 | transform: translateY(-50%);
61 | left: 20px;
62 | }
63 | }
64 |
65 | &.active:not(.fixed) {
66 | color: white;
67 | .list-payment-price {
68 | color: @text-color;
69 | }
70 | .text-success,
71 | .text-danger,
72 | .text-warning {
73 | color: #fff;
74 | }
75 |
76 | .label,
77 | .list-payment-price {
78 | color: #fff;
79 | border: 1px solid #fff;
80 | background-color: transparent;
81 | }
82 |
83 | .list-avatar-col .fa {
84 | color: white;
85 | }
86 |
87 | background-color: lighten(@navbar-default-link-active-bg, 25%);
88 | }
89 | a,
90 | a:hover {
91 | text-transform: lowercase;
92 | color: @text-color;
93 | text-decoration: none;
94 | }
95 |
96 | .list-comment-link {
97 | position: absolute;
98 | right: 2px;
99 | top: 0px;
100 | }
101 | .list-title {
102 | padding-top: 15px;
103 | font-size: @font-size-large;
104 | white-space: normal;
105 | }
106 | .list-avatar-col {
107 | position: relative;
108 | padding: 10px 20px 10px 0;
109 | .fa {
110 | color: @link-color;
111 | }
112 | .fa-inverse {
113 | color: @text-color;
114 | }
115 | .icon-text {
116 | color: @link-color;
117 | font-size: 20px;
118 | margin-top: 4px;
119 | }
120 | }
121 | .list-label,
122 | .list-header-label {
123 | font-size: @font-size-small;
124 | }
125 | .list-header-label {
126 | padding-top: @font-size-large - @font-size-small;
127 | }
128 | .list-payment-price {
129 | font-size: @font-size-large;
130 | font-weight: bold;
131 | background-color: lighten(@well-border, 40%);
132 | border: solid 1px @well-border;
133 | border-radius: @border-radius-base;
134 | margin-top: 10px;
135 | margin-bottom: 10px;
136 | padding: 5px 4px;
137 | width: 100%;
138 | }
139 | .list-balance-price .price-symbol {
140 | font-size: ceil((@font-size-small * 0.65));
141 | }
142 | .list-inline {
143 | white-space: normal;
144 | li {
145 | padding-right: 0;
146 | padding-bottom: 5px;
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/backend/managers/emailmanager.js:
--------------------------------------------------------------------------------
1 | const { promisify } = require('util');
2 | const moment = require('moment');
3 | const axios = require('axios');
4 | const logger = require('winston');
5 | const config = require('../../config');
6 | const occupantModel = require('../models/occupant');
7 |
8 | const _sendEmail = async (req, message) => {
9 | const postData = {
10 | templateName: message.document,
11 | recordId: message.tenantId,
12 | params: {
13 | term: message.term,
14 | },
15 | };
16 |
17 | try {
18 | const response = await axios.post(config.EMAILER_URL, postData, {
19 | headers: {
20 | organizationId: req.headers.organizationid || String(req.realm._id),
21 | 'Accept-Language': req.language,
22 | },
23 | });
24 |
25 | logger.info(`POST ${config.EMAILER_URL} ${response.status}`);
26 | logger.debug(`data sent: ${JSON.stringify(postData)}`);
27 | logger.debug(`response: ${JSON.stringify(response.data)}`);
28 |
29 | return response.data.map(
30 | ({ templateName, recordId, params, email, status /*, error*/ }) => ({
31 | document: templateName,
32 | tenantId: recordId,
33 | term: params.term,
34 | email,
35 | status,
36 | })
37 | );
38 | } catch (error) {
39 | logger.error(`POST ${config.EMAILER_URL} failed`);
40 | logger.error(`data sent: ${JSON.stringify(postData)}`);
41 | logger.error(
42 | (error.response && error.response.data && error.response.data.message) ||
43 | error.message
44 | );
45 | throw error;
46 | }
47 | };
48 |
49 | module.exports = {
50 | send: async (req, res) => {
51 | try {
52 | const realm = req.realm;
53 | const { document, tenantIds, terms, year, month } = req.body;
54 | const findTenant = promisify(occupantModel.findOne).bind(occupantModel);
55 | const messages = [];
56 | await Promise.all(
57 | tenantIds.map(async (tenantId, index) => {
58 | const tenant = await findTenant(realm, tenantId);
59 | messages.push({
60 | name: tenant.name,
61 | tenantId,
62 | document,
63 | term: Number(
64 | (terms && terms[index]) ||
65 | moment(`${year}/${month}/01`, 'YYYY/MM/DD').format('YYYYMMDDHH')
66 | ),
67 | });
68 | })
69 | );
70 | const statusList = await Promise.all(
71 | messages.map(async (message) => {
72 | try {
73 | return await _sendEmail(req, message);
74 | } catch (error) {
75 | return [
76 | {
77 | ...message,
78 | error: (error.response && error.response.data) || {
79 | status: 500,
80 | message: 'Something went wrong',
81 | },
82 | },
83 | ];
84 | }
85 | })
86 | );
87 | const results = statusList.reduce((acc, statuses, index) => {
88 | acc.push(
89 | ...statuses.map((status) => ({
90 | name: messages[index].name,
91 | ...status,
92 | }))
93 | );
94 | return acc;
95 | }, []);
96 | res.json(results);
97 | } catch (err) {
98 | logger.error(err);
99 | res.status(500).send(err);
100 | }
101 | },
102 | };
103 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loca",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "npm run buildprod && NODE_ENV=production node server.js",
7 | "buildprod": "NODE_ENV=production node scripts/build.js",
8 | "dev": "npm-run-all builddev watch",
9 | "watch": "npm-run-all --parallel watch:img watch:less watch:front:js watch:back:js",
10 | "watch:front": "npm-run-all --parallel watch:img watch:less watch:front:js",
11 | "watch:back:js": "nodemon -e js,ejs -w backend -w server.js --inspect=0.0.0.0:9229 server.js",
12 | "watch:front:js": "nodemon -e js,ejs -w frontend/js -w backend/pages scripts/build.js js",
13 | "watch:less": "nodemon -e less -w frontend/less scripts/build.js css",
14 | "watch:img": "nodemon -e png,jpg -w frontend/images scripts/build.js img",
15 | "builddev": "node scripts/build.js static",
16 | "lint": "eslint .",
17 | "prepare": "husky install",
18 | "test": "LOCA_DEMOMODE=false LOCA_PRODUCTIVE=true mocha",
19 | "coverage": "LOCA_DEMOMODE=false LOCA_PRODUCTIVE=true nyc --reporter=lcov --reporter=text-summary mocha",
20 | "mongodump": "node scripts/mongodump.js",
21 | "mongorestore": "node scripts/mongorestore.js"
22 | },
23 | "lint-staged": {
24 | "**/*": "prettier --write --ignore-unknown"
25 | },
26 | "dependencies": {
27 | "axios": "0.21.1",
28 | "bcryptjs": "2.4.3",
29 | "body-parser": "1.19.0",
30 | "bootbox": "5.4.0",
31 | "bootstrap": "3.4.1",
32 | "bootstrap-datepicker": "1.9.0",
33 | "cookie-parser": "1.4.5",
34 | "ejs": "2.7.4",
35 | "errorhandler": "1.5.1",
36 | "express": "4.17.1",
37 | "express-session": "1.17.0",
38 | "express-winston": "2.6.0",
39 | "frontexpress": "1.2.1",
40 | "handlebars": "^4.7.7",
41 | "i18next": "14.1.1",
42 | "i18next-browser-languagedetector": "2.2.4",
43 | "i18next-express-middleware": "1.9.1",
44 | "i18next-localstorage-cache": "1.1.1",
45 | "i18next-node-fs-backend": "2.1.3",
46 | "i18next-sprintf-postprocessor": "0.2.2",
47 | "i18next-xhr-backend": "1.5.1",
48 | "intl": "1.2.5",
49 | "jquery": "3.5.0",
50 | "jquery-validation": "1.19.3",
51 | "mathjs": "7.5.1",
52 | "method-override": "3.0.0",
53 | "minivents": "2.2.0",
54 | "moment": "2.24.0",
55 | "moment-timezone": "0.5.28",
56 | "mongobackup": "0.3.5",
57 | "mongojs": "3.1.0",
58 | "nanoid": "3.1.22",
59 | "node-vault": "0.9.21",
60 | "passport": "0.4.1",
61 | "passport-local": "1.0.0",
62 | "request": "2.88.2",
63 | "serve-favicon": "2.5.0",
64 | "sugar": "2.0.6",
65 | "winston": "2.4.4"
66 | },
67 | "devDependencies": {
68 | "@babel/cli": "7.12.1",
69 | "@babel/core": "7.9.0",
70 | "@babel/preset-env": "7.9.5",
71 | "babel-eslint": "10.1.0",
72 | "browser-sync": "2.26.14",
73 | "eslint": "7.26.0",
74 | "fs-extra": "9.0.0",
75 | "husky": "6.0.0",
76 | "imagemin": "6.1.0",
77 | "imagemin-mozjpeg": "8.0.0",
78 | "imagemin-pngquant": "6.0.1",
79 | "less": "3.11.1",
80 | "lint-staged": "11.0.0",
81 | "mocha": "6.2.3",
82 | "nodemon": "2.0.3",
83 | "npm-run-all": "4.1.5",
84 | "nyc": "15.0.1",
85 | "prettier": "2.3.0",
86 | "proxyquire": "2.0.0",
87 | "purify-css": "1.2.6",
88 | "rollup": "2.6.1",
89 | "rollup-plugin-babel": "4.4.0",
90 | "rollup-plugin-commonjs": "10.1.0",
91 | "rollup-plugin-includepaths": "0.2.3",
92 | "rollup-plugin-terser": "5.3.0",
93 | "sinon": "9.0.1",
94 | "supertest": "4.0.2"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/backend/models/realm.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const OF = require('./objectfilter');
3 | const Model = require('./model');
4 | const logger = require('winston');
5 |
6 | class RealmModel extends Model {
7 | constructor() {
8 | super('realms');
9 | this.schema = new OF({
10 | _id: String,
11 | name: String,
12 | members: Array, // [{ name, email, role, registered },]
13 | addresses: Array, // [{ street1, street2, zipCode, city, state, country }, ]
14 | bankInfo: Object, // { name, iban }
15 | contacts: Array, // [{ name, email, phone1, phone2 }]
16 | isCompany: Boolean,
17 | companyInfo: Object, // { name, legalStructure, capital, ein, dos, vatNumber, legalRepresentative }
18 | locale: String,
19 | currency: String,
20 | tenants: Array, // [{ name, emails, access },]
21 | thirdParties: Object, // { mailgun: { apiKey, domain, fromEmail, replyToEmail }}
22 |
23 | // TODO to remove, replaced by companyInfo
24 | creation: String,
25 | company: String,
26 | legalForm: String,
27 | vatNumber: String,
28 | capital: Number,
29 | siret: String,
30 | rcs: String,
31 | manager: String,
32 |
33 | // TODO to remove, replaced by bankInfo
34 | bank: String,
35 | rib: String,
36 |
37 | // TODO to remove, replaced by contacts
38 | contact: String,
39 | email: String,
40 | phone1: String,
41 | phone2: String,
42 |
43 | // TODO to remove, replaced by addresses
44 | street1: String,
45 | street2: String,
46 | zipCode: String,
47 | city: String,
48 |
49 | // TODO to remove, replaced by members
50 | administrator: String,
51 | user1: String,
52 | user2: String,
53 | user3: String,
54 | user4: String,
55 | user5: String,
56 | user6: String,
57 | user7: String,
58 | user8: String,
59 | user9: String,
60 | user10: String,
61 | });
62 | }
63 |
64 | findOne(id, callback) {
65 | super.findOne(null, id, function (errors, realm) {
66 | if (errors) {
67 | callback(errors);
68 | } else if (!realm) {
69 | callback(['realm not found']);
70 | } else {
71 | callback(null, realm);
72 | }
73 | });
74 | }
75 |
76 | findAll(callback) {
77 | super.findAll(null, function (errors, realms) {
78 | if (errors) {
79 | callback(errors);
80 | } else if (!realms || realms.length === 0) {
81 | callback(null, null);
82 | } else {
83 | callback(null, realms);
84 | }
85 | });
86 | }
87 |
88 | findByEmail(email, callback) {
89 | // TODO to optimize: filter should by applied on DB
90 | super.findAll(null, function (errors, realms) {
91 | if (errors) {
92 | callback(errors);
93 | } else if (!realms || realms.length === 0) {
94 | callback(null, null);
95 | } else {
96 | const realmsFound = realms.filter((realm) =>
97 | realm.members.map(({ email }) => email).includes(email)
98 | );
99 | callback(null, realmsFound);
100 | }
101 | });
102 | }
103 |
104 | add(realm, callback) {
105 | super.add(null, realm, callback);
106 | }
107 |
108 | update(realm, callback) {
109 | super.update(null, realm, callback);
110 | }
111 |
112 | remove() {
113 | logger.error('method not implemented!');
114 | }
115 | }
116 |
117 | module.exports = new RealmModel();
118 |
--------------------------------------------------------------------------------
/frontend/js/property/propertyform.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import i18next from 'i18next';
3 | import Form from '../form';
4 | import Helper from '../lib/helper';
5 |
6 | const domSelector = '#property-form';
7 |
8 | class PropertyForm extends Form {
9 | constructor() {
10 | super({
11 | domSelector,
12 | uri: '/api/properties',
13 | manifest: {
14 | type: {
15 | required: true,
16 | },
17 | name: {
18 | required: true,
19 | minlength: 2,
20 | },
21 | surface: {
22 | number: true,
23 | min: 0,
24 | },
25 | phone: {
26 | phoneFR: true,
27 | },
28 | price: {
29 | required: true,
30 | number: true,
31 | min: 0,
32 | },
33 | expense: {
34 | required: {
35 | depends: () => {
36 | const type = $(domSelector + ' #type').val();
37 | return type === 'office';
38 | },
39 | },
40 | number: true,
41 | min: 0,
42 | },
43 | },
44 | defaultData: {
45 | _id: '',
46 | type: 'office',
47 | name: '',
48 | description: '',
49 | surface: '',
50 | phone: '',
51 | building: '',
52 | level: '',
53 | location: '',
54 | price: '',
55 | expense: '',
56 | },
57 | });
58 | }
59 |
60 | beforeSetData(args) {
61 | const property = args[0];
62 | if (property) {
63 | if (!property.phone) {
64 | property.phone = '';
65 | }
66 | if (!property.surface) {
67 | property.surface = '';
68 | }
69 | if (!property.expense) {
70 | property.expense = '';
71 | }
72 | }
73 | }
74 |
75 | afterSetData(args) {
76 | const property = args[0];
77 |
78 | if (property && property._id) {
79 | $(domSelector + ' #propertyNameLabel').html(property.name);
80 | $('.js-user-action[data-id="list-action-remove-property"]').show();
81 | } else {
82 | $(domSelector + ' #propertyNameLabel').html(
83 | i18next.t('Property to rent')
84 | );
85 | $('.js-user-action[data-id="list-action-remove-property"]').hide();
86 | }
87 |
88 | this._typeChanged($(domSelector + ' #type'));
89 | this._computeRent();
90 | }
91 |
92 | onBind() {
93 | const that = this;
94 |
95 | $(domSelector + ' #type').change(function () {
96 | that._typeChanged($(this));
97 | });
98 |
99 | $(domSelector + ' #price').keyup(() => {
100 | this._computeRent();
101 | });
102 | }
103 |
104 | //----------------------------------------
105 | // Helpers
106 | //----------------------------------------
107 | _typeChanged($select) {
108 | const selection = $select.find(':selected').val();
109 | if (selection !== 'office') {
110 | $('.property-no-expense').hide();
111 | } else {
112 | $('.property-no-expense').show();
113 | }
114 | }
115 |
116 | _computeRent() {
117 | const data = this.getData();
118 | const rentWithExpenses = Number(data.price) + Number(data.expense);
119 |
120 | $('#property-form-summary-rent').html(
121 | Helper.formatMoney(data.price, false, false)
122 | );
123 | $('#property-form-summary-expense').html(
124 | Helper.formatMoney(data.expense, false, false)
125 | );
126 | $('#property-form-summary-totla-rentwithexpenses').html(
127 | Helper.formatMoney(rentWithExpenses, false, false)
128 | );
129 | }
130 | }
131 |
132 | export default PropertyForm;
133 |
--------------------------------------------------------------------------------
/backend/pages/rent/view/rent.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include paymentform.ejs %>
4 |
5 |
6 |
7 | <%- include rentshistory.ejs %>
8 |
9 |
10 |
38 |
39 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/frontend/js/language.js:
--------------------------------------------------------------------------------
1 | /*eslint no-console: ["error", { allow: ["warn", "error"] }] */
2 |
3 | import moment from 'moment';
4 | import i18next from 'i18next';
5 |
6 | const LangsForJQueryValidate = {
7 | pt: 'pt_PT',
8 | };
9 |
10 | async function updateLanguageScript(id, src) {
11 | let fileref = document.getElementById(id);
12 |
13 | if (fileref) {
14 | document.getElementsByTagName('head')[0].removeChild(fileref);
15 | }
16 |
17 | await new Promise((resolve, reject) => {
18 | try {
19 | fileref = document.createElement('script');
20 | fileref.id = id;
21 | fileref.type = 'text/javascript';
22 | fileref.async = true;
23 | fileref.onload = (res) => {
24 | if (res.type === 'error') {
25 | reject(
26 | `an error has occurred when loading the localization file ${src}`
27 | );
28 | } else {
29 | resolve(res);
30 | }
31 | };
32 | fileref.onerror = () => {
33 | reject(
34 | `an error has occurred when loading the localization file ${src}`
35 | );
36 | };
37 | fileref.src = src;
38 | document.getElementsByTagName('head')[0].appendChild(fileref);
39 | } catch (error) {
40 | reject(error);
41 | }
42 | });
43 | }
44 |
45 | export default (defaultCountryCode, callback) => {
46 | document.addEventListener('DOMContentLoaded', () => {
47 | // Init locale
48 | i18next
49 | .use(window.i18nextBrowserLanguageDetector)
50 | .use(window.i18nextLocalStorageCache)
51 | .use(window.i18nextXHRBackend)
52 | .use(window.i18nextSprintfPostProcessor)
53 | .init({
54 | fallbackLng: 'en',
55 | debug: false,
56 | pluralSeparator: '_',
57 | keySeparator: '::',
58 | nsSeparator: ':::',
59 | detection: {
60 | order: [/*'querystring', 'localStorage',*/ 'cookie', 'navigator'],
61 | //lookupQuerystring: 'lng',
62 | lookupCookie: 'locaI18next',
63 | cookieDomain: 'loca',
64 | // lookupLocalStorage: 'i18nextLng',
65 | caches: [/*'localStorage', */ 'cookie'],
66 | },
67 | // cache: {
68 | // enabled: false,
69 | // prefix: 'i18next_res_',
70 | // expirationTime: 7 * 24 * 60 * 60 * 1000
71 | // },
72 | backend: {
73 | loadPath: '/public/locales/{{lng}}.json',
74 | allowMultiLoading: false,
75 | },
76 | });
77 |
78 | i18next.on('languageChanged', function (countryCode = defaultCountryCode) {
79 | const splittedCountryCode = countryCode.split('-');
80 | const lang = splittedCountryCode[0].toLowerCase();
81 | const langForJQueryValidate = LangsForJQueryValidate[lang] || lang;
82 | try {
83 | Promise.all([
84 | updateLanguageScript(
85 | 'moment-language',
86 | `//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/locale/${lang}.js`
87 | ),
88 | updateLanguageScript(
89 | 'jquery-validate-language',
90 | `//ajax.aspnetcdn.com/ajax/jquery.validate/1.13.1/localization/messages_${langForJQueryValidate}.js`
91 | ),
92 | updateLanguageScript(
93 | 'bootstrap-datepicker-language',
94 | `/node_modules/bootstrap-datepicker/dist/locales/bootstrap-datepicker.${lang}.min.js`
95 | ),
96 | ]);
97 | } catch (error) {
98 | console.error(error);
99 | }
100 |
101 | moment.locale(lang);
102 | const dateFormat = moment.localeData().longDateFormat('L').toLowerCase();
103 | $.fn.datepicker.defaults.language = countryCode;
104 | $.fn.datepicker.defaults.format = dateFormat;
105 | if (callback) {
106 | callback(lang);
107 | }
108 | });
109 | });
110 | };
111 |
--------------------------------------------------------------------------------
/frontend/less/tiles.less:
--------------------------------------------------------------------------------
1 | .tiles {
2 | .col-xs-1,
3 | .col-xs-2,
4 | .col-xs-3,
5 | .col-xs-4,
6 | .col-xs-5,
7 | .col-xs-6,
8 | .col-xs-7,
9 | .col-xs-8,
10 | .col-xs-9,
11 | .col-xs-10,
12 | .col-xs-11,
13 | .col-xs-12 {
14 | padding-left: 0;
15 | padding-right: 5px;
16 | padding-bottom: 5px;
17 | &.last-col {
18 | padding-right: 0;
19 | }
20 | &.last-row {
21 | padding-bottom: 0;
22 | }
23 | }
24 |
25 | .tile {
26 | height: 195px;
27 | padding: 10px;
28 | color: white;
29 | &:not(.disabled) {
30 | cursor: pointer;
31 | }
32 |
33 | .h1,
34 | .h2,
35 | .h3,
36 | .h4,
37 | .h5,
38 | h1,
39 | h2,
40 | h3,
41 | h4,
42 | h5 {
43 | color: white;
44 | }
45 |
46 | .carousel {
47 | height: 175px; // - padding
48 | .item {
49 | height: 175px; //- padding
50 | }
51 | }
52 |
53 | .vertical-center {
54 | position: relative;
55 | top: 50%;
56 | transform: translateY(-50%);
57 | margin: 0;
58 | .h1,
59 | .h2,
60 | .h3,
61 | .h4,
62 | .h5,
63 | h1,
64 | h2,
65 | h3,
66 | h4,
67 | h5 {
68 | &:first-child {
69 | margin-top: 0;
70 | }
71 | }
72 | }
73 |
74 | .tilecaption {
75 | font-size: 70px;
76 | }
77 |
78 | .tilecurrency {
79 | font-size: 24px;
80 | font-weight: bold;
81 | white-space: nowrap;
82 | }
83 |
84 | &.blue {
85 | background: rgb(0, 172, 238);
86 | &:hover:not(.disabled) {
87 | background-color: darken(rgb(0, 172, 238), 10%);
88 | }
89 | &.lighter {
90 | background: rgb(62, 157, 215);
91 | &:hover:not(.disabled) {
92 | background-color: darken(rgb(62, 157, 215), 10%);
93 | }
94 | }
95 | &.light {
96 | background: rgb(71, 193, 228);
97 | &:hover:not(.disabled) {
98 | background-color: darken(rgb(71, 193, 228), 10%);
99 | }
100 | }
101 | &.dark {
102 | background: rgb(0, 93, 233);
103 | &:hover:not(.disabled) {
104 | background-color: darken(rgb(0, 93, 233), 10%);
105 | }
106 | }
107 | }
108 |
109 | &.grey {
110 | color: #555;
111 | .h1,
112 | .h2,
113 | .h3,
114 | .h4,
115 | .h5,
116 | h1,
117 | h2,
118 | h3,
119 | h4,
120 | h5 {
121 | color: #555;
122 | }
123 | background: rgb(243, 243, 243);
124 | &:hover:not(.disabled) {
125 | background-color: darken(rgb(243, 243, 243), 10%);
126 | }
127 | }
128 |
129 | &.cover {
130 | background-size: cover;
131 | background-image: url('/public/images/manhattan.jpg');
132 | background-position: center center;
133 | font-weight: bold;
134 | .h1,
135 | .h2,
136 | .h3,
137 | .h4,
138 | .h5,
139 | h1,
140 | h2,
141 | h3,
142 | h4,
143 | h5 {
144 | font-weight: bold;
145 | }
146 | }
147 |
148 | &.red {
149 | background: rgb(175, 26, 63);
150 | &:hover:not(.disabled) {
151 | background-color: darken(rgb(175, 26, 63), 10%);
152 | }
153 | }
154 |
155 | &.white {
156 | background: white;
157 | &:hover:not(.disabled) {
158 | background-color: darken(white, 10%);
159 | }
160 | }
161 |
162 | &.orange {
163 | background: rgb(209, 70, 37);
164 | &:hover:not(.disabled) {
165 | background-color: darken(rgb(209, 70, 37), 10%);
166 | }
167 | }
168 |
169 | &.green {
170 | background: rgb(0, 142, 0);
171 | &:hover:not(.disabled) {
172 | background-color: darken(rgb(0, 142, 0), 10%);
173 | }
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/frontend/js/occupant/contractdocumentsform.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import moment from 'moment';
3 | import i18next from 'i18next';
4 | import Form from '../form';
5 |
6 | const domSelector = '#contract-documents-form';
7 |
8 | class ContractDocumentsForm extends Form {
9 | constructor() {
10 | super({
11 | domSelector,
12 | uri: '',
13 | manifest: {
14 | name_0: {
15 | minlength: 2,
16 | },
17 | expirationDate_0: {
18 | required: {
19 | depends: () =>
20 | $(domSelector + ' #name_0')
21 | .val()
22 | .trim() !== '',
23 | },
24 | date: true,
25 | },
26 | },
27 | defaultData: {
28 | _id: '',
29 | occupantId: '',
30 | documents: [{ name: '', expirationDate: '' }],
31 | },
32 | });
33 | }
34 |
35 | beforeSetData(args) {
36 | const occupant = args[0];
37 |
38 | this.documentRowCount = 0;
39 |
40 | if (occupant.documents) {
41 | occupant.documents.forEach((doc, index) => {
42 | if (doc.expirationDate) {
43 | doc.expirationDate = moment(doc.expirationDate).format('L'); //db formtat to display one
44 | }
45 | if (index !== 0) {
46 | // Except first one row still exists
47 | this.addDocumentRow();
48 | }
49 | });
50 | }
51 | }
52 |
53 | afterSetData(args) {
54 | const occupant = args[0];
55 |
56 | $(domSelector + ' #occupantNameLabel').html(
57 | i18next.t("'s documents", { name: occupant.name })
58 | );
59 | }
60 |
61 | onGetData(data) {
62 | if (data.documents) {
63 | data.documents.forEach((doc) => {
64 | if (doc.expirationDate) {
65 | doc.expirationDate = moment(doc.expirationDate, 'L').toDate(); //display format to db one
66 | }
67 | });
68 | }
69 | return data;
70 | }
71 |
72 | onBind() {
73 | // Dynamic property rows
74 | $(domSelector + ' #btn-add-document').click(() => {
75 | this.addDocumentRow();
76 | return false;
77 | });
78 |
79 | // Remove dynamic rows
80 | $(domSelector + ' .js-btn-form-remove-row').click(function () {
81 | const $row = $(this).parents('.js-form-row');
82 | if (!$row.hasClass('js-master-form-row')) {
83 | $row.remove();
84 | } else {
85 | $(domSelector + ' #name_0').val('');
86 | $(domSelector + ' #expirationDate_0').val('');
87 | }
88 | return false;
89 | });
90 | $(domSelector + ' .js-master-form-row .js-btn-form-remove-row').hide();
91 | }
92 |
93 | addDocumentRow() {
94 | // Create new property row
95 | this.documentRowCount++;
96 | const $newRow = $(domSelector + ' #documents .js-master-form-row')
97 | .clone(true)
98 | .removeClass('js-master-form-row');
99 | $('.has-error', $newRow).removeClass('has-error');
100 | $('label.error', $newRow).remove();
101 | const itemDocumentName = 'name_' + this.documentRowCount;
102 | const itemExpirtationDateName = 'expirationDate_' + this.documentRowCount;
103 | $('#name_0', $newRow)
104 | .attr('id', itemDocumentName)
105 | .attr('name', itemDocumentName)
106 | .val('');
107 | $('#expirationDate_0', $newRow)
108 | .attr('id', itemExpirtationDateName)
109 | .attr('name', itemExpirtationDateName)
110 | .val('');
111 | $('.js-btn-form-remove-row', $newRow).show();
112 | // Add new property row in DOM
113 | $(domSelector + ' #documents').append($newRow);
114 |
115 | //Add jquery validation rules for new added fields
116 | $('#' + itemDocumentName, $newRow).rules('add', {
117 | required: true,
118 | minlength: 2,
119 | });
120 |
121 | $('#' + itemExpirtationDateName, $newRow).rules('add', {
122 | required: true,
123 | date: true,
124 | });
125 | }
126 | }
127 |
128 | export default ContractDocumentsForm;
129 |
--------------------------------------------------------------------------------