├── .gitignore ├── .idea └── workspace.xml ├── .nomedia ├── .prettierrc.json ├── README.md ├── client ├── .gitignore ├── README.md ├── a.txt ├── angular.json ├── browserslist ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── extra-webpack.config.js ├── firebase.json ├── karma.conf.js ├── package.json ├── src │ ├── app │ │ ├── admin │ │ │ ├── admin.module.ts │ │ │ └── admin │ │ │ │ ├── admin.component.html │ │ │ │ ├── admin.component.scss │ │ │ │ ├── admin.component.spec.ts │ │ │ │ └── admin.component.ts │ │ ├── auth │ │ │ ├── auth.component.html │ │ │ ├── auth.component.scss │ │ │ ├── auth.component.ts │ │ │ ├── auth.module.ts │ │ │ └── auth.service.ts │ │ ├── guard.ts │ │ ├── intercom-conf │ │ │ ├── app.compnent.ts │ │ │ ├── conf.service.ts │ │ │ ├── intercom-conf.module.ts │ │ │ ├── intercom-conf │ │ │ │ ├── contacts │ │ │ │ │ ├── contacts.component.html │ │ │ │ │ ├── contacts.component.scss │ │ │ │ │ ├── contacts.component.spec.ts │ │ │ │ │ └── contacts.component.ts │ │ │ │ ├── intercom-conf.component.html │ │ │ │ ├── intercom-conf.component.scss │ │ │ │ ├── intercom-conf.component.spec.ts │ │ │ │ ├── intercom-conf.component.ts │ │ │ │ └── logs │ │ │ │ │ ├── logs.component.html │ │ │ │ │ ├── logs.component.scss │ │ │ │ │ ├── logs.component.spec.ts │ │ │ │ │ └── logs.component.ts │ │ │ └── panels │ │ │ │ ├── panels.component.html │ │ │ │ ├── panels.component.scss │ │ │ │ ├── panels.component.spec.ts │ │ │ │ └── panels.component.ts │ │ ├── shared │ │ │ ├── components │ │ │ │ ├── components.module.ts │ │ │ │ ├── content │ │ │ │ │ ├── content.component.html │ │ │ │ │ ├── content.component.scss │ │ │ │ │ ├── content.component.spec.ts │ │ │ │ │ └── content.component.ts │ │ │ │ ├── filters │ │ │ │ │ ├── autocomplete │ │ │ │ │ │ ├── autocomplete.component.html │ │ │ │ │ │ ├── autocomplete.component.scss │ │ │ │ │ │ ├── autocomplete.component.spec.ts │ │ │ │ │ │ └── autocomplete.component.ts │ │ │ │ │ ├── base.component.ts │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── checkbox.component.spec.ts │ │ │ │ │ │ └── checkbox.component.ts │ │ │ │ │ ├── combobox │ │ │ │ │ │ ├── combobox.component.html │ │ │ │ │ │ ├── combobox.component.scss │ │ │ │ │ │ ├── combobox.component.spec.ts │ │ │ │ │ │ └── combobox.component.ts │ │ │ │ │ ├── dropdown │ │ │ │ │ │ ├── dropdown.component.spec.ts │ │ │ │ │ │ └── dropdown.component.ts │ │ │ │ │ ├── filter │ │ │ │ │ │ ├── filter.component.spec.ts │ │ │ │ │ │ └── filter.component.ts │ │ │ │ │ ├── quantity-filter │ │ │ │ │ │ ├── quantity-filter.component.html │ │ │ │ │ │ ├── quantity-filter.component.scss │ │ │ │ │ │ ├── quantity-filter.component.spec.ts │ │ │ │ │ │ └── quantity-filter.component.ts │ │ │ │ │ ├── special-filter-grid │ │ │ │ │ │ ├── special-filter-grid.component.html │ │ │ │ │ │ ├── special-filter-grid.component.scss │ │ │ │ │ │ ├── special-filter-grid.component.spec.ts │ │ │ │ │ │ └── special-filter-grid.component.ts │ │ │ │ │ └── special-filter │ │ │ │ │ │ ├── special-filter.component.html │ │ │ │ │ │ ├── special-filter.component.scss │ │ │ │ │ │ ├── special-filter.component.spec.ts │ │ │ │ │ │ └── special-filter.component.ts │ │ │ │ ├── keyboard │ │ │ │ │ ├── keyboard.component.html │ │ │ │ │ ├── keyboard.component.scss │ │ │ │ │ ├── keyboard.component.spec.ts │ │ │ │ │ └── keyboard.component.ts │ │ │ │ ├── material │ │ │ │ │ └── material.module.ts │ │ │ │ ├── multi-slider-range-selector │ │ │ │ │ ├── multi-slider-range-selector.component.html │ │ │ │ │ ├── multi-slider-range-selector.component.scss │ │ │ │ │ ├── multi-slider-range-selector.component.spec.ts │ │ │ │ │ └── multi-slider-range-selector.component.ts │ │ │ │ ├── nav-menu │ │ │ │ │ ├── menu.interface.ts │ │ │ │ │ ├── nav-menu.component.html │ │ │ │ │ ├── nav-menu.component.scss │ │ │ │ │ ├── nav-menu.component.spec.ts │ │ │ │ │ └── nav-menu.component.ts │ │ │ │ ├── parameter-picker │ │ │ │ │ ├── parameter-picker.component.html │ │ │ │ │ ├── parameter-picker.component.scss │ │ │ │ │ ├── parameter-picker.component.spec.ts │ │ │ │ │ └── parameter-picker.component.ts │ │ │ │ ├── topbar │ │ │ │ │ ├── topbar.component.html │ │ │ │ │ ├── topbar.component.scss │ │ │ │ │ ├── topbar.component.ts │ │ │ │ │ └── topbar.interface.ts │ │ │ │ └── tree │ │ │ │ │ ├── tree.component.html │ │ │ │ │ ├── tree.component.scss │ │ │ │ │ ├── tree.component.spec.ts │ │ │ │ │ └── tree.component.ts │ │ │ ├── directives │ │ │ │ └── focus.directive.ts │ │ │ └── services │ │ │ │ ├── dialog.service.ts │ │ │ │ ├── i18n.service.ts │ │ │ │ ├── interceptors.service.ts │ │ │ │ └── xlsx.service.ts │ │ └── starter.module.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── i18n │ │ │ ├── login │ │ │ │ ├── en.json │ │ │ │ └── he.json │ │ │ └── tador │ │ │ │ ├── en.json │ │ │ │ └── he.json │ │ ├── logo.png │ │ └── styles │ │ │ ├── material │ │ │ ├── mat-horizontal-stepper.scss │ │ │ ├── mat-slide-toggle.scss │ │ │ └── style.scss │ │ │ └── style.scss │ ├── environments │ │ ├── environment.dev.ts │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── generator └── index.ts ├── new server ├── .eslintrc.json ├── .prettierrc.json ├── package.json ├── src │ └── app.ts ├── tsconfig.build.json └── tsconfig.json ├── package-lock.json ├── package.json ├── serve-static.ts ├── server ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── env │ ├── docker-compose.yml │ └── env-up.sh ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package.json ├── src │ ├── admin │ │ ├── admin.controller.ts │ │ └── admin.module.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ └── roles.decorator.ts │ ├── decorators │ │ └── user.decorator.ts │ ├── main.ts │ ├── middlewares │ │ ├── authorize.middleware.ts │ │ ├── frontend.middleware.ts │ │ ├── login.middleware.ts │ │ └── normelize.middleware.ts │ ├── pipes │ │ └── validation.pipe.ts │ ├── services │ │ ├── mailer.service.ts │ │ └── user.service.ts │ ├── tador │ │ ├── client.ts │ │ ├── initial_daumps.ts │ │ ├── tador.controller.ts │ │ ├── tador.module.ts │ │ ├── tador.service.spec.ts │ │ └── tador.service.ts │ └── utils.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig-paths-bootstrap.js ├── tsconfig.build.json ├── tsconfig.json └── tslint.json ├── shared ├── constants.ts ├── guard.ts ├── index.ts ├── models │ ├── Entity.ts │ ├── filter.base.model.ts │ ├── filter.model.ts │ ├── index.ts │ ├── tador │ │ ├── add-panel-request.ts │ │ ├── conf.ts │ │ ├── enum.ts │ │ └── panels │ │ │ ├── base.panel.ts │ │ │ ├── index.ts │ │ │ ├── mp.panel.ts │ │ │ └── panels.ts │ ├── tslint.json │ └── user.model.ts ├── tsconfig.json └── utils.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | client/.firebase/hosting.ZGlzdA.cache 4 | .idea 5 | dist 6 | -------------------------------------------------------------------------------- /.nomedia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrab/nest-angular/dccc0d5514ac564e0db78d0752657433a71a8f79/.nomedia -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 130 7 | } 8 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /.well-known 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # profiling files 13 | chrome-profiler-events.json 14 | speed-measure-plugin.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | yarn-error.log 40 | testem.log 41 | /typings 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | 47 | CordovaMobileApp 48 | 49 | api 50 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Angify 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /client/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | last 2 Chrome versions 9 | -------------------------------------------------------------------------------- /client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to temppp!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser 19 | .manage() 20 | .logs() 21 | .get(logging.Type.BROWSER); 22 | expect(logs).not.toContain( 23 | jasmine.objectContaining({ 24 | level: logging.Level.SEVERE, 25 | }) 26 | ); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/') as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /client/extra-webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = cfg => { 2 | // const options = cfg.optimization.minimizer[cfg.optimization.minimizer.length - 1].options.terserOptions; 3 | // if (options) { 4 | // options.keep_classnames = true; 5 | // console.log('start build with options.keep_classnames = true;'); 6 | // } else { 7 | // console.log('problem with set keep_classnames'); 8 | // } 9 | return cfg; 10 | }; 11 | -------------------------------------------------------------------------------- /client/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/Angify'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angify", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start:ssl": "ng serve -c dev --ssl --ssl-key ../../localhost-key.pem --ssl-cert ../../localhost.pem --open", 7 | "start": "ng serve --open", 8 | "build": "ng build", 9 | "build:prod": "ng build --prod", 10 | "build:dev": "ng build -c dev", 11 | "build:prod:cordova": "ng build --prod --base-href . --output-path ./CordovaMobileApp/www/", 12 | "build:dev:cordova": "ng build --base-href . --output-path ./CordovaMobileApp/www/ --watch", 13 | "cordova:init": "cordova create CordovaMobileApp com.yantrab.app CordovaMobileApp", 14 | "cordova:run:browser": "cd CordovaMobileApp&&cordova platform add browser&&cordova run browser", 15 | "cordiva:add:android": "cd CordovaMobileApp&&cordova platform add android", 16 | "cordova:run:android": "cd CordovaMobileApp&&cordova plugin add cordovarduino&&cordova run android", 17 | "test": "ng test", 18 | "lint": "ng lint", 19 | "e2e": "ng e2e", 20 | "flogin": "firebase login", 21 | "fdeploy": "firebase deploy", 22 | "deploy-dev": "npm run build:dev && npm run fdeploy", 23 | "deploy-prod": "npm run build:prod && npm run fdeploy" 24 | }, 25 | "private": true, 26 | "dependencies": { 27 | "@angular/animations": "^9.0.0-rc.8", 28 | "@angular/cdk": "^9.0.0-rc.7", 29 | "@angular/common": "~9.0.0-rc.8", 30 | "@angular/compiler": "~9.0.0-rc.8", 31 | "@angular/core": "~9.0.0-rc.8", 32 | "@angular/flex-layout": "^9.0.0-beta.29", 33 | "@angular/forms": "~9.0.0-rc.8", 34 | "@angular/material": "^9.0.0-rc.7", 35 | "@angular/platform-browser": "~9.0.0-rc.8", 36 | "@angular/platform-browser-dynamic": "~9.0.0-rc.8", 37 | "@angular/router": "~9.0.0-rc.8", 38 | "angular-svg-icon": "^8.0.0", 39 | "angular2-websocket": "^0.9.8", 40 | "date-fns": "^2.2.1", 41 | "devextreme": "^19.1.7", 42 | "devextreme-angular": "^19.1.7", 43 | "file-saver": "^2.0.2", 44 | "hammerjs": "^2.0.8", 45 | "lodash": "^4.17.15", 46 | "luxon": "^1.16.0", 47 | "mat-virtual-table": "^9.0.2", 48 | "ng-dialog-animation": "^1.0.0", 49 | "ng-dyna-form": "^9.0.4", 50 | "ngx-material-timepicker": "4.0.2", 51 | "ngx-socket-io": "^3.0.1", 52 | "rxjs": "~6.5.3", 53 | "saturn-datepicker": "^8.0.1", 54 | "socket.io-client": "^2.3.0", 55 | "tslib": "^1.10.0", 56 | "xlsx": "^0.15.1", 57 | "zone.js": "~0.10.2" 58 | }, 59 | "devDependencies": { 60 | "@angular-builders/custom-webpack": "^8.2.0", 61 | "@angular-devkit/build-angular": "~0.900.0-rc.0", 62 | "@angular/cli": "~9.0.0-rc.0", 63 | "@angular/compiler-cli": "~9.0.0-rc.0", 64 | "@angular/language-service": "~9.0.0-rc.0", 65 | "@types/jasmine": "~3.4.0", 66 | "@types/jasminewd2": "~2.0.3", 67 | "@types/lodash": "^4.14.144", 68 | "@types/node": "^12.12.29", 69 | "codelyzer": "^5.1.2", 70 | "jasmine-core": "~3.5.0", 71 | "jasmine-spec-reporter": "~4.2.1", 72 | "karma": "~4.3.0", 73 | "karma-chrome-launcher": "~3.1.0", 74 | "karma-coverage-istanbul-reporter": "~2.1.0", 75 | "karma-jasmine": "~2.0.1", 76 | "karma-jasmine-html-reporter": "^1.4.2", 77 | "protractor": "~5.4.2", 78 | "ts-node": "~8.3.0", 79 | "tslint": "~5.18.0", 80 | "typescript": "3.6.4" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /client/src/app/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AdminComponent } from './admin/admin.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ComponentsModule } from '../shared/components/components.module'; 6 | import { AdminController } from '../../api/admin.controller'; 7 | 8 | @NgModule({ 9 | declarations: [AdminComponent], 10 | imports: [CommonModule, ComponentsModule, RouterModule.forChild([{ path: '', component: AdminComponent }])], 11 | providers: [AdminController, { provide: 'baseUrlI18n', useValue: '../../assets/i18n/login' }], 12 | }) 13 | export class AdminModule {} 14 | -------------------------------------------------------------------------------- /client/src/app/admin/admin/admin.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | edit 7 | 8 | 9 | {{ row.email }} 10 | 11 | {{ row.fullName }} 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /client/src/app/admin/admin/admin.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | height: 100%; 3 | background-color: #f1f2f6; 4 | padding: 20px; 5 | } 6 | .edit { 7 | display: none; 8 | } 9 | .mat-row { 10 | color: #000; 11 | :hover { 12 | .edit { 13 | display: block; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/app/admin/admin/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminComponent } from './admin.component'; 4 | 5 | describe('AdminComponent', () => { 6 | let component: AdminComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AdminComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/admin/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, Optional, ViewEncapsulation } from '@angular/core'; 2 | import { ITopBarModel } from '../../shared/components/topbar/topbar.interface'; 3 | import { AdminController } from '../../../api/admin.controller'; 4 | import { App, Permission, User } from 'shared/models'; 5 | import { ColumnDef } from 'mat-virtual-table'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { FormComponent, FormModel } from 'ng-dyna-form'; 8 | import { MatDialog } from '@angular/material/dialog'; 9 | import { MatSnackBar } from '@angular/material/snack-bar'; 10 | 11 | export interface AdminModel { 12 | app: App; 13 | userFormModel: FormModel; 14 | } 15 | 16 | @Component({ 17 | selector: 'p-admin', 18 | templateUrl: './admin.component.html', 19 | styleUrls: ['./admin.component.scss'], 20 | encapsulation: ViewEncapsulation.None, 21 | }) 22 | export class AdminComponent { 23 | topbarModel: ITopBarModel = { logoutTitle: 'logout', routerLinks: [], menuItems: [] }; 24 | users: User[]; 25 | columns: ColumnDef[] = [ 26 | { field: 'edit', title: ' ', width: '70px', isSortable: false }, 27 | { field: 'email', title: 'מייל' }, 28 | { field: 'company', title: 'חברה' }, 29 | { field: 'name', title: 'שם' }, 30 | { field: 'phone', title: 'טלפון' }, 31 | // { field: 'code', title: 'קוד' }, 32 | { field: 'permission', title: 'הרשאה' }, 33 | { field: 'details', title: 'פרטים נוספים' }, 34 | ]; 35 | 36 | constructor( 37 | private api: AdminController, 38 | private dialog: MatDialog, 39 | private snackBar: MatSnackBar, 40 | private route: ActivatedRoute, 41 | @Optional() @Inject('userEditModel') private readonly model: AdminModel, 42 | ) { 43 | if (!model) { 44 | this.model = { 45 | app: App.tador, 46 | userFormModel: { 47 | fields: [ 48 | { placeHolder: 'אמייל', key: 'email' }, 49 | { placeHolder: 'חברה', key: 'company' }, 50 | { placeHolder: 'שם פרטי', key: 'fName' }, 51 | { placeHolder: 'שם משפחה', key: 'lName' }, 52 | { placeHolder: 'טלפון', key: 'phone' }, 53 | ], 54 | modelConstructor: User, 55 | model: undefined, 56 | }, 57 | }; 58 | } 59 | this.api.users(this.model.app).then(users => (this.users = users)); 60 | } 61 | 62 | openEditUserDialog(id?: string): void { 63 | this.model.userFormModel.model = id 64 | ? { ...this.users.find(user => user.id === id) } 65 | : new User({ roles: [{ app: this.model.app, permission: Permission.user }] }); 66 | 67 | const dialogRef = this.dialog.open(FormComponent, { 68 | width: '80%', 69 | maxWidth: '540px', 70 | data: this.model.userFormModel, 71 | direction: 'rtl', 72 | }); 73 | 74 | dialogRef.afterClosed().subscribe(result => { 75 | if (result) { 76 | const relevant = this.users.find(user => user._id === result._id); 77 | if (!relevant) { 78 | this.users = this.users.concat([new User(result)]); 79 | } else { 80 | Object.keys(result).forEach(key => (relevant[key] = result[key])); 81 | } 82 | this.api.addUser(result).then(saveResult => { 83 | if (!saveResult.ok) { 84 | // tslint:disable-next-line:no-console 85 | console.error('not saved!'); 86 | return; 87 | } 88 | 89 | this.snackBar.open('נשמר', 'בטל', { 90 | duration: 2000, 91 | }); 92 | }); 93 | } 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /client/src/app/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | -------------------------------------------------------------------------------- /client/src/app/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | #login-page { 2 | background-image: linear-gradient(to top, #1e90ff, #70a1ff); 3 | -moz-osx-font-smoothing: grayscale; 4 | width: calc(100vw); 5 | height: calc(100vh); 6 | font-family: 'Roboto', sans-serif; 7 | 8 | .login-main { 9 | width: 470px; 10 | height: 411px; 11 | background: #ffffff; 12 | padding-right: 5em; 13 | padding-left: 5em; 14 | padding-top: 40px; 15 | .login-logo { 16 | margin-bottom: 30px; 17 | text-align: center; 18 | 19 | img { 20 | width: 70px; 21 | height: 70px; 22 | object-fit: contain; 23 | } 24 | } 25 | 26 | ::ng-deep { 27 | .save_button { 28 | text-transform: uppercase; 29 | width: 100%; 30 | padding: 4px; 31 | color: #ffffff; 32 | font-size: 14px; 33 | cursor: pointer; 34 | border-color: #1e90ff; 35 | background: #70a1ff; 36 | } 37 | 38 | p-text-box { 39 | width: 100%; 40 | .mat-form-field-should-float { 41 | .mat-form-field-label { 42 | top: 40px; 43 | } 44 | } 45 | ::ng-deep.mat-form-field-infix { 46 | border-top: 0 !important; 47 | } 48 | 49 | ::ng-deep.mat-form-field-flex { 50 | height: 42px; 51 | } 52 | 53 | ::ng-deep.mat-form-field-appearance-outline .mat-form-field-infix { 54 | padding: 14px 0 0 0 !important; 55 | } 56 | 57 | ::ng-deep .mat-input-element { 58 | padding: 0 0 1px !important; 59 | font-size: 13px !important; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/src/app/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { LoginRequest, signinRequest } from 'shared'; 5 | import { DynaFormBuilder, FormModel } from 'ng-dyna-form'; 6 | import { I18nService } from '../shared/services/i18n.service'; 7 | import { I18nRootObject } from 'src/api/i18n/login.i18n'; 8 | @Component({ 9 | selector: 'app-auth', 10 | templateUrl: './auth.component.html', 11 | styleUrls: ['./auth.component.scss'], 12 | }) 13 | export class AuthComponent { 14 | loginFormModel: FormModel; 15 | signinFormModel: FormModel; 16 | dic: I18nRootObject; 17 | loginError; 18 | state: 'login' | 'signin'; 19 | currentSystem: 'macro' | 'mf'; 20 | settings; 21 | constructor( 22 | private router: Router, 23 | private route: ActivatedRoute, 24 | private dynaFB: DynaFormBuilder, 25 | private authService: AuthService, 26 | public i18nService: I18nService, 27 | ) { 28 | this.settings = { 29 | macro: { 30 | background: '#eeeeee', 31 | button: '#ffc400', 32 | backgroundHover: 'rgba(255, 220, 0, 0.6)', 33 | }, 34 | mf: { 35 | button: '#1e90ff', 36 | backgroundHover: 'rgb(127, 203, 247)', 37 | }, 38 | }; 39 | this.i18nService.dic.subscribe(result => { 40 | this.dic = result as any; 41 | this.loginFormModel = { 42 | appearance: 'outline', 43 | errorTranslations: this.dic.validation, 44 | fields: [ 45 | { key: 'email', placeHolder: this.dic.loginPage.email }, 46 | { placeHolder: this.dic.loginPage.password, key: 'password', type: 'password' }, 47 | ], 48 | modelConstructor: LoginRequest, 49 | formSaveButtonTitle: this.dic.loginPage.login, 50 | }; 51 | this.signinFormModel = { 52 | appearance: 'outline', 53 | errorTranslations: this.dic.validation, 54 | fields: [ 55 | { key: 'email', placeHolder: this.dic.loginPage.email }, 56 | { placeHolder: this.dic.loginPage.password, key: 'password', type: 'password' }, 57 | { placeHolder: this.dic.loginPage.rePassword, key: 'rePassword', type: 'password' }, 58 | ], 59 | modelConstructor: signinRequest, 60 | model: { token: this.route.snapshot.params.token }, 61 | formSaveButtonTitle: this.dic.loginPage.signin, 62 | }; 63 | }); 64 | this.currentSystem = this.route.snapshot.params.site; 65 | this.state = this.route.snapshot.params.state; 66 | } 67 | 68 | login(val) { 69 | this.authService.login(val).then(res => { 70 | if (res.status) { 71 | this.router.navigate([this.route.snapshot.params.site]).then(); 72 | } else { 73 | this.loginError = this.dic.validation.loginFailedMsg; 74 | } 75 | }); 76 | } 77 | 78 | signin(val) { 79 | this.authService.signin(val).then(() => { 80 | this.router.navigate([this.route.snapshot.params.site]).then(); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { AuthComponent } from './auth.component'; 4 | import { ComponentsModule } from 'src/app/shared/components/components.module'; 5 | import { RouterModule } from '@angular/router'; 6 | import { DynaFormModule } from 'ng-dyna-form'; 7 | import { AuthController } from 'src/api/auth.controller'; 8 | import { AngularSvgIconModule } from 'angular-svg-icon'; 9 | 10 | @NgModule({ 11 | declarations: [AuthComponent], 12 | imports: [ 13 | CommonModule, 14 | ComponentsModule, 15 | RouterModule.forChild([ 16 | { path: ':state/:token', component: AuthComponent }, 17 | { path: ':state', component: AuthComponent }, 18 | ]), 19 | DynaFormModule, 20 | AngularSvgIconModule, 21 | ], 22 | providers: [AuthController, { provide: 'baseUrlI18n', useValue: '../../assets/i18n/login' }], 23 | }) 24 | export class AuthModule {} 25 | -------------------------------------------------------------------------------- /client/src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthController } from 'src/api/auth.controller'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | @Injectable() 5 | export class AuthService extends AuthController { 6 | async logout(): Promise<{ status: number }> { 7 | const result = await super.logout(); 8 | window.location.reload(); 9 | return result; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/app/guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router'; 3 | import { AuthService } from './auth/auth.service'; 4 | import { App } from 'shared/models'; 5 | 6 | @Injectable() 7 | export class Guard implements CanActivate { 8 | constructor(private authService: AuthService, private router: Router) {} 9 | async canActivate(route: ActivatedRouteSnapshot) { 10 | const user = await this.authService.getUserAuthenticated(); 11 | const hasPerm = user && user.hasPermission(route.data.app); 12 | if (!hasPerm) { 13 | this.router.navigate([App[route.data.app] + '/auth/login', {}]); 14 | } 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/app.compnent.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { I18nService } from '../shared/services/i18n.service'; 3 | import { AuthService } from '../auth/auth.service'; 4 | import { ITopBarModel } from '../shared/components/topbar/topbar.interface'; 5 | import { App } from 'shared'; 6 | 7 | @Component({ 8 | selector: 'p-root', 9 | template: ` 10 |
11 | 12 |
13 | 14 |
15 |
16 | `, 17 | }) 18 | export class AppComponent { 19 | constructor(public i18nService: I18nService, private authService: AuthService) { 20 | authService.getUserAuthenticated().then(user => { 21 | if(user.hasPermission(App.admin)) { 22 | this.topbarModel.routerLinks.push( {title: 'users', link: 'admin'}) 23 | } 24 | }) 25 | } 26 | inProgress = false; 27 | topbarModel: ITopBarModel = { 28 | logoutTitle: 'logout', 29 | routerLinks: [{title: 'panels', link: 'panels'}], 30 | menuItems: [], 31 | logout: () => this.authService.logout() 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/conf.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | import { ContactNameDirection, Panel } from 'shared/models/tador/panels'; 3 | import { TadorController } from '../../api/tador.controller'; 4 | import * as Panels from 'shared/models/tador/panels'; 5 | import { PanelType } from 'shared/models/tador/enum'; 6 | import {Subject, BehaviorSubject, ReplaySubject} from "rxjs"; 7 | import { AddPanelRequest } from 'shared/models/tador/add-panel-request'; 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ConfService { 12 | constructor(private api: TadorController) { 13 | this.api.initialData().then(data => { 14 | this.data = {panels: data.panels.map(d => new Panels[d.panel.type + 'Panel'](d.panel, d.dump)), users: data.users}; 15 | this.dataSub.next(this.data); 16 | }); 17 | } 18 | data: { users: any; panels: any }; 19 | dataSub: ReplaySubject<{ users: any; panels: any }> = new ReplaySubject(); 20 | 21 | async getPanel(id: string): Promise { 22 | const panel = await this.api.panels(id); 23 | if (!panel) return panel; 24 | const result = new Panels[panel.type + 'Panel'](panel); 25 | return result; 26 | } 27 | 28 | async savePanel(panel: Panel): Promise<{ panel: any; dump: any }> { 29 | return this.api.savePanel(panel); 30 | } 31 | 32 | async addNewPanel(body: AddPanelRequest): Promise { 33 | if (this.data.panels.find((p: Panel) => p.panelId === body.panelId) || await this.getPanel(body.panelId)){ 34 | throw new Error("הפנל כבר קיים במערכת!") 35 | } 36 | 37 | const result = await this.api.addNewPanel(body); 38 | this.data.panels.push(new Panels[result.type + 'Panel'](result)); 39 | this.dataSub.next(this.data); 40 | return result; 41 | } 42 | 43 | async status(panel: Panel): Promise { 44 | return this.api.status(panel); 45 | } 46 | 47 | async removeChanges(panel: Panel): Promise { 48 | return this.api.removeChanges(panel); 49 | } 50 | 51 | async getDefaultFile(type: string, direction: ContactNameDirection): Promise { 52 | return this.api.getDefaultFile(type, direction); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { TadorController } from 'src/api/tador.controller'; 4 | import { IntercomConfComponent } from './intercom-conf/intercom-conf.component'; 5 | import { ContactsComponent } from './intercom-conf/contacts/contacts.component'; 6 | import { NgDialogAnimationService } from 'ng-dialog-animation'; 7 | import { LogsComponent } from './intercom-conf/logs/logs.component'; 8 | import { PanelsComponent } from './panels/panels.component'; 9 | import { ConfService } from './conf.service'; 10 | import { AppComponent } from './app.compnent'; 11 | import { ComponentsModule } from '../shared/components/components.module'; 12 | import { AdminComponent } from '../admin/admin/admin.component'; 13 | import { AdminController } from '../../api/admin.controller'; 14 | @NgModule({ 15 | declarations: [IntercomConfComponent, ContactsComponent, LogsComponent, PanelsComponent, AppComponent, AdminComponent], 16 | imports: [ 17 | ComponentsModule, 18 | RouterModule.forChild([ 19 | { path: '', component: AppComponent, children: [ 20 | { path: 'panels', component: PanelsComponent}, 21 | { path: 'edit/:id', component: IntercomConfComponent}, 22 | // { path: 'admin', loadChildren: 'src/app/admin/admin.module#AdminModule' }, 23 | { path: 'admin', component: AdminComponent}, 24 | 25 | { path: '', redirectTo: 'panels', pathMatch: 'full' } 26 | ] 27 | }, 28 | 29 | ]), 30 | ], 31 | providers: [ 32 | AdminController, 33 | NgDialogAnimationService, 34 | TadorController, 35 | ConfService, 36 | { provide: 'baseUrlI18n', useValue: '../../assets/i18n/login' }, 37 | ], 38 | entryComponents: [LogsComponent] 39 | }) 40 | export class IntercomConfModule {} 41 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/contacts/contacts.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | עברית 7 | 8 | 9 | English 10 | 11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 | 29 | 30 | 31 |
32 |
33 | 34 | {{ row[field.property] }} 35 | 36 |
37 | 38 | 47 | 48 |
49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/contacts/contacts.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | ::ng-deep { 3 | .mat-form-field-infix { 4 | width: auto; 5 | } 6 | .mat-cell { 7 | text-align: start; 8 | } 9 | 10 | } 11 | .upload-btn-wrapper button{ 12 | margin-top: 0 !important; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/contacts/contacts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ContactsComponent } from './contacts.component'; 4 | 5 | describe('ContactsComponent', () => { 6 | let component: ContactsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ContactsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ContactsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/contacts/contacts.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | OnInit, 8 | Output, 9 | ViewChild, 10 | } from '@angular/core'; 11 | import { ContactNameDirection, Contacts, Source } from 'shared/models/tador/panels'; 12 | import { ColumnDef } from 'mat-virtual-table'; 13 | import { XLSXData, XLSXService } from '../../../shared/services/xlsx.service'; 14 | import { fromEvent, Subject } from 'rxjs'; 15 | import { takeUntil } from 'rxjs/operators'; 16 | import { ActionType } from 'shared/models/tador/enum'; 17 | @Component({ 18 | selector: 'p-contacts', 19 | templateUrl: './contacts.component.html', 20 | styleUrls: ['./contacts.component.scss'], 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | }) 23 | export class ContactsComponent implements OnInit { 24 | ActionType = ActionType; 25 | @Input() inProgress: boolean; 26 | ContactNameDirection = ContactNameDirection; 27 | @Input() contacts: Contacts; 28 | @Output() fieldChange = new EventEmitter(); 29 | @Output() sendStatus = new EventEmitter(); 30 | complete$ = new Subject(); 31 | @ViewChild('myFileInput') myFileInput; 32 | @ViewChild('ref', { static: true }) set refField(ref) { 33 | if (!ref) { 34 | return; 35 | } 36 | setTimeout(() => ref.nativeElement.focus()); 37 | } 38 | 39 | contactColumns: ColumnDef[] = [{ field: 'id', title: '', isSortable: false }]; 40 | Source = Source; 41 | 42 | constructor(private xlsxService: XLSXService, private ref: ChangeDetectorRef) { 43 | } 44 | 45 | ngOnInit() { 46 | fromEvent(document, 'keypress').pipe( 47 | takeUntil(this.complete$), 48 | ).subscribe((e: KeyboardEvent) => { 49 | if (this.contacts.nameDirection === ContactNameDirection.LTR && e.keyCode >= 1488) { 50 | e.preventDefault(); 51 | } 52 | }); 53 | 54 | this.contactColumns.push( 55 | ...this.contacts.contactFields.map(f => ({ 56 | field: f.property, 57 | title: f.title, 58 | })), 59 | ); 60 | } 61 | 62 | getColor(index, property: string) { 63 | // console.log(index + property); 64 | const source: Source = this.contacts.changesList 65 | ? this.contacts.changesList[index] 66 | ? this.contacts.changesList[index][property] 67 | : undefined 68 | : undefined; 69 | if (source === Source.client) { 70 | return 'yellow'; 71 | } 72 | if (source === Source.Panel) { 73 | return 'greenyellow'; 74 | } 75 | 76 | if (source === Source.PanelProgress) { 77 | return 'orangered'; 78 | } 79 | } 80 | 81 | signChange(index, field) { 82 | this.contacts.changesList = this.contacts.changesList || []; 83 | this.contacts.changesList[index] = this.contacts.changesList[index] || {}; 84 | this.contacts.changesList[index][field.property] = Source.client; 85 | } 86 | 87 | export() { 88 | const data: XLSXData[] = [{ rows: this.contacts.list }]; 89 | this.xlsxService.export(data, ['contacts'], 'contacts.xlsx'); 90 | } 91 | 92 | handleFileInput(files: FileList) { 93 | const file = files.item(0); 94 | const reader = new FileReader(); 95 | reader.onload = e => { 96 | this.inProgress = true; 97 | const asd: any[] = this.xlsxService.import(reader.result); 98 | const props = this.contacts.contactFields.map(c => c.property); 99 | // tslint:disable-next-line:prefer-for-of 100 | for (let i = 0; i < this.contacts.list.length; i++) { 101 | props.forEach(prop => { 102 | const val = asd[i] ? asd[i][prop] : ''; 103 | this.contacts.list[i][prop] = val; 104 | }); 105 | } 106 | this.contacts.list = [...this.contacts.list]; 107 | this.ref.detectChanges(); 108 | this.myFileInput.nativeElement.value = ''; 109 | this.inProgress = false; 110 | }; 111 | reader.readAsArrayBuffer(file); 112 | } 113 | 114 | // tslint:disable-next-line:use-lifecycle-interface 115 | ngOnDestroy(): void { 116 | this.complete$.next(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/intercom-conf.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .circle { 3 | border-radius: 50%; 4 | width: 18px; 5 | height: 18px; 6 | opacity: .25; 7 | margin: 0 5px; 8 | &.connect{ 9 | background-color: green; 10 | } 11 | &.disconnect{ 12 | background-color: red; 13 | } 14 | } 15 | .snack { 16 | margin: 26px 30px 17px 5px !important; 17 | width: 52vw; 18 | max-width: 100% !important; 19 | } 20 | 21 | .content { 22 | height: 100%; 23 | 24 | background-color: #f1f2f6; 25 | padding: 5px; 26 | 27 | .upload-btn-wrapper { 28 | position: relative; 29 | overflow: hidden; 30 | display: inline-block; 31 | 32 | button { 33 | margin-top: 10%; 34 | } 35 | } 36 | 37 | .upload-btn-wrapper input[type='file'] { 38 | font-size: 100px; 39 | position: absolute; 40 | left: 0; 41 | top: 0; 42 | opacity: 0; 43 | } 44 | 45 | .mat-tab-body-wrapper { 46 | height: 100%; 47 | 48 | .mat-tab-body-content { 49 | // padding: 25px; 50 | height: 100%; 51 | } 52 | 53 | .general_container { 54 | .mat-form-field { 55 | margin: 20px; 56 | } 57 | } 58 | 59 | //div { 60 | // height: 100%; 61 | // 62 | //} 63 | } 64 | } 65 | 66 | .cdk-overlay-pane { 67 | //height: 80%; 68 | max-height: 80%; 69 | 70 | .mat-autocomplete-panel { 71 | //height: 100%; 72 | max-height: none; 73 | } 74 | } 75 | 76 | ::ng-deep { 77 | .mat-dialog-content { 78 | flex-direction: row; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/intercom-conf.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { IntercomConfComponent } from './intercom-conf.component'; 4 | 5 | describe('IntercomConfComponent', () => { 6 | let component: IntercomConfComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ IntercomConfComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(IntercomConfComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/logs/logs.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{log.time | date : "HH:MM:SS"}} 4 | 5 | {{log.type}} 6 | 7 | {{log.msg}} 8 | 9 | {{log.result}} 10 |
11 |
12 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/logs/logs.component.scss: -------------------------------------------------------------------------------- 1 | :host{ 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/logs/logs.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LogsComponent } from './logs.component'; 4 | 5 | describe('LogsComponent', () => { 6 | let component: LogsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LogsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LogsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/intercom-conf/logs/logs.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'p-logs', 6 | templateUrl: './logs.component.html', 7 | styleUrls: ['./logs.component.scss'] 8 | }) 9 | export class LogsComponent { 10 | constructor( @Inject(MAT_DIALOG_DATA) public logs: any) { } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/panels/panels.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 | 15 | 16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/panels/panels.component.scss: -------------------------------------------------------------------------------- 1 | :host{ 2 | height: 100%; 3 | .content { 4 | height: 100%; 5 | background-color: #f1f2f6; 6 | padding: 20px; 7 | } 8 | 9 | ::ng-deep.mat-row { 10 | color: #000; 11 | .buttons { 12 | display: none !important; 13 | } 14 | 15 | :hover { 16 | .buttons{ 17 | display: flex !important; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/panels/panels.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PanelsComponent } from './panels.component'; 4 | 5 | describe('PanelsComponent', () => { 6 | let component: PanelsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PanelsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PanelsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/intercom-conf/panels/panels.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { ConfService } from '../conf.service'; 3 | import { ContactNameDirection, MPPanel, Panel } from 'shared/models/tador/panels'; 4 | import { ColumnDef } from 'mat-virtual-table'; 5 | import { FormComponent, FormModel } from 'ng-dyna-form'; 6 | import { AddPanelRequest } from 'shared/models/tador/add-panel-request'; 7 | import { MatSnackBar } from '@angular/material/snack-bar'; 8 | import { NgDialogAnimationService } from 'ng-dialog-animation'; 9 | 10 | @Component({ 11 | selector: 'p-panels', 12 | templateUrl: './panels.component.html', 13 | styleUrls: ['./panels.component.scss'] 14 | }) 15 | export class PanelsComponent implements OnInit { 16 | constructor(public api: ConfService, 17 | private dialog: NgDialogAnimationService, 18 | private snackBar: MatSnackBar, 19 | private ref: ChangeDetectorRef) { } 20 | panels: Panel[]; 21 | columns: ColumnDef[] = [ 22 | { field: 'edit', title: ' ', width: '150px', isSortable: false }, 23 | { field: 'userId', title: 'טכנאי' }, 24 | { field: 'panelId', title: 'מזהה פנל' }, 25 | { field: 'phoneNumber', title: 'טלפון פנל' }, 26 | { field: 'Address', title: 'כתובת' }, 27 | { field: 'contactName', title: 'שם איש קשר' }, 28 | { field: 'contactPhone', title: 'טלפון איש קשר' }, 29 | { field: 'type', title: 'סוג' }, 30 | { field: 'version', title: 'גירסה' }, 31 | ]; 32 | 33 | addPanelFormModel: FormModel = { 34 | fields: [ 35 | { placeHolder: 'iemi', key: 'panelId' }, 36 | { placeHolder: 'id', key: 'id' }, 37 | { placeHolder: 'Lang', key: 'direction', type: 'radio', 38 | options: [{title: 'Hebrew', value: ContactNameDirection.RTL}, {title: 'English', value: ContactNameDirection.LTR}]}, 39 | {key: 'address', placeHolder: 'כתובת'}, 40 | {key: 'contactName', placeHolder: 'שם איש קשר'}, 41 | {key: 'contactPhone', placeHolder: 'טלפון איש קשר'}, 42 | {key: 'phoneNumber', placeHolder: 'טלפון פנל'}, 43 | ], 44 | modelConstructor: AddPanelRequest, 45 | formTitle: 'הוספת פאנל', 46 | model: new AddPanelRequest(), 47 | }; 48 | 49 | editPanelFormModel: FormModel = { 50 | fields: [ 51 | {key: 'address', placeHolder: 'כתובת'}, 52 | {key: 'contactName', placeHolder: 'שם איש קשר'}, 53 | {key: 'contactPhone', placeHolder: 'טלפון איש קשר'}, 54 | {key: 'phoneNumber', placeHolder: 'טלפון פנל'}, 55 | ], 56 | modelConstructor: Panel, 57 | formTitle: 'עריכת פאנל', 58 | }; 59 | 60 | ngOnInit() { 61 | this.api.dataSub.subscribe(async data => { 62 | this.panels = undefined; 63 | this.ref.detectChanges(); 64 | this.panels = data.panels; 65 | this.ref.detectChanges(); 66 | }); 67 | } 68 | 69 | openEditPanelDialog(panel) { 70 | const model = this.editPanelFormModel; 71 | model.model = panel; 72 | const dialogRef = this.dialog.open(FormComponent, { 73 | width: '80%', 74 | maxWidth: '450px', 75 | height: '350px', 76 | data: model, 77 | direction: 'rtl', 78 | }); 79 | 80 | dialogRef.afterClosed().subscribe(result => { 81 | if (!result) {return; } 82 | this.api.savePanel(result).then((saveResult: any) => { 83 | if (!saveResult.ok) { 84 | // tslint:disable-next-line:no-console 85 | console.error('not saved!'); 86 | return; 87 | } 88 | 89 | this.snackBar.open('נשמר', 'בטל', { 90 | duration: 2000, 91 | }); 92 | }); 93 | }); 94 | } 95 | openCreatePanelDialog() { 96 | const model = this.addPanelFormModel; 97 | 98 | const dialogRef = this.dialog.open(FormComponent, { 99 | width: '80%', 100 | maxWidth: '540px', 101 | data: model 102 | }); 103 | 104 | dialogRef.afterClosed().subscribe(result => { 105 | if (!result) {return; } 106 | result = Object.assign(new AddPanelRequest(), result); 107 | if (!result.isMatch) { 108 | this.addPanelFormModel.model = result; 109 | return this.openCreatePanelDialog(); 110 | } 111 | 112 | this.api.addNewPanel(result).then(saveResult => { 113 | this.snackBar.open('נשמר', 'בטל', { 114 | duration: 2000, 115 | }); 116 | }).catch(e =>{ 117 | this.snackBar.open(e, 'סבבה', { 118 | duration: 10000, 119 | }); 120 | }); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /client/src/app/shared/components/content/content.component.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 | {{ moreTitle }} 5 | {{ lessTitle }} 6 | -------------------------------------------------------------------------------- /client/src/app/shared/components/content/content.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | z-index: 1; 3 | white-space: pre-wrap; 4 | overflow: auto; 5 | color: white; 6 | p { 7 | white-space: pre-wrap; 8 | } 9 | a { 10 | color: #feffdf; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/shared/components/content/content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ContentComponent } from './content.component'; 4 | 5 | describe('ContentComponent', () => { 6 | let component: ContentComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ContentComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ContentComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/content/content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'p-section-content', 5 | templateUrl: './content.component.html', 6 | styleUrls: ['./content.component.scss'], 7 | }) 8 | export class ContentComponent { 9 | @Input() moreTitle: string; 10 | @Input() lessTitle: string; 11 | @Input() content: string; 12 | @Input() contentMore: string; 13 | @Input() contentMoreFooter: string; 14 | @Output() open = new EventEmitter(); 15 | @Output() close = new EventEmitter(); 16 | more = false; 17 | ngOnChanges() { 18 | this.more = false; 19 | this.close.emit(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/autocomplete/autocomplete.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ title }} 3 |
4 | 5 | 10 | {{ selectedOption.name }} 11 | cancel 12 | 13 | 14 | 15 | 23 |
24 | 30 | 36 |
37 | {{ option.name }} 38 |
39 | clear 40 |
41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/autocomplete/autocomplete.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | } 4 | 5 | .selected { 6 | background: rgba(0, 0, 0, 0.04) !important; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/autocomplete/autocomplete.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, ComponentFixture } from '@angular/core/testing'; 2 | import { Component } from '@angular/core'; 3 | import { MaterialModule } from '../../material/material.module'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { ComponentsModule } from '../../components.module'; 6 | import { Filter } from 'shared'; 7 | @Component({ 8 | selector: `host-component`, 9 | template: ` 10 | 11 | `, 12 | }) 13 | class TestHostComponent { 14 | settings = { 15 | placeholder: 'placeholder', 16 | options: [{ _id: 1, name: 'name1' }, { _id: 2, name: 'name2' }], 17 | selected: { _id: 2, name: 'name2' }, 18 | }; 19 | } 20 | 21 | describe('autocomplete Component', () => { 22 | let component: TestHostComponent; 23 | let fixture: ComponentFixture; 24 | beforeEach(async(() => { 25 | TestBed.configureTestingModule({ 26 | declarations: [TestHostComponent], 27 | imports: [ 28 | MaterialModule, 29 | BrowserAnimationsModule, 30 | ComponentsModule, 31 | ], 32 | }).compileComponents(); 33 | })); 34 | 35 | beforeEach(() => { 36 | fixture = TestBed.createComponent(TestHostComponent); 37 | component = fixture.componentInstance; 38 | fixture.detectChanges(); 39 | }); 40 | 41 | it('should show placeholder', () => { 42 | const el: HTMLElement = fixture.nativeElement; 43 | fixture.detectChanges(); 44 | const input = el.querySelector('.mat-input-element'); 45 | 46 | expect(input.getAttribute('placeholder')).toEqual('placeholder'); 47 | }); 48 | 49 | it('should show placeholder', () => { 50 | const el: HTMLElement = fixture.nativeElement; 51 | fixture.detectChanges(); 52 | const input = el.querySelector('.mat-input-element'); 53 | expect(input.getAttribute('placeholder')).toEqual('placeholder'); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/base.component.ts: -------------------------------------------------------------------------------- 1 | import { Input, Output, EventEmitter, KeyValueDiffers, DoCheck, KeyValueDiffer } from '@angular/core'; 2 | import { Filter } from 'shared'; 3 | 4 | export class BaseFilterComponent implements DoCheck { 5 | get isDisabled() { 6 | return !this.settings.lastChange && this.settings.isDisabled; 7 | } 8 | @Input() set options(ops) { 9 | this._options = ops; 10 | if (this.options.filter(o => !o.isDisabled).length === 1) { 11 | // this.settings.selected = this.options.filter(o => !o.isDisabled); 12 | } 13 | } 14 | get options() { 15 | return this._options || this.settings.options || this.settings._options; 16 | } 17 | 18 | get placeholder() { 19 | return ( 20 | this.dic.placeholders[this.settings.placeholder] || 21 | this.settings.placeholder || 22 | this.dic.placeholders[this.settings.optionIdPath] || 23 | this.settings.optionIdPath || 24 | '' 25 | ); 26 | } 27 | 28 | get title() { 29 | return this.dic.titles[this.settings.placeholder] || this.dic.titles[this.settings.optionIdPath] || ''; 30 | } 31 | 32 | constructor(private _differs: KeyValueDiffers) {} 33 | @Input() set settings(settings: Filter) { 34 | this._settings = settings; 35 | if (!this._differ && settings) { 36 | this._differ = this._differs.find(settings).create(); 37 | } 38 | } 39 | get settings(): Filter { 40 | return this._settings; 41 | } 42 | @Input() dic = { placeholders: {}, titles: {} }; 43 | _options; 44 | private _differ: KeyValueDiffer; 45 | _settings: Filter; 46 | @Output() selectedChange = new EventEmitter(); 47 | ngDoCheck() { 48 | if (this._differ) { 49 | const changes = this._differ.diff(this._settings); 50 | if (changes) { 51 | this.onSettingsChange(changes); 52 | } 53 | } 54 | } 55 | onSettingsChange(changes) {} 56 | optionSelected(val) { 57 | this.settings.lastChange = true; 58 | if (this.settings.isMultiple) { 59 | if (!this.settings.selected) { 60 | this.settings.selected = []; 61 | } 62 | if (Array.isArray(val)) { 63 | this.settings.selected = val.length ? val : undefined; 64 | } else { 65 | this.settings.selected.push(val); 66 | } 67 | } else { 68 | this.settings.selected = val; 69 | } 70 | if (this.settings.selected) { 71 | delete this.settings.selected._query; 72 | } 73 | this.settings.isActive = !!this.settings.selected; 74 | this.selectedChange.emit(this.settings.selected); 75 | } 76 | 77 | optionDeSelected(val) { 78 | this.settings.selected = this.settings.selected.filter(s => s._id !== val._id); 79 | if (this.settings.isMultiple && !this.settings.selected.length) { 80 | this.settings.selected = undefined; 81 | } 82 | this.selectedChange.emit(this.settings.selected); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/checkbox/checkbox.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CheckboxComponent } from './checkbox.component'; 4 | import { MaterialModule } from '../../material/material.module'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { FlexLayoutModule } from '@angular/flex-layout'; 7 | 8 | describe('CheckboxComponent', () => { 9 | let component: CheckboxComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [CheckboxComponent], 15 | imports: [ 16 | MaterialModule, 17 | BrowserAnimationsModule, 18 | FlexLayoutModule, 19 | ], 20 | }).compileComponents(); 21 | })); 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(CheckboxComponent); 25 | component = fixture.componentInstance; 26 | component.settings = { 27 | _id: '1', 28 | options: [ 29 | { _id: '1', name: 'op1' }, 30 | { _id: '2', name: 'op2' }, 31 | { _id: '3', name: 'op3' }, 32 | ], 33 | } as any; 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/checkbox/checkbox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, KeyValueDiffers, OnInit } from '@angular/core'; 2 | import { BaseFilterComponent } from '../base.component'; 3 | 4 | @Component({ 5 | selector: 'p-checkbox', 6 | template: ` 7 |
8 | {{ placeholder }} 9 | {{ option.name }} 18 |
19 | `, 20 | }) 21 | export class CheckboxComponent extends BaseFilterComponent implements OnInit { 22 | checkedList: any = {}; 23 | constructor(differs: KeyValueDiffers) { 24 | super(differs); 25 | } 26 | 27 | change(checked, option) { 28 | if (!this.settings.isActive) { 29 | this.checkedList = {}; 30 | } 31 | this.checkedList[option._id] = checked; 32 | 33 | if (!checked) { 34 | this.optionDeSelected(option); 35 | } else { 36 | this.optionSelected(option); 37 | } 38 | } 39 | 40 | ngOnInit(): void { 41 | if (this.settings.selected) { 42 | this.settings.selected.forEach(s => (this.checkedList[s._id] = true)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/combobox/combobox.component.html: -------------------------------------------------------------------------------- 1 | {{ placeholder }} 2 | 3 | {{ option.name }} 12 | 13 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/combobox/combobox.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/combobox/combobox.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { ComboboxComponent } from './combobox.component'; 3 | 4 | describe('ComboboxComponent', () => { 5 | let component: ComboboxComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ComboboxComponent], 11 | }).compileComponents(); 12 | })); 13 | 14 | beforeEach(() => { 15 | fixture = TestBed.createComponent(ComboboxComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/combobox/combobox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, KeyValueDiffers } from '@angular/core'; 2 | import { BaseFilterComponent } from '../base.component'; 3 | import { FormControl } from '@angular/forms'; 4 | 5 | @Component({ 6 | selector: 'p-combobox', 7 | templateUrl: './combobox.component.html', 8 | styleUrls: ['./combobox.component.scss'], 9 | }) 10 | export class ComboboxComponent extends BaseFilterComponent { 11 | @Input() labelPosition = 'after'; 12 | constructor(differs: KeyValueDiffers) { 13 | super(differs); 14 | } 15 | input: FormControl = new FormControl(); 16 | change(option) { 17 | this.optionSelected(option); 18 | this.selectedChange.emit(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/dropdown/dropdown.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, ComponentFixture } from '@angular/core/testing'; 2 | import { DropdownComponent } from './dropdown.component'; 3 | import { Component } from '@angular/core'; 4 | import { MaterialModule } from '../../material/material.module'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | @Component({ 8 | selector: `host-component`, 9 | template: ` 10 | 11 | `, 12 | }) 13 | class TestHostComponent { 14 | settings = { 15 | options: [{ _id: 1, name: 'name1' }, { _id: 2, name: 'name2' }], 16 | selected: { _id: 2, name: 'name2' }, 17 | }; 18 | } 19 | 20 | describe('Drodown Component', () => { 21 | let component: TestHostComponent; 22 | let fixture: ComponentFixture; 23 | beforeEach(async(() => { 24 | TestBed.configureTestingModule({ 25 | declarations: [TestHostComponent, DropdownComponent], 26 | imports: [MaterialModule, BrowserAnimationsModule], 27 | }).compileComponents(); 28 | })); 29 | 30 | beforeEach(() => { 31 | fixture = TestBed.createComponent(TestHostComponent); 32 | component = fixture.componentInstance; 33 | fixture.detectChanges(); 34 | }); 35 | 36 | it('should show placeholder', () => { 37 | const el: HTMLElement = fixture.nativeElement; 38 | fixture.detectChanges(); 39 | const p = el.querySelector('.mat-form-field-label'); 40 | expect(p.textContent).toEqual('name2'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/dropdown/dropdown.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, KeyValueDiffers } from '@angular/core'; 2 | import { BaseFilterComponent } from '../base.component'; 3 | 4 | @Component({ 5 | selector: 'p-dropdown', 6 | template: ` 7 | 8 | {{ title }} 9 | 17 | 18 | {{ option.name }} 19 | 20 | 21 | 22 | `, 23 | styles: [':host {width: 100%}'], 24 | }) 25 | export class DropdownComponent extends BaseFilterComponent { 26 | @Input() appearance = 'outline'; 27 | constructor(differs: KeyValueDiffers) { 28 | super(differs); 29 | } 30 | compareFn(a, b) { 31 | return a && b && a.id === b.id; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/filter/filter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FilterComponent } from './filter.component'; 4 | import { MaterialModule } from '../../material/material.module'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { FlexLayoutModule } from '@angular/flex-layout'; 7 | import { CheckboxFilter, DropdownFilter, Filter } from 'shared'; 8 | import { CheckboxComponent } from '../checkbox/checkbox.component'; 9 | import { DropdownComponent } from '../dropdown/dropdown.component'; 10 | import { Component } from '@angular/core'; 11 | 12 | @Component({ 13 | selector: `host-component`, 14 | template: ` 15 | 16 | `, 17 | }) 18 | class TestHostComponent { 19 | filter: Filter; // = { options: [{ _id: 1, name: 'name1' }, { _id: 2, name: 'name2' }], selected: { _id: 2, name: 'name2' } }; 20 | constructor() { 21 | // tslint:disable-next-line: max-line-length 22 | this.filter = new CheckboxFilter({ 23 | options: [{ _id: '1', name: 'name1' }, { _id: '2', name: 'name2' }], 24 | selected: { _id: '2', name: 'name2' }, 25 | }); 26 | } 27 | } 28 | 29 | describe('FilterComponent', () => { 30 | let component: TestHostComponent; 31 | let fixture: ComponentFixture; 32 | 33 | beforeEach(async(() => { 34 | TestBed.configureTestingModule({ 35 | declarations: [ 36 | FilterComponent, 37 | CheckboxComponent, 38 | DropdownComponent, 39 | TestHostComponent, 40 | ], 41 | imports: [ 42 | MaterialModule, 43 | BrowserAnimationsModule, 44 | FlexLayoutModule, 45 | ], 46 | }).compileComponents(); 47 | })); 48 | 49 | beforeEach(() => { 50 | fixture = TestBed.createComponent(TestHostComponent); 51 | component = fixture.componentInstance; 52 | }); 53 | 54 | it('should create check box', () => { 55 | component.filter = new CheckboxFilter({ 56 | options: [{ _id: '1', name: 'name1' }, { _id: '2', name: 'name2' }], 57 | selected: { _id: '2', name: 'name2' }, 58 | }); 59 | fixture.detectChanges(); 60 | const elcheckbox = fixture.debugElement.nativeElement.querySelector( 61 | 'p-checkbox' 62 | ); 63 | const eldropdown = fixture.debugElement.nativeElement.querySelector( 64 | 'p-dropdown' 65 | ); 66 | expect(elcheckbox).toBeDefined(); 67 | expect(eldropdown).toBeNull(); 68 | }); 69 | 70 | it('should create dropdown box', () => { 71 | component.filter = new DropdownFilter({ 72 | options: [{ _id: '1', name: 'name1' }, { _id: '2', name: 'name2' }], 73 | selected: { _id: '2', name: 'name2' }, 74 | }); 75 | fixture.detectChanges(); 76 | const elcheckbox = fixture.debugElement.nativeElement.querySelector( 77 | 'p-checkbox' 78 | ); 79 | const eldropdown = fixture.debugElement.nativeElement.querySelector( 80 | 'p-dropdown' 81 | ); 82 | expect(eldropdown).toBeDefined(); 83 | expect(elcheckbox).toBeNull(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/filter/filter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Filter } from 'shared'; 3 | import * as Filters from 'shared/models/filter.model'; 4 | 5 | @Component({ 6 | selector: 'p-filter', 7 | styles: [':host {height: 100%;width: 100%;}'], 8 | template: ` 9 | 15 | 21 | 27 | 33 | 39 | 45 | `, 46 | }) 47 | export class FilterComponent { 48 | @Input() filter: Filter; 49 | @Input() dic = { placeholder: {}, titles: {} }; 50 | @Output() selectedChange = new EventEmitter(); 51 | filters = Filters; 52 | } 53 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/quantity-filter/quantity-filter.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ placeholder }} 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/quantity-filter/quantity-filter.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/quantity-filter/quantity-filter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { QuantityFilterComponent } from './quantity-filter.component'; 4 | 5 | describe('QuantityFilterComponent', () => { 6 | let component: QuantityFilterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ QuantityFilterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(QuantityFilterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/quantity-filter/quantity-filter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, KeyValueDiffers } from '@angular/core'; 2 | import { BaseFilterComponent } from '../base.component'; 3 | 4 | @Component({ 5 | selector: 'p-quantity-filter', 6 | templateUrl: './quantity-filter.component.html', 7 | styleUrls: ['./quantity-filter.component.scss'], 8 | }) 9 | export class QuantityFilterComponent extends BaseFilterComponent { 10 | constructor(differs: KeyValueDiffers) { 11 | super(differs); 12 | } 13 | flag = false; 14 | onValueChanged(ev) { 15 | // if (!ev.event) return; 16 | if (this.flag) { 17 | this.flag = false; 18 | return; 19 | } 20 | this.optionSelected(ev.value); 21 | } 22 | 23 | dataSourceChange() { 24 | this.flag = true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter-grid/special-filter-grid.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | 8 |
9 | 10 |
11 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter-grid/special-filter-grid.component.scss: -------------------------------------------------------------------------------- 1 | #special-filter-grid { 2 | .gridContainer { 3 | width: 470px; 4 | height: 271px; 5 | box-shadow: 0 4px 32px 0 rgba(164, 176, 190, 0.16); 6 | background-color: #f6fbff; 7 | position: absolute; 8 | margin: 0 18.4%; 9 | top: 50%; 10 | z-index: 2; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter-grid/special-filter-grid.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SpecialFilterGridComponent } from './special-filter-grid.component'; 4 | 5 | describe('SpecialFilterGridComponent', () => { 6 | let component: SpecialFilterGridComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SpecialFilterGridComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SpecialFilterGridComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter-grid/special-filter-grid.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, KeyValueDiffers } from '@angular/core'; 2 | import { BaseFilterComponent } from '../base.component'; 3 | 4 | @Component({ 5 | selector: 'p-special-filter-grid', 6 | templateUrl: './special-filter-grid.component.html', 7 | styleUrls: ['./special-filter-grid.component.scss'], 8 | }) 9 | export class SpecialFilterGridComponent extends BaseFilterComponent { 10 | isOpen = false; 11 | constructor(differs: KeyValueDiffers) { 12 | super(differs); 13 | } 14 | 15 | add(filterGroup: any) { 16 | if (!this.settings.selected) { 17 | this.settings.selected = []; 18 | } 19 | this.settings.selected.push(filterGroup); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter/special-filter.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter/special-filter.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrab/nest-angular/dccc0d5514ac564e0db78d0752657433a71a8f79/client/src/app/shared/components/filters/special-filter/special-filter.component.scss -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter/special-filter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SpecialFilterComponent } from './special-filter.component'; 4 | 5 | describe('SpecialFilterComponent', () => { 6 | let component: SpecialFilterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SpecialFilterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SpecialFilterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/filters/special-filter/special-filter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, KeyValueDiffers, OnChanges, OnInit, SimpleChanges, Input } from '@angular/core'; 2 | import { BaseFilterComponent } from '../base.component'; 3 | import { AutocompleteFilter, Filter } from 'shared/models'; 4 | import { cloneDeep } from 'lodash'; 5 | 6 | @Component({ 7 | selector: 'p-special-filter', 8 | templateUrl: './special-filter.component.html', 9 | styleUrls: ['./special-filter.component.scss'], 10 | }) 11 | export class SpecialFilterComponent extends BaseFilterComponent implements OnInit, OnChanges { 12 | autoSettings: Filter; 13 | constructor(differs: KeyValueDiffers) { 14 | super(differs); 15 | } 16 | onSelect(option: any) { 17 | if (!this.settings.selected) { 18 | this.settings.selected = []; 19 | } 20 | this.settings.isActive = true; 21 | this.settings.selected.unshift(...[option.filter]); 22 | } 23 | filterChange(filter) { 24 | filter.isActive = true; 25 | this.selectedChange.emit(); 26 | } 27 | 28 | ngOnInit(): void { 29 | const options = cloneDeep(this.settings.options); 30 | options.forEach(o => (o.name = this.dic[o.name] || o.name)); 31 | this.autoSettings = new AutocompleteFilter({ options, placeholder: this.settings.placeholder }); 32 | if (this.settings.selected) { 33 | this.settings.selected = this.settings.selected.filter(s => s.selected && s.isActive); 34 | if (!this.settings.selected.length) { 35 | this.settings.selected = undefined; 36 | } 37 | } 38 | } 39 | ngOnChanges(changes: SimpleChanges) { 40 | if (!this.autoSettings || !this.autoSettings.options) { 41 | return; 42 | } 43 | this.autoSettings.options.forEach(o => (o.name = this.dic[o.name] || o.name)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/app/shared/components/keyboard/keyboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
-------------------------------------------------------------------------------- /client/src/app/shared/components/keyboard/keyboard.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrab/nest-angular/dccc0d5514ac564e0db78d0752657433a71a8f79/client/src/app/shared/components/keyboard/keyboard.component.scss -------------------------------------------------------------------------------- /client/src/app/shared/components/keyboard/keyboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { KeyboardComponent } from './keyboard.component'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { MaterialModule } from '../material/material.module'; 6 | 7 | describe('KeyboardComponent', () => { 8 | let component: KeyboardComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [KeyboardComponent], 14 | imports: [MaterialModule], 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(KeyboardComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/src/app/shared/components/keyboard/keyboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'p-keyboard', 5 | templateUrl: './keyboard.component.html', 6 | styleUrls: ['./keyboard.component.scss'], 7 | }) 8 | export class KeyboardComponent { 9 | // tslint:disable-next-line: no-output-on-prefix 10 | @Output() onKeyPress = new EventEmitter(); 11 | keyPress(key) { 12 | this.onKeyPress.emit(key); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/app/shared/components/material/material.module.ts: -------------------------------------------------------------------------------- 1 | import { ScrollingModule } from '@angular/cdk/scrolling'; 2 | import { NgModule } from '@angular/core'; 3 | import { SatDatepickerModule, SatNativeDateModule } from 'saturn-datepicker'; 4 | import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker'; 5 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 6 | import { MatButtonModule } from '@angular/material/button'; 7 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatCardModule } from '@angular/material/card'; 11 | import { MatInputModule } from '@angular/material/input'; 12 | import { MatMenuModule } from '@angular/material/menu'; 13 | import { MatGridListModule } from '@angular/material/grid-list'; 14 | import { MatTabsModule } from '@angular/material/tabs'; 15 | import { MatSelectModule } from '@angular/material/select'; 16 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 17 | import { MatTreeModule } from '@angular/material/tree'; 18 | import { MatTableModule } from '@angular/material/table'; 19 | import { MatCheckboxModule } from '@angular/material/checkbox'; 20 | import { MatExpansionModule } from '@angular/material/expansion'; 21 | import { MatChipsModule } from '@angular/material/chips'; 22 | import { MatSortModule } from '@angular/material/sort'; 23 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 24 | import { MatStepperModule } from '@angular/material/stepper'; 25 | import { MatSliderModule } from '@angular/material/slider'; 26 | import { MatDividerModule } from '@angular/material/divider'; 27 | import { MatRadioModule } from '@angular/material/radio'; 28 | import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; 29 | const modules = [ 30 | MatProgressBarModule, 31 | MatGridListModule, 32 | MatInputModule, 33 | MatFormFieldModule, 34 | MatMenuModule, 35 | MatButtonModule, 36 | MatTabsModule, 37 | MatIconModule, 38 | MatAutocompleteModule, 39 | MatSelectModule, 40 | MatCardModule, 41 | MatSlideToggleModule, 42 | MatTreeModule, 43 | MatTableModule, 44 | MatCheckboxModule, 45 | MatExpansionModule, 46 | MatChipsModule, 47 | MatSortModule, 48 | SatDatepickerModule, 49 | SatNativeDateModule, 50 | MatSnackBarModule, 51 | ScrollingModule, 52 | NgxMaterialTimepickerModule, 53 | MatStepperModule, 54 | MatRadioModule, 55 | MatDividerModule, 56 | MatSliderModule, 57 | MatDialogModule, 58 | ]; 59 | 60 | // @Injectable() 61 | // export class MyDateAdapter extends NativeDateAdapter { 62 | // constructor(matDateLocale: string) { 63 | // super(matDateLocale, new Platform()); 64 | // } 65 | // format(date: Date, displayFormat: any): string { 66 | // return format(date, 'dd/MM/yyyy'); 67 | // } 68 | // } 69 | 70 | @NgModule({ 71 | imports: modules, 72 | exports: modules, 73 | //providers: [{ provide: DateAdapter, useClass: MyDateAdapter, deps: [MAT_DATE_LOCALE] }], 74 | }) 75 | export class MaterialModule {} 76 | -------------------------------------------------------------------------------- /client/src/app/shared/components/multi-slider-range-selector/multi-slider-range-selector.component.html: -------------------------------------------------------------------------------- 1 |
2 |
8 |
13 |
14 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /client/src/app/shared/components/multi-slider-range-selector/multi-slider-range-selector.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 1px; 3 | .thumb { 4 | border-radius: 50%; 5 | margin-right: -7px; 6 | margin-top: -7px; 7 | border: 3px solid rgba(0, 0, 0, 0.26); 8 | width: 14px; 9 | height: 14px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/app/shared/components/multi-slider-range-selector/multi-slider-range-selector.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MultiSliderRangeSelectorComponent } from './multi-slider-range-selector.component'; 4 | 5 | describe('MultiSliderRangeSelectorComponent', () => { 6 | let component: MultiSliderRangeSelectorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MultiSliderRangeSelectorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MultiSliderRangeSelectorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/multi-slider-range-selector/multi-slider-range-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, OnInit, ViewChild, ViewChildren } from '@angular/core'; 2 | import { I18nService } from '../../services/i18n.service'; 3 | import { sum } from 'lodash'; 4 | 5 | @Component({ 6 | selector: 'p-multi-slider-range-selector', 7 | templateUrl: './multi-slider-range-selector.component.html', 8 | styleUrls: ['./multi-slider-range-selector.component.scss'], 9 | }) 10 | export class MultiSliderRangeSelectorComponent implements OnInit { 11 | constructor(private i18Service: I18nService) {} 12 | @ViewChildren('headercell') set headerCells(cells) { 13 | this._headerCells = cells.toArray(); 14 | setTimeout(() => { 15 | this.totalWidth = sum(this._headerCells.map(c => c.nativeElement.clientWidth)); 16 | }); 17 | } 18 | @Input() group; 19 | @Input() color; 20 | private totalWidth: any; 21 | _headerCells: ElementRef[]; 22 | isResizeActive = false; 23 | inMove: boolean; 24 | 25 | ngOnInit() {} 26 | private getTargetX(e) { 27 | const rect = e.currentTarget.getBoundingClientRect(); 28 | return e.clientX - rect.left; 29 | } 30 | resizeTable(event, i) { 31 | const cells = this._headerCells; 32 | const elNextIndex = i + 1; 33 | if (this.inMove || !cells[elNextIndex] || !this.isResizeActive) { 34 | return; 35 | } 36 | this.inMove = true; 37 | const el = cells[i].nativeElement; 38 | const elStartWidth = el.clientWidth; 39 | const startX = event.pageX; 40 | const dir = this.i18Service.dir === 'ltr' ? 1 : -1; 41 | const elNextStartWidth = cells[elNextIndex].nativeElement.clientWidth; 42 | const moveFn = (ev: any) => { 43 | const offset = (ev.pageX - startX) * dir; 44 | if (elStartWidth + offset < 0 || elNextStartWidth - offset < 0) { 45 | return; 46 | } 47 | this.group[i].percent = ((elStartWidth + offset) / this.totalWidth) * 100; 48 | 49 | this.group[elNextIndex].percent = ((elNextStartWidth - offset) / this.totalWidth) * 100; 50 | }; 51 | const upFn = () => { 52 | document.removeEventListener('mousemove', moveFn); 53 | document.removeEventListener('mouseup', upFn); 54 | this.inMove = false; 55 | }; 56 | document.addEventListener('mousemove', moveFn); 57 | document.addEventListener('mouseup', upFn); 58 | } 59 | 60 | mousemove(ev, i) { 61 | if (this.inMove) { 62 | return; 63 | } 64 | this.isResizeActive = false; 65 | const el = ev.currentTarget; 66 | const elWidth = el.clientWidth; 67 | let x = this.getTargetX(ev); 68 | if (this.i18Service.dir === 'rtl') { 69 | x = elWidth - x; 70 | } 71 | if (elWidth - x < 10) { 72 | this.isResizeActive = true; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/src/app/shared/components/nav-menu/menu.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMenuItem { 2 | icon?: string; 3 | title: string; 4 | action: () => void; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/shared/components/nav-menu/nav-menu.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /client/src/app/shared/components/nav-menu/nav-menu.component.scss: -------------------------------------------------------------------------------- 1 | .navbar-toggler .navbar-toggler-icon { 2 | width: 22px; 3 | height: 2px; 4 | vertical-align: middle; 5 | outline: 0; 6 | display: block; 7 | border-radius: 1px; 8 | color: black; 9 | } 10 | 11 | .navbar-toggler .icon-bar { 12 | display: block; 13 | position: relative; 14 | background: #555 !important; 15 | width: 24px; 16 | height: 2px; 17 | border-radius: 1px; 18 | margin: 4px auto; 19 | } 20 | 21 | .navbar-toggler { 22 | padding: 0.25rem 0.75rem; 23 | font-size: 1.25rem; 24 | line-height: 1; 25 | background-color: transparent; 26 | border: 1px solid transparent; 27 | border-radius: 0.25rem; 28 | } -------------------------------------------------------------------------------- /client/src/app/shared/components/nav-menu/nav-menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NavMenuComponent } from './nav-menu.component'; 4 | 5 | describe('NavMenuComponent', () => { 6 | let component: NavMenuComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [NavMenuComponent], 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(NavMenuComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /client/src/app/shared/components/nav-menu/nav-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { IMenuItem } from './menu.interface'; 3 | 4 | @Component({ 5 | selector: 'p-nav-menu', 6 | templateUrl: './nav-menu.component.html', 7 | styleUrls: ['./nav-menu.component.scss'], 8 | }) 9 | export class NavMenuComponent { 10 | @Input() 11 | items: IMenuItem[]; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/shared/components/parameter-picker/parameter-picker.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ title || 'Customize parameters' }} 4 |
5 |
6 |
7 |
8 | {{ dic[group.name] || group.name }} 9 |
10 |
11 |
12 | {{ 13 | dic[parameter[parameterTitleName]] || parameter[parameterTitleName] 14 | }} 15 |
16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /client/src/app/shared/components/parameter-picker/parameter-picker.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/styles/style'; 2 | .text { 3 | } 4 | 5 | ::ng-deep p-parameter-picker { 6 | .dialog { 7 | .text { 8 | @include letter( 9 | $font-size: 17px, 10 | $letter-spacing: 0.2px, 11 | $line-height: normal, 12 | $font-weight: bold, 13 | $font-family: sans-serif, 14 | $color: #2f3542 15 | ); 16 | } 17 | ::ng-deep.mat-checkbox-inner-container { 18 | width: 20px !important; 19 | height: 20px !important; 20 | margin-bottom: 15px !important; 21 | margin-left: 10px !important; 22 | border: solid 1px #ced6e0 !important; 23 | } 24 | ::ng-deep.mat-checkbox-frame { 25 | border-style: none !important; 26 | } 27 | ::ng-deep.mat-checkbox-checkmark-path { 28 | stroke: black !important; 29 | } 30 | 31 | ::ng-deep.mat-checkbox-checked.mat-accent .mat-checkbox-background { 32 | background: none !important; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/app/shared/components/parameter-picker/parameter-picker.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ParameterPickerComponent } from './parameter-picker.component'; 4 | 5 | describe('ParameterPickerComponent', () => { 6 | let component: ParameterPickerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ParameterPickerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ParameterPickerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/parameter-picker/parameter-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, Input, Optional } from '@angular/core'; 2 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'p-parameter-picker', 6 | templateUrl: './parameter-picker.component.html', 7 | styleUrls: ['./parameter-picker.component.scss'], 8 | }) 9 | export class ParameterPickerComponent { 10 | constructor(@Optional() @Inject(MAT_DIALOG_DATA) public data?) { 11 | if (data) { 12 | this.groups = data.groups; 13 | this.bindingParamterName = data.bindingParamterName || this.bindingParamterName; 14 | this.childrenPatameterName = data.childrenPatameterName || this.childrenPatameterName; 15 | this.parameterTitleName = data.parameterTitleName || this.parameterTitleName; 16 | this.dic = data.dic; 17 | this.isSmall = data.isSmall; 18 | this.title = data.title; 19 | } 20 | } 21 | 22 | @Input() title: string; 23 | @Input() groups: any[]; 24 | @Input() dic: any[]; 25 | @Input() childrenPatameterName = 'parameters'; 26 | @Input() bindingParamterName = 'isActive'; 27 | @Input() parameterTitleName = 'name'; 28 | @Input() isSmall = false; 29 | } 30 | -------------------------------------------------------------------------------- /client/src/app/shared/components/topbar/topbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 10 |
11 |
12 | {{ link.title }} 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /client/src/app/shared/components/topbar/topbar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/styles/style'; 2 | 3 | #topbar { 4 | box-shadow: 0 1px 0 0 #dfe4ea; 5 | background-color: #ffffff; 6 | 7 | .logout { 8 | height: 36px; 9 | font-size: 12px !important; 10 | } 11 | 12 | .logo { 13 | width: 133px; 14 | height: 42px; 15 | object-fit: contain; 16 | } 17 | .tab { 18 | cursor: pointer; 19 | @include letter(12px, 0.2px, $font-weight: bold, $line-height: none, $font-family: Cabin, $color: #2f3542); 20 | 21 | a { 22 | text-decoration: none; 23 | } 24 | height: 36px; 25 | } 26 | 27 | .active { 28 | border-bottom: 2px solid #1e90ff; 29 | border-radius: 0px !important; 30 | } 31 | } 32 | .mat-button, 33 | .mat-flat-button, 34 | .mat-icon-button, 35 | .mat-stroked-button { 36 | padding: 0 !important; 37 | margin: 0 25px !important; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/app/shared/components/topbar/topbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { ITopBarModel } from './topbar.interface'; 3 | import { I18nService } from '../../services/i18n.service'; 4 | 5 | @Component({ 6 | selector: 'p-topbar', 7 | templateUrl: './topbar.component.html', 8 | styleUrls: ['./topbar.component.scss'], 9 | }) 10 | export class TopbarComponent implements OnInit { 11 | @Input() model: ITopBarModel; 12 | @Input() inProgress; 13 | ngOnInit(): void { 14 | this.model.logo = 'assets/logo.png'; 15 | // this.model.menuItems.push({ action: () => (this.i18nService.language = 'en'), title: 'en' }); 16 | // this.model.menuItems.push({ action: () => (this.i18nService.language = 'he'), title: 'he' }); 17 | } 18 | constructor(public i18nService: I18nService) {} 19 | } 20 | -------------------------------------------------------------------------------- /client/src/app/shared/components/topbar/topbar.interface.ts: -------------------------------------------------------------------------------- 1 | import { IMenuItem } from '../nav-menu/menu.interface'; 2 | export interface ITopBarModel { 3 | menuItems: IMenuItem[]; 4 | logoutTitle?: string; 5 | logout?: () => any; 6 | routerLinks: Array<{ link: string; title: string }>; 7 | logo?: string; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/shared/components/tree/tree.component.html: -------------------------------------------------------------------------------- 1 |
{{ node.name }}
4 | 5 | 11 |
  • 12 | 13 | 14 |
  • 15 |
    16 | 17 |
  • 18 |
    19 | 24 | 25 |
    26 |
      27 | 28 |
    29 |
  • 30 |
    31 |
    32 | -------------------------------------------------------------------------------- /client/src/app/shared/components/tree/tree.component.scss: -------------------------------------------------------------------------------- 1 | mat-tree { 2 | .selected { 3 | background-color: bisque; 4 | } 5 | .text { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | ul, 11 | li { 12 | margin-top: 0; 13 | margin-bottom: 0; 14 | list-style-type: none; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/app/shared/components/tree/tree.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TreeComponent } from './tree.component'; 4 | 5 | describe('TreeComponent', () => { 6 | let component: TreeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TreeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TreeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/shared/components/tree/tree.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { NestedTreeControl } from '@angular/cdk/tree'; 3 | import { MatTreeNestedDataSource } from '@angular/material/tree'; 4 | interface Node { 5 | name: string; 6 | children?: Node[]; 7 | } 8 | @Component({ 9 | selector: 'p-tree', 10 | templateUrl: './tree.component.html', 11 | styleUrls: ['./tree.component.scss'], 12 | }) 13 | export class TreeComponent { 14 | @Input() set data(data) { 15 | this.dataSource.data = data; 16 | } 17 | treeControl = new NestedTreeControl(node => node.children); 18 | selectedNode; 19 | 20 | @Output() select = new EventEmitter(); 21 | dataSource = new MatTreeNestedDataSource(); 22 | 23 | @Input() itemSize = '40px'; 24 | 25 | @Input() iconMore = 'expand_more'; 26 | @Input() iconless = 'chevron_right'; 27 | hasChild = (_: number, node: Node) => !!node.children && node.children.length > 0; 28 | updateNode(node, selected) { 29 | node.selected = selected; 30 | if (selected) { 31 | this.selectedNode = node; 32 | } else { 33 | this.selectedNode = undefined; 34 | } 35 | // for multy select options 36 | // node.selected = selected; 37 | // if (node.children) { 38 | // node.children.forEach(c => this.updateNode(c, selected)); 39 | // } 40 | } 41 | 42 | nodeSelected(node) { 43 | this.updateNode(node, !node.selected); 44 | this.select.emit(node.selected ? this.selectedNode : undefined); 45 | // this.select.emit({ selected: node.selected, value: node }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/src/app/shared/directives/focus.directive.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // Directive, 3 | // Input, 4 | // EventEmitter, 5 | // ElementRef, 6 | // Inject, 7 | // OnInit, 8 | // } from '@angular/core'; 9 | // 10 | // @Directive({ 11 | // selector: '[p-focus]', 12 | // }) 13 | // export class FocusDirective implements OnInit { 14 | // @Input('p-focus') focusEvent: EventEmitter; 15 | // 16 | // constructor(@Inject(ElementRef) private element: ElementRef) {} 17 | // ngOnInit() { 18 | // this.focusEvent.subscribe(event => { 19 | // this.element.nativeElement.focus(); 20 | // }); 21 | // } 22 | // } 23 | -------------------------------------------------------------------------------- /client/src/app/shared/services/dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, NgZone, Optional, TemplateRef } from '@angular/core'; 2 | import { DialogPosition, MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; 3 | import { ComponentType } from '@angular/cdk/portal'; 4 | import { Subject } from 'rxjs'; 5 | const diractionMap = { left: 'left', right: 'left', top: 'top', bottom: 'top' }; 6 | const multyMap = { left: 1, right: -1, top: 1, bottom: -1 }; 7 | interface MyMatDialogConfig extends MatDialogConfig { 8 | title?: string; 9 | slideAnimation?: { 10 | to: 'aside' | 'top' | 'bottom' | 'left' | 'right'; 11 | incomingOption?: KeyframeAnimationOptions; 12 | outgoingOption?: KeyframeAnimationOptions; 13 | }; 14 | position?: DialogPosition & { rowStart?: string; rowEnd?: string }; 15 | } 16 | 17 | @Injectable() 18 | export class DialogService { 19 | constructor( 20 | private dialog: MatDialog, 21 | private ngZone: NgZone, 22 | @Optional() @Inject('INCOMING_OPTION') private incomingOption?: KeyframeAnimationOptions, 23 | @Optional() @Inject('OUTGOING_OPTION') private outgoingOption?: KeyframeAnimationOptions, 24 | ) {} 25 | open( 26 | componentOrTemplateRef: ComponentType | TemplateRef, 27 | config?: MyMatDialogConfig, 28 | ): MatDialogRef { 29 | const dir: 'ltr' | 'rtl' = config.direction || (document.querySelectorAll('[dir="rtl"]').length ? 'rtl' : 'ltr'); 30 | config.direction = config.direction || dir; 31 | if (config.slideAnimation) { 32 | if (config.slideAnimation.to === 'aside') { 33 | config.slideAnimation.to = dir === 'rtl' ? 'left' : 'right'; 34 | } 35 | 36 | if (config.position.rowEnd) { 37 | if (dir === 'rtl') { 38 | config.position.right = config.position.rowEnd; 39 | } else { 40 | config.position.left = config.position.rowEnd; 41 | } 42 | } 43 | 44 | if (config.position.rowStart) { 45 | if (dir === 'rtl') { 46 | config.position.left = config.position.rowEnd; 47 | } else { 48 | config.position.right = config.position.rowEnd; 49 | } 50 | } 51 | } 52 | const ref = this.dialog.open(componentOrTemplateRef, config); 53 | const container = document.getElementsByTagName('mat-dialog-container')[0] as HTMLElement; 54 | if (config.title) { 55 | const el = document.createElement('span'); 56 | el.textContent = config.title; 57 | el.className = 'dialogTitle'; 58 | container.prepend(el); 59 | } 60 | if (config.slideAnimation) { 61 | const incomingOption = config.slideAnimation.incomingOption || 62 | this.incomingOption || { duration: 600, easing: 'ease-in' }; 63 | const outgoingOption = config.slideAnimation.outgoingOption || 64 | this.outgoingOption || { duration: 600, easing: 'ease-out' }; 65 | 66 | const to = diractionMap[config.slideAnimation.to]; 67 | const wrapper = document.getElementsByClassName('cdk-global-overlay-wrapper')[0]; 68 | 69 | const animate = (keyframes, options) => { 70 | return wrapper.animate(keyframes, options); 71 | }; 72 | const _afterClosed = new Subject(); 73 | ref.afterClosed = () => { 74 | return _afterClosed.asObservable(); 75 | }; 76 | const closeFunction = ref.close; 77 | const keyFrame100 = {}; 78 | const keyFrame0 = {}; 79 | keyFrame0[to] = 0; 80 | keyFrame100[to] = 81 | to === 'top' || to === 'bottom' 82 | ? container.clientHeight * multyMap[config.slideAnimation.to] + 'px' 83 | : container.clientWidth * multyMap[config.slideAnimation.to] + 'px'; 84 | 85 | animate([keyFrame100, keyFrame0], incomingOption); 86 | const closeHandler = (dialogResult?: R) => { 87 | _afterClosed.next(); 88 | const animation = animate([keyFrame0, keyFrame100], outgoingOption); 89 | animation.onfinish = () => { 90 | (wrapper as HTMLElement).style.display = 'none'; 91 | this.ngZone.run(() => ref.close(dialogResult)); 92 | }; 93 | ref.close = closeFunction; 94 | }; 95 | ref.close = (dialogResult?: R) => closeHandler(dialogResult); 96 | } 97 | 98 | return ref; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /client/src/app/shared/services/i18n.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import { ReplaySubject } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class I18nService { 6 | constructor(@Inject('baseUrlI18n') private baseUrl: string) { 7 | const storedLanguage = localStorage.getItem('language') as 'en' | 'he'; 8 | this.language = storedLanguage || 'he'; 9 | } 10 | dic = new ReplaySubject(); 11 | // tslint:disable-next-line:variable-name 12 | private _language: 'en' | 'he'; 13 | dir: 'rtl' | 'ltr' = 'rtl'; 14 | get language() { 15 | return this._language; 16 | } 17 | set language(value: 'en' | 'he') { 18 | this._language = value; 19 | localStorage.setItem('language', value); 20 | this.dir = value === 'he' ? 'rtl' : 'ltr'; 21 | fetch(this.baseUrl + '/' + value + '.json').then(async res => { 22 | this.dic.next(await res.json()); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/src/app/shared/services/interceptors.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; 3 | import { environment } from '../../../environments/environment'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class InterceptorsService implements HttpInterceptor { 8 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 9 | const proxyReq = req.clone({ url: req.url.endsWith('svg') ? req.url : environment.apiUrl + '/' + req.url }); 10 | return next.handle(proxyReq); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/shared/services/xlsx.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as XLSX from 'xlsx'; 3 | import { saveAs } from 'file-saver'; 4 | export class XLSXData { 5 | rows: any[]; 6 | description?: {}; 7 | title?: string; 8 | } 9 | type AOA = any[][]; 10 | @Injectable() 11 | export class XLSXService { 12 | constructor() {} 13 | private wopts: XLSX.WritingOptions = { bookType: 'xlsx', type: 'binary' }; 14 | 15 | private s2ab(s: string): ArrayBuffer { 16 | const buf: ArrayBuffer = new ArrayBuffer(s.length); 17 | const view: Uint8Array = new Uint8Array(buf); 18 | // tslint:disable-next-line: no-bitwise 19 | for (let i = 0; i !== s.length; ++i) { 20 | view[i] = s.charCodeAt(i) & 0xff; 21 | } 22 | return buf; 23 | } 24 | 25 | export(sheetsData: XLSXData[], sheetNames: string[], fileName): void { 26 | const wb: XLSX.WorkBook = XLSX.utils.book_new(); 27 | wb.Workbook = { Views: sheetNames.map(s => ({ RTL: true })) }; 28 | sheetsData.forEach((data, i) => { 29 | if (!data.rows[0]) { 30 | return; 31 | } 32 | // Auto Fit Column Widths 33 | const colsWidth = []; 34 | Object.keys(data.rows[0]).forEach((key, i) => { 35 | if (typeof data.rows[0][key].getMonth === 'function') { 36 | colsWidth[i] = { wpx: 70 }; 37 | return; 38 | } 39 | const items = data.rows.map(r => (r[key] ? r[key].toString() : '')); 40 | items.push(key); 41 | const maxStrLength = items.reduce((a, b) => (a.length > b.length ? a : b)).length; 42 | colsWidth[i] = { wch: maxStrLength + 1 }; 43 | }); 44 | 45 | const aoa: AOA = new Array(); // = _.keys(data.rows[0]).concat(['','','','','','',]) 46 | const merges = []; 47 | if (data.title) { 48 | aoa.push([data.title]); 49 | merges.push({ s: { r: 0, c: 0 }, e: { r: 0, c: 15 } }); 50 | } 51 | 52 | aoa.push( 53 | Object.keys(data.rows[0]) 54 | .map(key => key) 55 | .concat(['', '', '', '', '', '']), 56 | ); 57 | data.rows.forEach((v, i) => { 58 | aoa.push(Object.values(data.rows[i]).concat(['', '', '', '', '', ''])); 59 | }); 60 | if (data.description) { 61 | Object.keys(data.description).forEach((key, i) => { 62 | aoa[1 + i][aoa[0].length - 3] = key; 63 | aoa[1 + i][aoa[0].length - 2] = data.description[key]; 64 | }); 65 | } 66 | const sheet = XLSX.utils.aoa_to_sheet(aoa, { cellDates: true }); 67 | sheet['!cols'] = colsWidth; 68 | sheet['!merges'] = merges; 69 | XLSX.utils.book_append_sheet(wb, sheet, sheetNames[i]); 70 | // wb.Sheets[sheetNames[i]] = sheet; 71 | }); 72 | /* save to file */ 73 | const wbout: string = XLSX.write(wb, this.wopts); 74 | saveAs(new Blob([this.s2ab(wbout)]), fileName); 75 | } 76 | 77 | import(arrayBuffer) { 78 | const data = new Uint8Array(arrayBuffer); 79 | const arr = new Array(); 80 | for (let i = 0; i !== data.length; ++i) { arr[i] = String.fromCharCode(data[i]); } 81 | const bstr = arr.join(''); 82 | const workbook = XLSX.read(bstr, {type: 'binary'}); 83 | const worksheet = workbook.Sheets[workbook.SheetNames[0]]; 84 | return XLSX.utils.sheet_to_json(worksheet, {raw: true}); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /client/src/app/starter.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Component } from '@angular/core'; 3 | import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | import { Guard } from './guard'; 7 | import { App } from 'shared'; 8 | import { AuthService } from './auth/auth.service'; 9 | import { InterceptorsService } from './shared/services/interceptors.service'; 10 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 11 | import { APIService } from 'src/api/http.service'; 12 | import { GestureConfig } from '@angular/material/core'; 13 | import { NgDialogAnimationService } from 'ng-dialog-animation'; 14 | const isCordovaApp = Object(window).cordova != null; 15 | @Component({ 16 | selector: 'p-root', 17 | template: '', 18 | }) 19 | export class AppComponent {} 20 | 21 | const routes: Routes = [ 22 | { path: ':site/auth', loadChildren: () => import('src/app/auth/auth.module').then(m => m.AuthModule) }, 23 | { 24 | path: 'tador', 25 | loadChildren: () => import('src/app/intercom-conf/intercom-conf.module').then(m => m.IntercomConfModule), 26 | canActivate: [Guard], 27 | data: { app: App.tador }, 28 | }, 29 | { path: '', redirectTo: 'tador', pathMatch: 'full' }, 30 | { path: '**', redirectTo: 'tador' }, 31 | ]; 32 | 33 | @NgModule({ 34 | declarations: [AppComponent], 35 | imports: [BrowserModule, BrowserAnimationsModule, RouterModule.forRoot(routes, { useHash: isCordovaApp }), HttpClientModule], 36 | bootstrap: [AppComponent], 37 | providers: [ 38 | NgDialogAnimationService, 39 | Guard, 40 | AuthService, 41 | APIService, 42 | { 43 | provide: HTTP_INTERCEPTORS, 44 | useClass: InterceptorsService, 45 | multi: true, 46 | }, 47 | { provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig }, 48 | ], 49 | }) 50 | export class AppModule {} 51 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrab/nest-angular/dccc0d5514ac564e0db78d0752657433a71a8f79/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/src/assets/i18n/login/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "loginPage": { 3 | "login": "login", 4 | "signin": "signin", 5 | "email": "email", 6 | "password": "password", 7 | "rePassword": "rePassword" 8 | }, 9 | "validation": { 10 | "required": "required", 11 | "loginFailedMsg": "user name or password is incorrect" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/assets/i18n/login/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "loginPage": { 3 | "login": "כניסה", 4 | "signin": "שמור", 5 | "email": "שם משתמש", 6 | "password": "סיסמא", 7 | "rePassword": "הקש ססמה שנית" 8 | }, 9 | "validation": { 10 | "required": "required", 11 | "loginFailedMsg": "שם המשתמש או הסיסמא שגויים" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/assets/i18n/tador/en.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /client/src/assets/i18n/tador/he.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrab/nest-angular/dccc0d5514ac564e0db78d0752657433a71a8f79/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/assets/styles/material/mat-horizontal-stepper.scss: -------------------------------------------------------------------------------- 1 | .mat-stepper-horizontal { 2 | background-color: #ffffff00; 3 | } 4 | ::ng-deep .mat-horizontal-content-container, 5 | ::ng-deep .mat-stepper-horizontal, 6 | ::ng-deep .mat-horizontal-stepper-content { 7 | height: 100%; 8 | } 9 | ::ng-deep .mat-horizontal-stepper-header-container { 10 | width: 70%; 11 | height: 96px; 12 | align-self: center; 13 | } 14 | 15 | .mat-stepper-horizontal-line { 16 | border-top-style: solid; 17 | } 18 | 19 | ::ng-deep .mat-stepper-horizontal-line { 20 | border-top-width: 4px !important; 21 | width: 70%; 22 | top: 10px !important; 23 | } 24 | 25 | ::ng-deep .mat-horizontal-stepper-header { 26 | padding: 0 10px !important; 27 | .mat-stepper-horizontal-line { 28 | top: 10px !important; 29 | } 30 | .mat-step-label { 31 | position: fixed; 32 | margin-top: 15px; 33 | width: 200px !important; 34 | white-space: pre-wrap; 35 | text-align: center; 36 | } 37 | &::before { 38 | width: 0; 39 | } 40 | } 41 | .selectedIndex0 { 42 | } 43 | @for $i from 1 through 10 { 44 | .selectedIndex#{$i} { 45 | @extend .selectedIndex#{$i - 1} !optional; 46 | ::ng-deep .mat-stepper-horizontal-line:nth-child(#{$i * 2}) { 47 | border-top-width: 4px !important; 48 | border-top-color: #1e90ff !important; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/src/assets/styles/material/mat-slide-toggle.scss: -------------------------------------------------------------------------------- 1 | .mat-slide-toggle { 2 | .mat-slide-toggle-thumb-container { 3 | top: 0.5px; 4 | width: 16px; 5 | height: 16px; 6 | 7 | .mat-slide-toggle-thumb { 8 | background-color: #ffffff !important; 9 | height: 16px; 10 | width: 16px; 11 | } 12 | } 13 | 14 | .slideToggleText { 15 | font-size: 12px; 16 | letter-spacing: 0.2px; 17 | } 18 | 19 | .mat-slide-toggle-bar { 20 | width: 36px; 21 | height: 18px; 22 | border-radius: 10px; 23 | } 24 | 25 | &:not(.mat-checked) { 26 | .mat-slide-toggle-bar { 27 | background-color: #ced6e0 !important; 28 | [dir='rtl'] .mat-slide-toggle-thumb-container { 29 | left: auto !important; 30 | right: 1px !important; 31 | } 32 | } 33 | } 34 | 35 | &.mat-checked { 36 | .mat-slide-toggle-bar { 37 | background-color: #1e90ff !important; 38 | [dir='rtl'] .mat-slide-toggle-thumb-container { 39 | left: auto !important; 40 | right: 3px !important; 41 | } 42 | } 43 | } 44 | } 45 | 46 | [dir='rtl'] .mat-slide-toggle { 47 | &:not(.mat-checked) { 48 | .mat-slide-toggle-bar { 49 | .mat-slide-toggle-thumb-container { 50 | left: auto !important; 51 | right: 1px !important; 52 | } 53 | } 54 | } 55 | 56 | &.mat-checked { 57 | .mat-slide-toggle-bar { 58 | .mat-slide-toggle-thumb-container { 59 | left: auto !important; 60 | right: 3px !important; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/src/assets/styles/material/style.scss: -------------------------------------------------------------------------------- 1 | @import './mat-horizontal-stepper'; 2 | @import './mat-slide-toggle'; 3 | -------------------------------------------------------------------------------- /client/src/assets/styles/style.scss: -------------------------------------------------------------------------------- 1 | @mixin letter($font-size, $letter-spacing, $line-height: normal, $font-weight: normal, $font-family: Poppins, $color: #feffdf) { 2 | font-family: $font-family; 3 | font-size: $font-size; 4 | font-weight: $font-weight; 5 | font-style: normal; 6 | font-stretch: normal; 7 | line-height: $line-height; 8 | letter-spacing: $letter-spacing; 9 | color: $color; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | apiUrl: 'http://178.62.237.25:3000', 4 | socketUrl: 'http://178.62.237.25:4001', 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: 'http://128.199.41.162:3000', 4 | socketUrl: 'http://128.199.41.162:4001', 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | inBrowser: true, 8 | apiUrl: 'http://localhost:3000', 9 | socketUrl: 'http://localhost:4001', 10 | }; 11 | 12 | /* 13 | * For easier debugging in development mode, you can import the following file 14 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 15 | * 16 | * This import should be commented out in production mode because it will have a negative impact 17 | * on performance if an error is thrown. 18 | */ 19 | import 'zone.js/dist/zone-error'; // Included with Angular CLI. 20 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrab/nest-angular/dccc0d5514ac564e0db78d0752657433a71a8f79/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tador 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 36 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/starter.module'; 5 | import { environment } from './environments/environment'; 6 | import 'reflect-metadata'; 7 | import './polyfills'; 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | // let onDeviceReady = () => { 13 | platformBrowserDynamic() 14 | .bootstrapModule(AppModule) 15 | .catch(err => console.error(err)); 16 | // }; 17 | // document.addEventListener('deviceready', onDeviceReady, false); 18 | -------------------------------------------------------------------------------- /client/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | import 'hammerjs'; 64 | (window as any).global = window; 65 | -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | @import './assets/styles/material/style'; 3 | @include mat-core(); 4 | @import url(//fonts.googleapis.com/earlyaccess/opensanshebrew.css); 5 | $candy-app-primary: mat-palette($mat-blue, A200, A100, A400); 6 | $candy-app-accent: mat-palette($mat-grey); 7 | 8 | // The warn palette is optional (defaults to red). 9 | $candy-app-warn: mat-palette($mat-red); 10 | 11 | // Create the theme object (a Sass map containing all of the palettes). 12 | $candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn); 13 | 14 | // Include theme styles for core and each component used in your app. 15 | // Alternatively, you can import and @include the theme mixins for each component 16 | // that you are using. 17 | @include angular-material-theme($candy-app-theme); 18 | body { 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | ::-webkit-scrollbar { 24 | width: 0px; /* Remove scrollbar space */ 25 | background: transparent; /* Optional: just make scrollbar invisible */ 26 | } 27 | 28 | .center { 29 | position: absolute; 30 | /*it can be fixed too*/ 31 | left: 0; 32 | right: 0; 33 | top: 0; 34 | bottom: 0; 35 | margin: auto; 36 | /*this to solve "the content will not be cut when the window is smaller than the content": */ 37 | max-width: 100%; 38 | max-height: 100%; 39 | } 40 | 41 | * { 42 | font-family: 'Open Sans Hebrew', sans-serif; 43 | } 44 | 45 | mat-tab-group { 46 | width: 100%; 47 | height: 70%; 48 | } 49 | 50 | .mat-step-icon { 51 | width: 20px !important; 52 | height: 20px !important; 53 | border: solid 4px #dfe4ea !important; 54 | background: none !important; 55 | } 56 | .mat-step-icon-selected { 57 | border: solid 4px #1e90ff !important; 58 | } 59 | .mat-step-icon-state-done, 60 | .mat-step-icon-state-edit { 61 | border: solid 4px #1e90ff !important; 62 | background: #1e90ff !important; 63 | } 64 | mat-grid-list { 65 | height: 100%; 66 | } 67 | 68 | // .login-main { 69 | // & .mat-form-field-appearance-outline { 70 | // & .mat-form-field-outline-thick { 71 | // color: #1e90ff; 72 | // } 73 | // } 74 | // & .mat-form-field-flex { 75 | // & .mat-form-field-infix { 76 | // padding: 0.5em 0; 77 | // } 78 | // } 79 | // } 80 | 81 | .mat-expansion-panel-content { 82 | height: 100%; 83 | } 84 | 85 | .mat-expansion-panel-header.mat-expanded { 86 | background: none !important; 87 | } 88 | .mat-expansion-panel-body { 89 | height: 100%; 90 | } 91 | @import '~saturn-datepicker/bundle.css'; 92 | 93 | .mat-dialog-container { 94 | display: flex !important; 95 | flex-direction: column; 96 | .dialogTitle { 97 | margin: 10px 0 10px 0; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": ["src/**/*.d.ts"], 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "paths": { 10 | "shared": ["../shared"], 11 | "shared/*": ["../shared/*"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es6", 14 | "typeRoots": ["node_modules/@types"], 15 | "lib": ["es2018", "dom"], 16 | "paths": { 17 | "shared": ["../shared"], 18 | "shared/*": ["../shared/*"], 19 | "jszip": ["node_modules/jszip/dist/jszip.min.js"] 20 | } 21 | }, 22 | "angularCompilerOptions": { 23 | "enableIvy": false, 24 | "fullTemplateTypeCheck": false, 25 | "strictInjectionParameters": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [true, "attribute", "p", "camelCase"], 13 | "component-selector": [true, "element", "p", "kebab-case"], 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "interface-name": false, 16 | "max-classes-per-file": false, 17 | "max-line-length": [true, 140], 18 | "member-access": false, 19 | "member-ordering": [ 20 | true, 21 | { 22 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 23 | } 24 | ], 25 | "no-consecutive-blank-lines": false, 26 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 27 | "no-empty": false, 28 | "no-inferrable-types": [true, "ignore-params"], 29 | "no-non-null-assertion": true, 30 | "no-redundant-jsdoc": true, 31 | "no-switch-case-fall-through": true, 32 | "no-var-requires": false, 33 | "object-literal-key-quotes": [true, "as-needed"], 34 | "object-literal-sort-keys": false, 35 | "ordered-imports": false, 36 | "quotemark": [true, "single"], 37 | "trailing-comma": false, 38 | "no-conflicting-lifecycle": true, 39 | "no-host-metadata-property": true, 40 | "no-input-rename": true, 41 | "no-inputs-metadata-property": true, 42 | "no-output-native": true, 43 | "no-output-on-prefix": true, 44 | "no-output-rename": true, 45 | "no-outputs-metadata-property": true, 46 | "template-banana-in-box": true, 47 | "template-no-negated-async": true, 48 | "use-lifecycle-interface": true, 49 | "use-pipe-transform-interface": true 50 | }, 51 | "rulesDirectory": ["codelyzer"] 52 | } 53 | -------------------------------------------------------------------------------- /generator/index.ts: -------------------------------------------------------------------------------- 1 | import { generateClientApi } from 'nest-client-generator'; 2 | import { generateClientInterfaces } from 'nest-client-generator'; 3 | 4 | const clientPath = './client/src/api/'; 5 | const serverPath = './server/src'; 6 | const decorators = { 7 | Get: `return new Promise((resolve) => {this.api.get('{url}').subscribe((data: any) => {resolve}); 8 | }); 9 | `, 10 | Post: ` 11 | return new Promise((resolve) => { 12 | this.api.post('{url}'{body}) 13 | .subscribe((data: any) => {resolve}); 14 | })`, 15 | }; 16 | const httpServiceTemplate = ` 17 | import { Injectable } from '@angular/core'; 18 | import { HttpClient } from '@angular/common/http'; 19 | import { map, catchError } from 'rxjs/operators'; 20 | import { denormalize } from 'nosql-normalizer'; 21 | import { throwError } from 'rxjs'; 22 | import { Router } from '@angular/router'; 23 | @Injectable() 24 | export class APIService { 25 | constructor(private httpClient: HttpClient, private router: Router) {} 26 | handleError(error) { 27 | let errorMessage = ''; 28 | if (error.status === 403) { 29 | return (window.location.href += '/login'); 30 | } 31 | if (error.error instanceof ErrorEvent) { 32 | // client-side error 33 | errorMessage = 'Error:' + error.error.message; 34 | } else { 35 | // server-side error 36 | errorMessage = 'Error Code:' + error.status + 'Message:' + error.message; 37 | } 38 | return throwError(errorMessage); 39 | } 40 | 41 | get(url) { 42 | return this.httpClient.get(url, { withCredentials: true }).pipe( 43 | catchError(err => this.handleError(err)), 44 | map(result => denormalize(result)), 45 | ); 46 | } 47 | post(url, body?) { 48 | return this.httpClient.post(url, body, { withCredentials: true }).pipe( 49 | catchError(err => this.handleError(err)), 50 | map(result => denormalize(result)), 51 | ); 52 | } 53 | } 54 | 55 | `; 56 | 57 | generateClientApi({ clientPath, decorators, httpServiceTemplate, serverPath }); 58 | 59 | generateClientInterfaces('client/src/assets/i18n/login', 'client/src/api/i18n/login.i18n.ts'); 60 | generateClientInterfaces('client/src/assets/i18n/tador', 'client/src/api/i18n/site.i18n.ts'); 61 | generateClientInterfaces('client/src/assets/i18n/tador', 'client/src/api/i18n/mf.i18n.ts'); 62 | -------------------------------------------------------------------------------- /new server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "prettier"], 4 | "extends": ["plugin:@typescript-eslint/recommended", "prettier"], 5 | "rules": { 6 | "prettier/prettier": "warn", 7 | "no-unused-vars": "off", 8 | "camelcase": "error", 9 | "no-var": "error", 10 | "eqeqeq": "error", 11 | "no-return-await": "error", 12 | "@typescript-eslint/no-array-constructor": "off", 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "@typescript-eslint/interface-name-prefix": "off", 15 | "@typescript-eslint/ban-ts-ignore": "off", 16 | "@typescript-eslint/no-empty-interface": "off", 17 | "@typescript-eslint/no-empty-function": "off", 18 | "@typescript-eslint/no-unused-vars": "off", 19 | "@typescript-eslint/explicit-module-boundary-types": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /new server/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "printWidth": 140 5 | } 6 | -------------------------------------------------------------------------------- /new server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "show-case", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node src/app.ts", 8 | "test": "jest --coverage" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "fastify-cookie": "^4.1.1", 14 | "fastify-jwt": "^2.1.3", 15 | "strongly": "^1.0.18" 16 | }, 17 | "devDependencies": { 18 | "@testdeck/jest": "^0.1.2", 19 | "@types/jest": "^26.0.15", 20 | "@types/lodash": "^4.14.165", 21 | "@types/node": "^14.14.8", 22 | "@typescript-eslint/eslint-plugin": "^4.8.0", 23 | "@typescript-eslint/parser": "^4.8.0", 24 | "eslint-config-prettier": "^6.15.0", 25 | "eslint-plugin-prettier": "^3.1.4", 26 | "prettier": "^1.19.1", 27 | "eslint": "^7.13.0", 28 | "jest": "^26.6.3", 29 | "ts-jest": "^26.4.4", 30 | "ts-node": "^9.0.0", 31 | "typescript": "^4.0.5" 32 | }, 33 | "jest": { 34 | "coverageDirectory": "/coverage", 35 | "testEnvironment": "node", 36 | "moduleFileExtensions": [ 37 | "ts", 38 | "tsx", 39 | "js" 40 | ], 41 | "transform": { 42 | "^.+\\.(ts|tsx)$": "ts-jest" 43 | }, 44 | "testMatch": [ 45 | "**/*.spec.(ts|js)" 46 | ], 47 | "testPathIgnorePatterns": [ 48 | "/dist/" 49 | ], 50 | "moduleDirectories": [ 51 | "node_modules", 52 | "src" 53 | ], 54 | "globals": { 55 | "ts-jest": { 56 | "compiler": "typescript", 57 | "tsconfig": "tsconfig.json" 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /new server/src/app.ts: -------------------------------------------------------------------------------- 1 | import { ServerFactory } from "strongly"; 2 | import fastifyJwt from "fastify-jwt"; 3 | import fastifyCookie from "fastify-cookie"; 4 | ServerFactory.create({ 5 | logger: { 6 | level: "debug" 7 | } 8 | }).then(app => { 9 | app.register(fastifyJwt, { 10 | secret: "supersecret", 11 | cookie: { 12 | cookieName: "token" 13 | } 14 | }); 15 | app.register(fastifyCookie); 16 | app.addHook("onRequest", async request => { 17 | try { 18 | await request.jwtVerify(); 19 | } catch (err) {} 20 | }); 21 | 22 | app.listen(3000, err => { 23 | if (err) { 24 | console.log(err); 25 | process.exit(1); 26 | } 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /new server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "noImplicitAny": false, 10 | "moduleResolution": "node", 11 | "alwaysStrict": true, 12 | "declaration": true, 13 | "module": "commonjs", 14 | "types": [ 15 | "jest", 16 | "node" 17 | ], 18 | "sourceMap": false, 19 | "target": "es6", 20 | "plugins": [ 21 | ] 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "node_modules", 28 | "**/*.spec.ts", 29 | "src/server/tests/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /new server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "noImplicitAny": false, 10 | "moduleResolution": "node", 11 | "alwaysStrict": true, 12 | "declaration": true, 13 | "module": "commonjs", 14 | "types": [ 15 | "jest", 16 | "node" 17 | ], 18 | "sourceMap": false, 19 | "target": "es6", 20 | "plugins": [ 21 | ] 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-angular", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "format": "prettier --write \"**/*.ts\"", 8 | "install-client": "cd client&&npm i", 9 | "install-server": "cd server&&npm i", 10 | "install": "npm run install-client&&npm run install-server", 11 | "gen-client": "ts-node ./generator && prettier --write \"./client/src/api/**/*.ts\"", 12 | "serve-client": "cd client&&npm run start", 13 | "debug-server": "cd server&&npm run start:debug", 14 | "start-server": "cd server&&npm run start", 15 | "build-client": "cd client&&npm run build", 16 | "build-client-prod": "cd client&&npm run build:prod", 17 | "build-client-dev": "cd client&&npm run build:dev", 18 | "dev": "start npm run debug-server | start npm run build-client | (cd client && code .) | (cd server && code .)", 19 | "prod": "set NODE_ENV=production&&ts-node app", 20 | "marker": "set NODE_ENV=marker&&ts-node app", 21 | "start-dev": "set NODE_ENV=dev&&npm run build-client-prod&&npm run start-server", 22 | "test": "ts-node ./test/index", 23 | "git": "git reset --hard HEAD&&git pull", 24 | "deploy-dev": "npm run git&&npm i&& npm run start-dev", 25 | "deploy-prod": "npm run git&&npm i&&npm run build-client-prod&& npm run serve-static &&npm run pm2-start-server", 26 | "pm2-start-server": "cd server && pm2 start --name server npm -- start", 27 | "pm2-serve-static": "tsc serve-static && pm2 start serve-static.js", 28 | "pm2-serve-static-restart": "tsc serve-static && pm2 restart serve-static", 29 | "build:client:cordova": "cd client&& npm run build:cordova", 30 | "serve-static": "ts-node serve-static", 31 | "gen-uml": "tsuml --glob ./shared/**/*.ts", 32 | "lint": "tslint -p .", 33 | "lint:fix": "tslint -p . --fix&&cd client&&ng lint --fix", 34 | "open-ports": "sudo iptables -A INPUT -p tcp --dport 4001 --jump ACCEPT", 35 | "open-client-port": "sudo iptables -A INPUT -p tcp --dport 4001 --jump ACCEPT" 36 | }, 37 | "author": "", 38 | "license": "ISC", 39 | "devDependencies": { 40 | "@types/lodash": "^4.14.123", 41 | "@types/node": "^10.17.28", 42 | "json-ts": "^1.6.4", 43 | "nest-client-generator": "^1.1.5", 44 | "prettier": "^1.15.3", 45 | "ts-morph": "^1.3.3", 46 | "ts-node": "^8.0.2", 47 | "tslint": "^5.13.1", 48 | "tslint-class-validator-rule": "^1.0.3", 49 | "tslint-eslint-rules": "^5.4.0", 50 | "typescript": "^3.3.1" 51 | }, 52 | "dependencies": { 53 | "bson": "^4.0.2", 54 | "class-validator": "^0.11.0", 55 | "fastify": "^2.14.1", 56 | "fastify-static": "^2.7.0", 57 | "lodash": "^4.17.11", 58 | "nosql-normalizer": "^1.0.6", 59 | "serve-static": "^1.14.1", 60 | "tsconfig-paths": "^3.8.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /serve-static.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { extname } from 'path'; 3 | const fastify = require('fastify')({ logger: true }) 4 | const mimeTypes = { 5 | '.html': 'text/html', 6 | '.js': 'text/javascript', 7 | '.css': 'text/css', 8 | '.json': 'application/json', 9 | '.png': 'image/png', 10 | '.jpg': 'image/jpg', 11 | '.gif': 'image/gif', 12 | '.svg': 'image/svg+xml', 13 | '.wav': 'audio/wav', 14 | '.mp4': 'video/mp4', 15 | '.woff': 'application/font-woff', 16 | '.ttf': 'application/font-ttf', 17 | '.eot': 'application/vnd.ms-fontobject', 18 | '.otf': 'application/font-otf', 19 | '.wasm': 'application/wasm', 20 | '.gz':'application/gzip' 21 | }; 22 | const cache = {}; 23 | 24 | fastify.get('*', (_, reply) => { 25 | try{ 26 | const filePath = extname(_.raw.url) ? './client/dist' + _.raw.url : './client/dist/index.html'; 27 | const ext = extname(filePath).toLowerCase(); 28 | let contentType = mimeTypes[ext] || 'application/octet-stream'; 29 | if (!cache[filePath]){ 30 | if (fs.existsSync(filePath)) 31 | cache[filePath] = {file:fs.readFileSync(filePath), type:contentType} 32 | else { 33 | cache[filePath] = {file:fs.readFileSync(filePath + '.gz'), type: mimeTypes['.gz']} 34 | } 35 | } 36 | 37 | const file = cache[filePath]; 38 | if (file.type === mimeTypes['.gz']){ 39 | reply.header('Content-Encoding', 'gzip').type(file.type).send(file.file) 40 | } else{ 41 | reply.type(file.type).send(file.file) 42 | } 43 | } 44 | catch (e) { 45 | console.log(e) 46 | } 47 | }) 48 | 49 | // Run the server! 50 | const start = async () => { 51 | try { 52 | await fastify.listen(4200,'0.0.0.0') 53 | fastify.log.info(`server listening on ${fastify.server.address().port}`) 54 | } catch (err) { 55 | fastify.log.error(err) 56 | process.exit(1) 57 | } 58 | } 59 | 60 | start() 61 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | test -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package.json ./ 6 | 7 | COPY ../../config.ts ../../ 8 | 9 | RUN npm i 10 | 11 | COPY . . 12 | 13 | EXPOSE 3000 14 | 15 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 |

    2 | Nest Logo 3 |

    4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

    A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

    11 |

    12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 21 | 22 | 23 |

    24 | 26 | 27 | ## Description 28 | 29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install 35 | ``` 36 | 37 | ## Running the app 38 | 39 | ```bash 40 | # development 41 | $ npm run start 42 | 43 | # watch mode 44 | $ npm run start:dev 45 | 46 | # production mode 47 | $ npm run start:prod 48 | ``` 49 | 50 | ## Test 51 | 52 | ```bash 53 | # unit tests 54 | $ npm run test 55 | 56 | # e2e tests 57 | $ npm run test:e2e 58 | 59 | # test coverage 60 | $ npm run test:cov 61 | ``` 62 | 63 | ## Support 64 | 65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 66 | 67 | ## Stay in touch 68 | 69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 70 | - Website - [https://nestjs.com](https://nestjs.com/) 71 | - Twitter - [@nestframework](https://twitter.com/nestframework) 72 | 73 | ## License 74 | 75 | Nest is [MIT licensed](LICENSE). 76 | -------------------------------------------------------------------------------- /server/env/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | mongodb_container: 4 | image: mongo:latest 5 | ports: 6 | - 27017:27017 7 | volumes: 8 | - mongodb_data_container:/data/db 9 | 10 | volumes: 11 | mongodb_data_container: -------------------------------------------------------------------------------- /server/env/env-up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sudo docker-compose up -d -------------------------------------------------------------------------------- /server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /server/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "description": "description", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 10 | "start-tador-client": "ts-node -r tsconfig-paths/register src/tador/client", 11 | "start:dev": "nodemon", 12 | "start:debug": "nodemon --config nodemon-debug.json", 13 | "prestart:prod": "rimraf dist && npm run build", 14 | "start:prod": "node node -r ./tsconfig-paths-bootstrap.js ./dist/nest-angular/server/src/main.js", 15 | "lint": "tslint -p tsconfig.json -c tslint.json --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nest-middlewares/compression": "^6.0.0", 24 | "@nestjs/common": "^6.0.0", 25 | "@nestjs/core": "^6.0.0", 26 | "@nestjs/platform-fastify": "^6.1.1", 27 | "@nestjs/platform-socket.io": "^6.10.12", 28 | "@nestjs/websockets": "^6.10.12", 29 | "@types/nodemailer": "^6.1.0", 30 | "axios": "^0.19.0", 31 | "bcryptjs": "^2.4.3", 32 | "fastify-cookie": "^3.0.2", 33 | "fastify-cors": "^2.1.2", 34 | "fastify-static": "^2.4.0", 35 | "module-alias": "^2.2.0", 36 | "mongo-nest": "^1.0.0", 37 | "mongodb": "^3.1.13", 38 | "mongodb-backup": "^1.6.9", 39 | "mongodb-restore": "^1.6.2", 40 | "mssql": "^5.0.4", 41 | "node-cache": "^4.2.0", 42 | "nodemailer": "^6.1.1", 43 | "reflect-metadata": "^0.1.12", 44 | "rxjs": "^6.3.3" 45 | }, 46 | "devDependencies": { 47 | "@nestjs/testing": "^6.6.7", 48 | "@types/bcryptjs": "^2.4.2", 49 | "@types/express": "^4.16.0", 50 | "@types/jest": "^23.3.13", 51 | "@types/mongodb": "^3.1.19", 52 | "@types/node": "^10.12.18", 53 | "@types/passport-local": "^1.0.33", 54 | "@types/socket.io": "^2.1.4", 55 | "@types/supertest": "^2.0.7", 56 | "jest": "^23.6.0", 57 | "nodemon": "^1.18.9", 58 | "prettier": "^1.17.1", 59 | "supertest": "^3.4.1", 60 | "ts-jest": "^23.10.5", 61 | "ts-node": "^7.0.1", 62 | "tsconfig-paths": "^3.7.0", 63 | "tslint": "5.12.1", 64 | "typescript": "^3.2.4" 65 | }, 66 | "jest": { 67 | "moduleFileExtensions": [ 68 | "js", 69 | "json", 70 | "ts" 71 | ], 72 | "rootDir": "src", 73 | "testRegex": ".spec.ts$", 74 | "transform": { 75 | "^.+\\.(t|j)s$": "ts-jest" 76 | }, 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/admin/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Post, Body, Req, UseInterceptors } from '@nestjs/common'; 2 | import { App, User, Permission, Role } from 'shared/models/user.model'; 3 | import { UserService } from '../services/user.service'; 4 | import { cryptPassword, getRandomToken } from '../utils'; 5 | import { MailerService } from '../services/mailer.service'; 6 | import { AuthorizeInterceptor } from '../middlewares/authorize.middleware'; 7 | @Controller('rest/admin') 8 | @UseInterceptors(AuthorizeInterceptor) 9 | export class AdminController { 10 | constructor(private userService: UserService, private mailer: MailerService) { 11 | this.userService.userRepo.collection.countDocuments().then(async usersCount => { 12 | if (usersCount) { 13 | return; 14 | } 15 | const user = new User({ 16 | email: 'admin@admin.com', 17 | phone: '0555555', 18 | fName: 'Admin', 19 | lName: '', 20 | roles: [{ app: App.admin, permission: Permission.user }], 21 | }); 22 | user.password = await cryptPassword('123456'); 23 | this.addUser(user).then(); 24 | }); 25 | } 26 | 27 | @Post('users/:app') 28 | async users(@Body() app: App): Promise { 29 | return this.userService.getUsers({ 'roles.app': App.tador } as any); 30 | } 31 | 32 | @Post('addUser') 33 | async addUser(@Body() user: User, @Req() req?) { 34 | const existUser = await this.userService.userRepo.findOne({ email: user.email }); 35 | let newRole: Role; 36 | if (existUser) { 37 | newRole = user.roles.find(r => !existUser.roles.find(rr => rr.permission === r.permission && rr.app == r.app)); 38 | if (newRole) existUser.roles.push(newRole); 39 | user.roles = existUser.roles; 40 | } 41 | 42 | const result = (await this.userService.saveUser(user)) as any; 43 | if (req && (newRole || !existUser)) { 44 | const token = await getRandomToken(); 45 | this.userService.saveUserToekn(user.email, token); 46 | const url = req.headers.origin + '/tador/auth/signin/' + token; 47 | this.mailer.send({ 48 | from: '"Tador system management" ', 49 | to: user.email, 50 | subject: 'הרשאות למערכת תאדור', 51 | html: `
    52 |

    שלום

    53 |

    יש לך הרשאות עבור מערכת תאדור

    54 | 55 | היכנס 56 | 57 |
    `, 58 | }); 59 | } 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdminController } from './admin.controller'; 3 | import { MailerService } from '../services/mailer.service'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | @Module({ 6 | controllers: [AdminController], 7 | providers: [MailerService], 8 | imports: [AuthModule], 9 | }) 10 | export class AdminModule {} 11 | -------------------------------------------------------------------------------- /server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res } from '@nestjs/common'; 2 | 3 | @Controller('site') 4 | export class AppController { 5 | @Get('*') 6 | async root(@Res() res) { 7 | res.sendFile('index.html'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, MiddlewareConsumer } from '@nestjs/common'; 2 | import { CompressionMiddleware } from '@nest-middlewares/compression'; 3 | import { AdminModule } from './admin/admin.module'; 4 | import { MongoRepoModule } from 'mongo-nest'; 5 | import { TadorModule } from './tador/tador.module'; 6 | import { AppController } from 'app.controller'; 7 | import { mongoUrl } from '../../../config'; 8 | import { FrontendMiddleware } from './middlewares/frontend.middleware'; 9 | @Module({ 10 | imports: [AdminModule, TadorModule, MongoRepoModule.forRoot(mongoUrl)], 11 | controllers: [AppController], 12 | }) 13 | export class AppModule { 14 | configure(consumer: MiddlewareConsumer): void { 15 | consumer.apply(CompressionMiddleware).forRoutes('*'); 16 | // consumer.apply(FrontendMiddleware).forRoutes('*'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Get, Body, UseInterceptors } from '@nestjs/common'; 2 | import { LoginRequest, User, signinRequest } from 'shared'; 3 | import { UserService } from 'services/user.service'; 4 | import { LoginInterceptor, GetUserAuthenticatedInterceptor } from '../middlewares/login.middleware'; 5 | import { ReqUser } from '../decorators/user.decorator'; 6 | import { AuthorizeInterceptor } from '../middlewares/authorize.middleware'; 7 | 8 | @Controller('rest/auth') 9 | export class AuthController { 10 | constructor(private readonly authService: UserService) {} 11 | 12 | @Post('login') 13 | @UseInterceptors(LoginInterceptor) 14 | async login(@Body() user: LoginRequest): Promise<{ status: number }> { 15 | return this.authService.validateUser(user.email, user.password) as any; 16 | } 17 | 18 | @Post('logout') 19 | @UseInterceptors(AuthorizeInterceptor) 20 | async logout(@ReqUser() user: User): Promise<{ status: number }> { 21 | return this.authService.logout(user) as any; 22 | } 23 | 24 | @Post('signin') 25 | async signin(@Body() user: signinRequest): Promise { 26 | return this.authService.changePassword(user); 27 | } 28 | 29 | @UseInterceptors(GetUserAuthenticatedInterceptor) 30 | @Get('getUserAuthenticated') 31 | async getUserAuthenticated(@ReqUser() user: User): Promise { 32 | return user; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthController } from './auth.controller'; 3 | import { UserService } from '../services/user.service'; 4 | 5 | @Module({ 6 | controllers: [AuthController], 7 | providers: [UserService], 8 | exports: [UserService], 9 | }) 10 | export class AuthModule {} 11 | -------------------------------------------------------------------------------- /server/src/auth/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { App, Permission } from 'shared'; 3 | 4 | export const ControllerRole = (role: App) => SetMetadata('controllerRole', role); 5 | export const MethodRoles = (permisisons: Permission[]) => SetMetadata('methodRoles', permisisons); 6 | -------------------------------------------------------------------------------- /server/src/decorators/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const ReqUser = createParamDecorator((data, req) => { 4 | return req.user; 5 | }); 6 | -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; 4 | import { ValidationPipe } from './pipes/validation.pipe'; 5 | import { readFileSync } from 'fs'; 6 | 7 | // import { AuthorizeInterceptor } from 'middlewares/authorize.middleware'; 8 | // import { AuthModule } from 'auth/auth.module'; 9 | // import { UserService } from 'services/user.service'; 10 | import { join } from 'path'; 11 | import { FrontendMiddleware } from './middlewares/frontend.middleware'; 12 | const clientPath = join(__dirname, '../../client/dist'); 13 | async function bootstrap() { 14 | const app = await NestFactory.create( 15 | AppModule, 16 | process.env.NODE_ENV === 'prudaction' 17 | ? new FastifyAdapter({ 18 | http2: true, 19 | https: { 20 | allowHTTP1: true, // fallback support for HTTP1 21 | cert: readFileSync(join(__dirname, '../../../localhost.pem')), 22 | key: readFileSync(join(__dirname, '../../../localhost-key.pem')), 23 | }, 24 | }) 25 | : new FastifyAdapter(), 26 | ); 27 | 28 | // enable cors for static angular site. 29 | const corsOptions = { 30 | origin: [ 31 | 'https://localhost:4200', 32 | 'http://localhost:4200', 33 | 'http://localhost:3200', 34 | 'http://128.199.41.162:4200', 35 | 'http://178.62.237.25:4200' 36 | ], 37 | optionsSuccessStatus: 200, 38 | credentials: true, 39 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], 40 | }; 41 | 42 | // const corsOptions = { 43 | // origin: '*', 44 | // optionsSuccessStatus: 200, 45 | // credentials: true, 46 | // methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], 47 | // }; 48 | app.register(require('fastify-cors'), corsOptions); 49 | 50 | // enable cookie for auth. 51 | app.register(require('fastify-cookie')); 52 | 53 | // validate types and extra 54 | app.useGlobalPipes( 55 | new ValidationPipe({ 56 | whitelist: true, 57 | forbidNonWhitelisted: true, 58 | forbidUnknownValues: true, 59 | }), 60 | ); 61 | 62 | app.useStaticAssets({ root: clientPath }); 63 | await app.listen(3000, '0.0.0.0'); 64 | } 65 | bootstrap(); 66 | -------------------------------------------------------------------------------- /server/src/middlewares/authorize.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler, ForbiddenException } from '@nestjs/common'; 2 | import { UserService } from '../services/user.service'; 3 | import { App } from 'shared/models'; 4 | 5 | @Injectable() 6 | export class AuthorizeInterceptor implements NestInterceptor { 7 | constructor(private userService: UserService) {} 8 | async intercept(context: ExecutionContext, next: CallHandler) { 9 | const user = await this.userService.getUserAuthenticated(context.getArgs()[0].cookies.t); 10 | const c = context.getClass()['app']; 11 | if (!user || (c && !user.hasPermission(c))) { 12 | throw new ForbiddenException(); 13 | } 14 | 15 | context.getArgs()[0].user = user; 16 | return next.handle(); 17 | } 18 | } 19 | 20 | // import { Injectable, NestInterceptor, ExecutionContext, CallHandler, ForbiddenException } from '@nestjs/common'; 21 | // import { UserService } from '../services/user.service'; 22 | // import { hasPermission, App, hasRole, Permission } from 'shared'; 23 | // import { Reflector } from '@nestjs/core'; 24 | 25 | // @Injectable() 26 | // export class AuthorizeInterceptor implements NestInterceptor { 27 | // readonly reflector: Reflector = new Reflector(); 28 | // constructor(private userService: UserService) {} 29 | // async intercept(context: ExecutionContext, next: CallHandler) { 30 | // const app = await this.reflector.get('controllerRole', context.getClass()); 31 | // if (!app) { 32 | // return next.handle(); 33 | // } 34 | // const user = await this.userService.getUserAuthenticated(context.getArgs()[0].cookies.t); 35 | 36 | // if (!hasPermission(user, app)) { 37 | // throw new ForbiddenException(); 38 | // } 39 | 40 | // context.getArgs()[0].user = user; 41 | // const permissions = this.reflector.get('methodRoles', context.getHandler()); 42 | // if (!permissions) { 43 | // return next.handle(); 44 | // } 45 | // if (!hasRole(user, app, permissions)) { 46 | // throw new ForbiddenException(); 47 | // } 48 | // return next.handle(); 49 | // } 50 | // } 51 | -------------------------------------------------------------------------------- /server/src/middlewares/frontend.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler, NestMiddleware } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { User } from 'shared'; 5 | import { UserService } from 'services/user.service'; 6 | 7 | @Injectable() 8 | export class FrontendMiddleware implements NestMiddleware { 9 | use(req: Request, res: Response, next: Function) { 10 | if (req.url.startsWith('/site')) { 11 | // return res. 12 | } 13 | next(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/middlewares/login.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { User } from 'shared'; 5 | import { UserService } from 'services/user.service'; 6 | 7 | @Injectable() 8 | export class LoginInterceptor implements NestInterceptor { 9 | intercept(context: ExecutionContext, next: CallHandler): Observable { 10 | return next.handle().pipe( 11 | map((user: User) => { 12 | if (user) { 13 | context.getArgs()[1].setCookie('t', user._id, { path: '/' }); 14 | context.getArgs()[0].user = user; 15 | return { status: 1 }; 16 | } 17 | return { status: 0 }; 18 | }), 19 | ); 20 | } 21 | } 22 | 23 | // tslint:disable-next-line: max-classes-per-file 24 | @Injectable() 25 | export class GetUserAuthenticatedInterceptor implements NestInterceptor { 26 | constructor(private userService: UserService) {} 27 | async intercept(context: ExecutionContext, next: CallHandler) { 28 | const user = await this.userService.getUserAuthenticated(context.getArgs()[0].cookies.t); 29 | context.getArgs()[0].user = user; 30 | return next.handle(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/middlewares/normelize.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Observable, of } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | import { normalize } from 'nosql-normalizer'; 10 | 11 | @Injectable() 12 | export class NormelizeInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | return next.handle().pipe(map(data => normalize(data))); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/pipes/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorOptions } from '@nestjs/common/interfaces/external/validator-options.interface'; 2 | import { ClassTransformOptions } from '@nestjs/common/interfaces/external/class-transform-options.interface'; 3 | import { ValidationError, Injectable, PipeTransform, Optional, BadRequestException, ArgumentMetadata } from '@nestjs/common'; 4 | import { isNil } from 'lodash'; 5 | 6 | import { validate } from 'class-validator'; 7 | export interface ValidationPipeOptions extends ValidatorOptions { 8 | transform?: boolean; 9 | disableErrorMessages?: boolean; 10 | transformOptions?: ClassTransformOptions; 11 | exceptionFactory?: (errors: ValidationError[]) => any; 12 | } 13 | @Injectable() 14 | export class ValidationPipe implements PipeTransform { 15 | protected isTransformEnabled: boolean; 16 | protected isDetailedOutputDisabled?: boolean; 17 | protected validatorOptions: ValidatorOptions; 18 | protected transformOptions: ClassTransformOptions; 19 | protected exceptionFactory: (errors: ValidationError[]) => any; 20 | 21 | constructor(@Optional() options?: ValidationPipeOptions) { 22 | options = options || {}; 23 | const { transform, disableErrorMessages, transformOptions, ...validatorOptions } = options; 24 | this.isTransformEnabled = !!transform; 25 | this.validatorOptions = validatorOptions; 26 | this.transformOptions = transformOptions; 27 | this.isDetailedOutputDisabled = disableErrorMessages; 28 | this.exceptionFactory = 29 | options.exceptionFactory || (errors => new BadRequestException(this.isDetailedOutputDisabled ? undefined : errors)); 30 | } 31 | 32 | public async transform(value: any, metadata: ArgumentMetadata) { 33 | const { metatype } = metadata; 34 | if (!metatype || !this.toValidate(metadata)) { 35 | return value; 36 | } 37 | const entity = new metadata.metatype(value); 38 | const errors = await validate(entity, this.validatorOptions); 39 | if (errors.length > 0) { 40 | throw this.exceptionFactory(errors as any); 41 | } 42 | return entity; 43 | } 44 | 45 | private toValidate(metadata: ArgumentMetadata): boolean { 46 | const { metatype, type } = metadata; 47 | if (type === 'custom') { 48 | return false; 49 | } 50 | const types = [String, Boolean, Number, Array, Object]; 51 | return !types.some(t => metatype === t) && !isNil(metatype); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/src/services/mailer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { createTransport } from 'nodemailer'; 3 | import { smtp } from '../../../../config'; 4 | import * as Mail from 'nodemailer/lib/mailer'; 5 | @Injectable() 6 | export class MailerService { 7 | private transporter: Mail; 8 | constructor() { 9 | this.transporter = createTransport(smtp); 10 | } 11 | 12 | async send(mailOptions: Mail.Options) { 13 | try { 14 | await this.transporter.sendMail(mailOptions); 15 | } catch (e) { 16 | Logger.error(e); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User, signinRequest } from 'shared'; 3 | import { Repository, RepositoryFactory } from 'mongo-nest'; 4 | import { compare } from 'bcryptjs'; 5 | import * as NodeCache from 'node-cache'; 6 | import { cryptPassword } from '../utils'; 7 | 8 | // const generateToken = (): Promise => 9 | // new Promise(resolve => randomBytes(48, (err, buffer) => resolve(buffer.toString('hex')))); 10 | 11 | const comparePassword = (plainPass, hashword) => { 12 | return compare(plainPass, hashword); 13 | }; 14 | 15 | @Injectable() 16 | export class UserService { 17 | userRepo: Repository; 18 | // remove object after 10 minute 19 | private cache = new NodeCache({ stdTTL: 60 * 60 * 12 }); 20 | 21 | constructor(private repositoryFactory: RepositoryFactory) { 22 | this.userRepo = this.repositoryFactory.getRepository(User, 'users'); 23 | } 24 | 25 | async validateUser(email, password) { 26 | const user = new User( 27 | await this.userRepo.collection.findOne({ 28 | email, 29 | }), 30 | ); 31 | if (!user.password || !(await comparePassword(password, user.password))) { 32 | return null; 33 | } 34 | delete user.password; 35 | // TODO use token instead of id. 36 | setTimeout(() => this.cache.set(user._id.toString(), user)); 37 | return user; 38 | } 39 | 40 | async saveUser(user: User): Promise<{ ok: number }> { 41 | return (await this.userRepo.saveOrUpdateOne(user)).result; 42 | } 43 | 44 | async getUserAuthenticated(id): Promise { 45 | if (!id) { 46 | return undefined; 47 | } 48 | const foundUser = this.cache.get(id); 49 | if (!foundUser) { 50 | return undefined; 51 | } 52 | 53 | setTimeout(() => this.cache.set(foundUser._id.toString(), foundUser)); 54 | return foundUser; 55 | } 56 | 57 | async getUsers(query: Partial) { 58 | return this.userRepo.collection 59 | .find(query, {}) 60 | .project({ password: 0 }) 61 | .toArray(); 62 | } 63 | 64 | saveUserToekn(email: string, token: string) { 65 | this.cache.set(email, token); 66 | } 67 | async changePassword(user: signinRequest) { 68 | const cacheToken = this.cache.get(user.email); 69 | if (!cacheToken || cacheToken != user.token) { 70 | return { status: 0 }; 71 | } 72 | const password = await cryptPassword(user.password); 73 | try { 74 | await this.userRepo.collection.updateOne({ email: user.email }, { $set: { password } }); 75 | } catch (e) { 76 | console.log(e); 77 | } 78 | return {}; 79 | } 80 | 81 | logout(user: User) { 82 | setTimeout(() => this.cache.del(user._id.toString())); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server/src/tador/client.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net'; 2 | import { ActionType, PanelType } from 'shared/models/tador/enum'; 3 | 4 | const client = new Socket(); 5 | const port = 4000; 6 | const host = '128.199.41.162'; 7 | client.setMaxListeners(100); 8 | const write = (str: string) => { 9 | return new Promise((resolve, reject) => { 10 | client.connect(port, host, function() { 11 | console.log('Connected'); 12 | client.write(str); 13 | client.on('data', data => { 14 | client.end(); 15 | client.on('close', () => { 16 | resolve(data.toString()); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }; 22 | 23 | client.on('data', function(data) { 24 | console.log('Server Says : ' + data); 25 | }); 26 | 27 | client.on('close', function() { 28 | console.log('Connection closed'); 29 | }); 30 | let pId: any = '5d7219d024cba11c06c38e1d'; 31 | const test = async () => { 32 | // ----------- REGISTER ------------ 33 | const registerAction = { type: ActionType.register, data: { type: PanelType.MP, uId: 'hadar453@012.net.il' } }; 34 | const registerActionString = JSON.stringify(registerAction); 35 | await write(registerActionString); 36 | // 5d7219d024cba11c06c38e1d 37 | // --------------------------------- 38 | // ----------- STATUS ------------{type:6,pId:"?"} 39 | const statusAction = { type: ActionType.status, pId }; 40 | const statusActionString = JSON.stringify(statusAction); 41 | for (let i = 0; i < 10; i++) await write(statusActionString); 42 | // --------------------------------- 43 | 44 | // ----------- WRITE ------------ 45 | const writeAction = { type: 5, pId: '867057031591342', data: { start: 2551, data: 'יניב טרבלסי' } }; 46 | const writeString = JSON.stringify(writeAction); 47 | await write(writeString); 48 | // --------------------------------- 49 | 50 | // ----------- GET CHANGES ------------ 51 | const getAction = { type: 4, pId: '?', data: { start: 2551, length: 100 } }; 52 | const getString = JSON.stringify(getAction); 53 | await write(getString); 54 | // --------------------------------- 55 | 56 | // ----------- GET ALL ------------ 57 | const getAllAction = { type: 3, pId: '?', data: { start: 318, length: 10 } }; 58 | const getAllString = JSON.stringify(getAllAction); 59 | await write(getAllString); 60 | // --------------------------------- 61 | }; 62 | 63 | test(); 64 | -------------------------------------------------------------------------------- /server/src/tador/tador.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, UseInterceptors } from '@nestjs/common'; 2 | import { TadorService } from './tador.service'; 3 | import { ReqUser } from '../decorators/user.decorator'; 4 | import { App, User } from 'shared/models'; 5 | import { ContactNameDirection, Panel } from 'shared/models/tador/panels'; 6 | import { AuthorizeInterceptor } from '../middlewares/authorize.middleware'; 7 | import { PanelType } from 'shared/models/tador/enum'; 8 | import { UserService } from '../services/user.service'; 9 | import { keyBy } from 'lodash'; 10 | import { dumps } from './initial_daumps'; 11 | import { AddPanelRequest } from 'shared/models/tador/add-panel-request'; 12 | 13 | @UseInterceptors(AuthorizeInterceptor) 14 | @Controller('tador') 15 | export class TadorController { 16 | static app = App.tador; 17 | constructor(private service: TadorService, private userService: UserService) {} 18 | 19 | @Get('initialData') 20 | async initialData(@ReqUser() user: User) { 21 | const isAdmins = user.roles.find(r => r.app === App.admin); 22 | const panels = await this.service.panelRepo.findMany(!isAdmins ? { userId: user.email } : {}); 23 | const users = isAdmins ? await this.userService.getUsers({}) : {}; 24 | const allPanels = panels.map(p => { 25 | const result = { panel: p, dump: new Panel(p).dump() }; 26 | return result; 27 | }); 28 | 29 | return {users, panels: allPanels}; 30 | } 31 | 32 | @Get('panel/:id') 33 | async panels(@Param('id') id: string) { 34 | return this.service.getPanel(id); 35 | } 36 | 37 | @Post('savePanel') 38 | async savePanel(@Body() panel: Panel) { 39 | const p = await this.service.updatePanel(panel); 40 | const result = { panel: p, dump: new Panel(p).dump() }; 41 | result.panel.reDump(result.dump); 42 | return result; 43 | } 44 | @Post('addPanel') 45 | async addNewPanel(@Body() body: any, @ReqUser() user: User): Promise { 46 | body.userId = user.email; 47 | return this.service.addNewPanel(body); 48 | } 49 | 50 | @Post('status') 51 | async status(@Body() panel: Panel): Promise { 52 | return this.service.addStatus(panel, panel.actionType); 53 | } 54 | 55 | @Post('removeChanges') 56 | async removeChanges(@Body() panel: Panel): Promise { 57 | const dump = await this.service.getDump(panel.panelId); 58 | panel.reDump(dump.dump); 59 | panel.contacts.changesList = undefined; 60 | await this.service.panelRepo.saveOrUpdateOne(panel); 61 | return panel; 62 | } 63 | 64 | @Get('getDefaultFile/:type/:direction') 65 | async getDefaultFile(@Param() params:{type: string, direction: ContactNameDirection}) { 66 | return {dump: dumps[params.type][params.direction]}; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/src/tador/tador.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TadorController } from './tador.controller'; 3 | import { TadorService } from './tador.service'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | 6 | @Module({ 7 | providers: [TadorService], 8 | controllers: [TadorController], 9 | imports: [AuthModule], 10 | }) 11 | export class TadorModule {} 12 | -------------------------------------------------------------------------------- /server/src/tador/tador.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net'; 2 | import { getRandomToken } from '../utils'; 3 | import { ActionType, PanelType } from '../../../shared/models/tador/enum'; 4 | const port = 4000; 5 | let pId = '867584033693636'//'861311009983668'; //'1'// 6 | const host = 'localhost'; //'128.199.41.162'; //'178.62.237.25' //' 7 | describe('tador', async () => { 8 | beforeAll(async () => { 9 | // pId = await getRandomToken(); 10 | }); 11 | const write = (str: string) => { 12 | console.log(str); 13 | return new Promise(resolve => { 14 | const client = new Socket(); 15 | client.setMaxListeners(100); 16 | 17 | client.connect(port, host, function() { 18 | console.log('Connected'); 19 | client.write(str); 20 | client.on('data', data => { 21 | console.log(data) 22 | client.end(); 23 | client.on('close', () => { 24 | console.log(data.toString()); 25 | return resolve(data.toString()); 26 | }); 27 | }); 28 | }); 29 | }); 30 | }; 31 | 32 | describe('status', () => { 33 | it('should return This panel is not register!!!', async () => { 34 | const result = 'This panel is not register!!!'; 35 | const registerAction = { type: ActionType.status, pId: '-1' }; 36 | const registerActionString = JSON.stringify(registerAction); 37 | expect(await write(registerActionString)).toBe(result); 38 | }); 39 | 40 | it('should return 0', async () => { 41 | const result = '0'; 42 | const registerAction = { type: ActionType.status, pId, d: 1 }; 43 | const registerActionString = JSON.stringify(registerAction); 44 | expect(await write(registerActionString)).toBe(result); 45 | }); 46 | it('should return change', async () => { 47 | const result = '0'; 48 | const registerAction = { type: ActionType.status, pId }; 49 | const registerActionString = JSON.stringify(registerAction); 50 | expect(await write(registerActionString)).not.toBe(result); 51 | }); 52 | 53 | it('should return 0 after return change', async () => { 54 | const result = '0'; 55 | const registerAction = { type: ActionType.status, pId, d: 1 }; 56 | const registerActionString = JSON.stringify(registerAction); 57 | expect(await write(registerActionString)).not.toBe(result); 58 | }); 59 | }); 60 | 61 | describe('read', () => { 62 | it('should return 0', async () => { 63 | const result = '0'; 64 | const registerAction = { type: ActionType.read, pId, data: { start: 58910, length: 480 } }; 65 | const registerActionString = JSON.stringify(registerAction); 66 | const res = await write(registerActionString) 67 | expect(res).toBe(result); 68 | }); 69 | }); 70 | 71 | describe('write', () => { 72 | it('should return 0', async () => { 73 | await write('!86705703159134200001000����201�����������������������123456 252525 123'); 74 | //await write('!8670570315913420118500 ' + ' '.repeat(1185)); 75 | //await write('!8670570315913420236900 ' + ' '.repeat(1185)); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /server/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { genSalt, hash } from 'bcryptjs'; 2 | import { randomBytes } from 'crypto'; 3 | export const cryptPassword = async password => { 4 | const salt = await genSalt(10); 5 | return hash(password, salt); 6 | }; 7 | 8 | export const getRandomToken = async () => { 9 | const buffer = await randomBytes(48); 10 | return buffer.toString('hex'); 11 | }; 12 | -------------------------------------------------------------------------------- /server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/app.module'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | // it('/ (GET)', () => { 18 | // return request(app.getHttpServer()) 19 | // .get('/') 20 | // .expect(200) 21 | // .expect('Hello World!'); 22 | // }); 23 | }); 24 | -------------------------------------------------------------------------------- /server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/tsconfig-paths-bootstrap.js: -------------------------------------------------------------------------------- 1 | const tsConfig = require('./tsconfig.json'); 2 | const tsConfigPaths = require('tsconfig-paths'); 3 | 4 | const baseUrl = './dist'; 5 | tsConfigPaths.register({ 6 | baseUrl, 7 | paths: tsConfig.compilerOptions.paths, 8 | }); 9 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "resolveJsonModule": true, 12 | 13 | "baseUrl": "./src", 14 | "paths": { 15 | "shared": ["../../shared"], 16 | "shared/*": ["../../shared/*"] 17 | } 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | // "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const ROUTE_PREFIX = 'rest'; 2 | -------------------------------------------------------------------------------- /shared/guard.ts: -------------------------------------------------------------------------------- 1 | import { User, App, Role, Permission } from './models/user.model'; 2 | 3 | // export const hasRole = (user: User, app: App, permissions: Permission[]) => 4 | // user && user.roles.some(role => role.app === App.admin || (role.app === app && permissions.includes(role.permission))); 5 | -------------------------------------------------------------------------------- /shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | export * from './guard'; 3 | export * from './constants'; 4 | export * from './models/Entity'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /shared/models/Entity.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:variable-name */ 2 | import { IsString, IsOptional } from 'class-validator'; 3 | 4 | export abstract class Entity { 5 | // tslint:disable-next-line: variable-name 6 | // tslint:disable-next-line: no-property-without-decorator 7 | @IsOptional() 8 | _id?: any; 9 | @IsOptional() 10 | @IsString() 11 | name?: string; 12 | 13 | get id() { 14 | return this._id ? this._id.toString() : undefined; 15 | } 16 | get isNew() { 17 | return !this.id; 18 | } 19 | constructor(data?) { 20 | if (data) { 21 | Object.assign(this, data); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /shared/models/filter.base.model.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsOptional, IsString } from 'class-validator'; 2 | import get = Reflect.get; 3 | import { uniqBy } from 'lodash'; 4 | 5 | export abstract class Filter { 6 | @IsOptional() @IsString() kind?: string; 7 | @IsOptional() options?: any[]; 8 | @IsOptional() selected?: any; 9 | @IsOptional() @IsString() optionNamePath?: string; 10 | @IsOptional() @IsString() optionIdPath?: string; 11 | @IsBoolean() @IsOptional() isMultiple?: boolean; 12 | @IsOptional() @IsBoolean() isActive?: boolean; 13 | @IsOptional() @IsBoolean() show?: boolean = true; 14 | @IsOptional() @IsString() placeholder?: string; 15 | @IsString() @IsOptional() format?: string; 16 | @IsOptional() 17 | @IsBoolean() lastChange?: boolean; 18 | _options: any[]; 19 | get isDisabled() { 20 | if (this.lastChange) { 21 | this.lastChange = false; 22 | return false; 23 | } 24 | return this._options && this._options.filter(o => !o.isDisabled).length < 2; 25 | } 26 | constructor(filter?: Partial) { 27 | Object.assign(this, filter); 28 | this.kind = this.constructor.name; 29 | } 30 | 31 | doFilter(items: any[]): any[] { 32 | throw new Error('not implemented'); 33 | } 34 | _getOptions(items: any[]): any[] { 35 | return ( 36 | this.options || 37 | uniqBy( 38 | items 39 | 40 | .map(item => ({ 41 | _id: get(item, this.optionIdPath), 42 | name: get(item, this.optionNamePath || this.optionIdPath), 43 | })) 44 | .filter(o => o._id != undefined), 45 | '_id', 46 | ) 47 | ); 48 | } 49 | get hasOptions() { 50 | return (this.options && this.options.length) || (this._options && this._options.length); 51 | } 52 | setOptions(all) { 53 | if (this.options) { 54 | return; 55 | } 56 | this._options = this._getOptions(all); 57 | this._options.forEach(option => (option.isDisabled = true)); 58 | } 59 | 60 | disableOptions(items: any[]) { 61 | if (!this._options) { 62 | return; 63 | } 64 | this._options.forEach(option => (option.isDisabled = false)); 65 | if (this.lastChange) { 66 | return; 67 | } 68 | const relevant = this._getOptions(items); 69 | this._options.forEach(option => (option.isDisabled = !relevant.some(r => r._id === option._id))); 70 | // const enabledOptions = this._options.filter(option => !option.isDisabled); 71 | // if (enabledOptions.length === 1) { 72 | // this.selected = this.isMultiple ? [enabledOptions[0]] : enabledOptions[0]; 73 | // } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /shared/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Entity'; 2 | export * from './filter.base.model' 3 | export * from './filter.model'; 4 | export * from './user.model'; 5 | -------------------------------------------------------------------------------- /shared/models/tador/add-panel-request.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsNumberString, IsOptional, IsString, NotEquals } from 'class-validator'; 2 | import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; 3 | import { ContactNameDirection, Panel } from './panels'; 4 | 5 | export function IsMatch(property: string, validationOptions?: ValidationOptions) { 6 | return function(object: Object, propertyName: string) { 7 | registerDecorator({ 8 | name: 'IsMatch', 9 | target: object.constructor, 10 | propertyName: propertyName, 11 | constraints: [property], 12 | options: validationOptions, 13 | validator: { 14 | validate(value: string, args: ValidationArguments) { 15 | const [relatedPropertyName] = args.constraints; 16 | const relatedValue = (args.object as any)[relatedPropertyName]; 17 | if (!value || !relatedValue) return true; 18 | const result = value 19 | .split('') 20 | .map(l => (10 - +l) % 10) 21 | .join(); 22 | return result === relatedValue; 23 | }, 24 | }, 25 | }); 26 | }; 27 | } 28 | 29 | export class AddPanelRequest{ 30 | 31 | @IsEnum(ContactNameDirection) 32 | direction: ContactNameDirection = ContactNameDirection.RTL; 33 | 34 | @IsNumberString() panelId: string; 35 | 36 | @IsOptional() 37 | @IsString() 38 | phoneNumber?: string; 39 | 40 | 41 | @IsOptional() 42 | @IsString() 43 | contactName?: string; 44 | 45 | @IsOptional() 46 | @IsString() 47 | contactPhone?: string; 48 | 49 | @IsString() 50 | @IsOptional() 51 | address?: string; 52 | 53 | @IsString() 54 | @IsOptional() 55 | userId?: string; 56 | 57 | @IsNumberString() 58 | @NotEquals('0', { message: 'Not initial yet!' }) 59 | @IsMatch('panelId', { message: 'Probably you were wrong!' }) 60 | id: string; 61 | 62 | 63 | 64 | get isMatch() { 65 | const result = this.panelId 66 | .split('') 67 | .map(l => (10 - +l) % 10) 68 | .join(''); 69 | return result === this.id; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /shared/models/tador/enum.ts: -------------------------------------------------------------------------------- 1 | export enum ActionType { 2 | idle = 0, 3 | register = 1, 4 | readAll = 2, 5 | writeAll = 3, 6 | read = 4, 7 | write = 5, 8 | status = 6, 9 | nameOrder, 10 | powerUp =8, 11 | writeAllProgress = 10, 12 | readAllProgress = 11, 13 | readProgress = 12, 14 | writeProgress = 13, 15 | } 16 | 17 | export enum PanelType { 18 | MP = 'MP', 19 | } 20 | -------------------------------------------------------------------------------- /shared/models/tador/panels/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.panel'; 2 | export * from './mp.panel'; 3 | -------------------------------------------------------------------------------- /shared/models/tador/panels/panels.ts: -------------------------------------------------------------------------------- 1 | export * from './mp.panel'; 2 | -------------------------------------------------------------------------------- /shared/models/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../tslint.json"], 3 | "rulesDirectory": ["tslint-class-validator-rule"], 4 | "rules": { 5 | "no-property-without-decorator": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /shared/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Length, IsEmail, IsOptional, IsString, IsEnum, ValidateNested, IsNotEmpty } from 'class-validator'; 2 | //import {EqualTo} from '../customValidation/equalTo' 3 | import { Entity } from './Entity'; 4 | export enum App { 5 | admin, 6 | tador, 7 | } 8 | 9 | export enum Permission { 10 | admin, 11 | user, 12 | } 13 | 14 | export class Role { 15 | @IsEnum(App) 16 | app: App; 17 | @IsEnum(Permission) 18 | permission: Permission; 19 | constructor(role: Role) { 20 | Object.assign(this, role); 21 | } 22 | } 23 | 24 | export class User extends Entity { 25 | @IsOptional() @IsString() password: string; 26 | @IsOptional() @IsString() company?: string; 27 | @IsString() phone: string; 28 | @IsString() @IsEmail() email: string; 29 | @IsOptional() @IsString() details?: string; 30 | @IsString() fName: string; 31 | @IsString() lName: string; 32 | 33 | get fullName() { 34 | return this.fName + ' ' + this.lName; 35 | } 36 | 37 | @ValidateNested({ each: true }) roles: Role[]; 38 | 39 | constructor(user: Partial) { 40 | super(user); 41 | if (user && user.roles) { 42 | this.roles = user.roles.map(role => new Role(role)); 43 | } 44 | } 45 | 46 | hasPermission(app: App) { 47 | return this.roles && this.roles.some(role => role.app === App.admin || role.app === app); 48 | } 49 | } 50 | 51 | export class LoginRequest { 52 | @IsString() 53 | @IsEmail() 54 | email: string; 55 | 56 | @IsString() 57 | @IsNotEmpty() 58 | password: string; 59 | constructor(login?: Partial) { 60 | Object.assign(this, login); 61 | } 62 | } 63 | 64 | export class signinRequest { 65 | @IsString() 66 | @IsEmail() 67 | email: string; 68 | 69 | @IsString() 70 | token: string; 71 | 72 | @IsString() 73 | @Length(5, 10) 74 | password: string; 75 | 76 | // @IsString() 77 | // @Length(5, 10) 78 | // @EqualTo('password', { 79 | // message: "סיסמה לא זהה" 80 | // }) 81 | @IsString() 82 | rePassword: string; 83 | 84 | constructor(login?: Partial) { 85 | Object.assign(this, login); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "noImplicitAny": true, 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "target": "es5", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["es2018", "dom"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shared/utils.ts: -------------------------------------------------------------------------------- 1 | import { round, max, min } from 'lodash'; 2 | 3 | export const pathBySelector = (fun: (a) => {}) => 4 | fun 5 | .toString() 6 | .split('=>')[1] 7 | .trim(); 8 | 9 | export const getDistribution = (data: number[], count?: number): Array<{ x: number; y: number }> => { 10 | if (!count) { 11 | count = round(Math.log10(data.length) * 3.32 + 1); 12 | } 13 | const step = (max(data) - min(data)) / count; 14 | const counterArr = data.reduce((arr, b) => { 15 | const index = Math.floor(b / step); 16 | if (!arr[index]) { 17 | arr[index] = { c: 0, a: 0, i: index }; 18 | } 19 | arr[index].c++; 20 | arr[index].a += b; 21 | return arr; 22 | }, []); 23 | const result = Object.values(counterArr.filter(c => c)).map(c => { 24 | return { 25 | x: round(c.a / c.c, 2), 26 | y: c.c, 27 | }; 28 | }); 29 | return [{ x: min(data), y: 0 }].concat(result).concat([{ x: max(data), y: 0 }]); 30 | }; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": false, 10 | "lib": [ 11 | "es2018" 12 | ], 13 | "outDir": "./dist", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true 16 | }, 17 | "files": [ 18 | "./shared/index.ts", 19 | "./generator/index.ts" 20 | ] 21 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-eslint-rules"], 3 | "defaultSeverity": "error", 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "variable-name": { 9 | "enabled": false 10 | }, 11 | "quotemark": [true, "single"], 12 | "member-access": [false], 13 | "ordered-imports": [false], 14 | "max-line-length": [false], 15 | "member-ordering": [false], 16 | "interface-name": [false], 17 | "arrow-parens": false, 18 | "semi": "never", 19 | "object-literal-sort-keys": false, 20 | "max-classes-per-file": false, 21 | "type-literal-delimiter": false 22 | }, 23 | "rulesDirectory": [] 24 | } 25 | --------------------------------------------------------------------------------