Переглянути джерело

Merge branch 'feat-map' of luccerny/senslog-dashboard into master

feat: Introduce map of units
František Zadražil 3 роки тому
батько
коміт
8a0866adb4
34 змінених файлів з 1515 додано та 692 видалено
  1. 1 0
      .gitignore
  2. 12 2
      angular.json
  3. 605 332
      package-lock.json
  4. 60 27
      package.json
  5. 1 1
      src/app/app-routing.module.ts
  6. 3 3
      src/app/app.module.ts
  7. 6 2
      src/app/auth/guards/role.guard.ts
  8. 12 11
      src/app/auth/interceptors/auth.interceptor.ts
  9. 10 23
      src/app/dashboard/components/dashboard.component.html
  10. 12 0
      src/app/dashboard/components/dashboard.component.scss
  11. 7 129
      src/app/dashboard/components/dashboard.component.ts
  12. 3 0
      src/app/dashboard/components/map/map.component.html
  13. 10 0
      src/app/dashboard/components/map/map.component.scss
  14. 248 0
      src/app/dashboard/components/map/map.component.ts
  15. 23 0
      src/app/dashboard/components/map/map.module.ts
  16. 28 0
      src/app/dashboard/components/unit-list/unit-list.component.html
  17. 4 0
      src/app/dashboard/components/unit-list/unit-list.component.scss
  18. 256 0
      src/app/dashboard/components/unit-list/unit-list.component.ts
  19. 30 24
      src/app/dashboard/dashboard.module.ts
  20. 3 0
      src/app/sensor/components/sensor.component.html
  21. 6 11
      src/app/shared/nav-bar/components/data-download/data-download-popup.component.ts
  22. 3 0
      src/app/unit/components/unit.component.html
  23. 8 6
      src/app/unit/unit.module.ts
  24. 38 7
      src/assets/scss/_dashboard.scss
  25. 2 2
      src/assets/scss/_graph.scss
  26. 1 1
      src/assets/scss/_login.scss
  27. 4 2
      src/assets/scss/_navbar.scss
  28. 2 1
      src/assets/scss/_popup-form.scss
  29. 2 2
      src/assets/scss/_themes.scss
  30. 102 103
      src/assets/scss/_variables.scss
  31. 3 1
      src/assets/scss/styles.scss
  32. 2 1
      src/environments/environment.prod.ts
  33. 2 1
      src/environments/environment.ts
  34. 6 0
      tsconfig.json

+ 1 - 0
.gitignore

@@ -5,3 +5,4 @@
 /.vs
 node_modules
 /dist
+.angular

+ 12 - 2
angular.json

@@ -1,5 +1,8 @@
 {
   "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "cli": {
+    "analytics": false
+  },
   "version": 1,
   "newProjectRoot": "projects",
   "projects": {
@@ -25,16 +28,23 @@
             "main": "src/main.ts",
             "polyfills": "src/polyfills.ts",
             "tsConfig": "tsconfig.app.json",
+            "preserveSymlinks": true,
             "aot": true,
             "assets": [
               "src/favicon.ico",
-              "src/assets"
+              "src/assets",
+              {
+                "glob": "**/*",
+                "input": "./node_modules/hslayers-ng/src/assets",
+                "output": "./hslayers-ng/assets"
+              }
             ],
             "styles": [
               "node_modules/primeng/resources/themes/bootstrap4-dark-blue/theme.css",
               "node_modules/primeng/resources/primeng.min.css",
               "node_modules/primeicons/primeicons.css",
-              "src/assets/scss/styles.scss"
+              "src/assets/scss/styles.scss",
+              "node_modules/hslayers-ng/css/hslayers-ng-wo-bootstrap.css"
             ],
             "scripts": [
               "node_modules/jquery/dist/jquery.js",

Різницю між файлами не показано, бо вона завелика
+ 605 - 332
package-lock.json


+ 60 - 27
package.json

@@ -12,48 +12,81 @@
   },
   "private": true,
   "dependencies": {
-    "@angular/animations": "~11.2.4",
-    "@angular/common": "~11.2.4",
-    "@angular/compiler": "~11.2.4",
-    "@angular/core": "~11.2.4",
-    "@angular/forms": "~11.2.4",
-    "@angular/platform-browser": "~11.2.4",
-    "@angular/platform-browser-dynamic": "~11.2.4",
-    "@angular/router": "~11.2.4",
-    "@fortawesome/angular-fontawesome": "^0.8.2",
-    "@fortawesome/fontawesome-svg-core": "^1.2.35",
-    "@fortawesome/free-solid-svg-icons": "^5.15.3",
+    "@angular/cdk": "^13.1.3",
+    "@angular/common": "^13.1.3",
+    "@angular/compiler": "^13.1.3",
+    "@angular/core": "^13.1.3",
+    "@angular/forms": "^13.1.3",
+    "@angular/localize": "^13.1.3",
+    "@angular/material": "^13.1.3",
+    "@angular/platform-browser": "^13.1.3",
+    "@angular/platform-browser-dynamic": "^13.1.3",
+    "@angular/router": "^13.1.3",
+    "@fortawesome/angular-fontawesome": "^0.10.1",
+    "@fortawesome/fontawesome-svg-core": "^1.2.36",
+    "@fortawesome/free-solid-svg-icons": "^5.15.4",
+    "@ng-bootstrap/ng-bootstrap": "^11.0.0",
+    "@ngx-translate/core": "^13.0.0",
+    "@ngx-translate/http-loader": "^6.0.0",
     "bootstrap": "^4.6.0",
+    "d3": "^6.7.0",
+    "dayjs": "^1.10.7",
+    "deepmerge": "^4.2.2",
+    "geostyler-legend": "^2.1.1",
+    "geostyler-openlayers-parser": "^3.0.0",
+    "geostyler-qgis-parser": "^1.0.0",
+    "geostyler-sld-parser": "^3.0.1",
+    "geostyler-style": "^5.0.0",
+    "geotiff": "^1.0.8",
+    "hslayers-ng": "c:/Projects/hslayers-ng/dist/hslayers/",
     "jquery": "^3.6.0",
     "lodash": "^4.17.21",
     "moment": "^2.29.1",
     "moment-timezone": "^0.5.33",
-    "ngx-cookie-service": "^11.0.2",
-    "primeicons": "^4.1.0",
-    "primeng": "^11.3.1",
-    "rxjs": "~6.6.0",
-    "tslib": "^2.0.0",
+    "ngx-color": "^7.2.0",
+    "ngx-cookie-service": "^13.1.2",
+    "ol": "^6.9.0",
+    "ol-popup": "^4.0.0",
+    "polygon-clipping": "^0.15.3",
+    "polygon-splitter": "^0.0.7",
+    "primeicons": "^5.0.0",
+    "primeng": "^13.1.0",
+    "proj4": "^2.7.5",
+    "queue": "^6.0.2",
+    "resumablejs": "^1.1.0",
+    "rxjs": "^7.5.2",
+    "share-api-polyfill": "^1.0.21",
+    "tslib": "2.0.0",
     "vega": "^5.20.2",
     "vega-tooltip": "^0.25.1",
-    "zone.js": "~0.11.3"
+    "xml-js": "^1.6.11",
+    "zone.js": "^0.11.3"
   },
   "devDependencies": {
-    "@angular-devkit/build-angular": "~0.1102.3",
-    "@angular/cli": "~11.2.3",
-    "@angular/compiler-cli": "~11.2.4",
+    "@angular-architects/module-federation": "^14.0.1",
+    "@angular-builders/custom-webpack": "^13.0.0",
+    "@angular-devkit/build-angular": "^13.1.3",
+    "@angular-eslint/builder": "^13.0.1",
+    "@angular-eslint/eslint-plugin": "^13.0.1",
+    "@angular-eslint/eslint-plugin-template": "^13.0.1",
+    "@angular-eslint/schematics": "^13.0.1",
+    "@angular-eslint/template-parser": "^13.0.1",
+    "@angular/cli": "~13.1.3",
+    "@angular/compiler-cli": "~13.1.3",
     "@types/jasmine": "~3.6.0",
     "@types/node": "^12.11.1",
     "codelyzer": "^6.0.0",
-    "jasmine-core": "~3.6.0",
-    "jasmine-spec-reporter": "~5.0.0",
-    "karma": "~6.1.0",
-    "karma-coverage": "~2.0.3",
+    "jasmine-core": "~4.0.0",
+    "jasmine-spec-reporter": "~7.0.0",
+    "karma": "~6.3.12",
     "karma-chrome-launcher": "~3.1.0",
-    "karma-jasmine": "~4.0.0",
-    "karma-jasmine-html-reporter": "^1.5.0",
+    "karma-coverage": "~2.1.0",
+    "karma-jasmine": "~4.0.1",
+    "karma-jasmine-html-reporter": "^1.7.0",
+    "ngx-build-plus": "^13.0.1",
     "protractor": "~7.0.0",
     "ts-node": "~8.3.0",
     "tslint": "~6.1.0",
-    "typescript": "~4.1.5"
+    "typescript": "4.5.5"
   }
 }

+ 1 - 1
src/app/app-routing.module.ts

@@ -5,7 +5,7 @@ import {DashboardComponent} from './dashboard/components/dashboard.component';
 import {AuthGuard} from './auth/guards/auth.guard';
 import {RoleGuard} from './auth/guards/role.guard';
 import {SensorComponent} from './sensor/components/sensor.component';
-import {UnitComponent} from './unit/components/unit.component';
+import { UnitComponent } from './unit/components/unit.component';
 
 const routes: Routes = [
   {

+ 3 - 3
src/app/app.module.ts

@@ -1,5 +1,5 @@
 import {NgModule} from '@angular/core';
-import { HashLocationStrategy, LocationStrategy } from '@angular/common';
+import { PathLocationStrategy, LocationStrategy } from '@angular/common';
 import { BrowserModule } from '@angular/platform-browser';
 
 import {AppRoutingModule} from './app-routing.module';
@@ -14,7 +14,7 @@ import {SensorModule} from './sensor/sensor.module';
 import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
 import {UnitModule} from './unit/unit.module';
 import {ConfirmationService, MessageService} from 'primeng/api';
-import {ToastModule} from 'primeng/toast';
+import { ToastModule } from 'primeng/toast';
 
 @NgModule({
   declarations: [
@@ -37,7 +37,7 @@ import {ToastModule} from 'primeng/toast';
   providers: [
     ConfirmationService,
     MessageService,
-    { provide: LocationStrategy, useClass: HashLocationStrategy }
+    { provide: LocationStrategy, useClass: PathLocationStrategy }
   ],
   bootstrap: [AppComponent]
 })

+ 6 - 2
src/app/auth/guards/role.guard.ts

@@ -7,6 +7,7 @@ import {
 } from '@angular/router';
 import { Observable } from 'rxjs';
 import { AuthService } from '../services/auth.service';
+import { LoginService } from '../../shared/api/endpoints/services';
 
 @Injectable({
   providedIn: 'root'
@@ -15,6 +16,7 @@ export class RoleGuard implements CanActivate {
 
   constructor(
     private authService: AuthService,
+    private loginService: LoginService,
     private router: Router,
   ) {}
 
@@ -23,9 +25,11 @@ export class RoleGuard implements CanActivate {
     state: RouterStateSnapshot
   ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
     const expectedRole: string[] = routeSnapshot.data.expectedRole;
-    if (expectedRole.includes(this.authService.getUser().userInfo?.rightsId.toString())) {
+    let user = this.authService.getUser();
+    let userInfo = user.userInfo;
+    if (expectedRole.includes(userInfo?.rightsId.toString()) || expectedRole.includes(user.userCookie.rightsID.toString())) {
       console.log('You have enough rights!');
-     return true;
+      return true;
     }
     console.log('You are not allowed to be here!');
     this.router.navigate(['']);

+ 12 - 11
src/app/auth/interceptors/auth.interceptor.ts

@@ -9,16 +9,15 @@ import {Observable, throwError, of} from 'rxjs';
 
 import {catchError} from 'rxjs/operators';
 import {AuthService} from '../services/auth.service';
-import {CookieService} from 'ngx-cookie-service';
 import {GlobalVariable} from '../../globals';
 import {ToastService} from '../../shared/services/toast.service';
+import { environment } from '../../../environments/environment';
 
 @Injectable()
 export class AuthInterceptor implements HttpInterceptor {
 
   constructor(
     private authService: AuthService,
-    private cookieService: CookieService,
     private toastService: ToastService
   ) {
   }
@@ -30,17 +29,19 @@ export class AuthInterceptor implements HttpInterceptor {
    */
   intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
     // check if data in cookies
-    if (!this.cookieService.get(GlobalVariable.SESSION_ID) && !request.url.includes('ControllerServlet')) {
-      console.log('No sessionid!');
-      this.cookieService.deleteAll();
-      this.authService.doLogout();
-      return of(null);
-    }
+    //if (!this.cookieService.get(GlobalVariable.SESSION_ID) && !request.url.includes('ControllerServlet')) {
+    //  console.log('No sessionid!');
+    //  this.cookieService.deleteAll();
+    //  this.authService.doLogout();
+    //  return of(null);
+    //}
 
     console.log('Sending request!', request.url);
-    request = request.clone({
-      withCredentials: true
-    });
+    if (request.url.includes(environment.sensLogBaseUrl)) {
+      request = request.clone({
+        withCredentials: true
+      });
+    }
 
     return next.handle(request)
       .pipe(

+ 10 - 23
src/app/dashboard/components/dashboard.component.html

@@ -1,29 +1,16 @@
 <app-nav-bar (emitNewUnit)="addUnit($event)" [sensorTypes]="sensorTypes"></app-nav-bar>
 
 <div class="container dashboard">
-  <p-accordion>
-      <p-accordionTab *ngFor="let unit of units">
-        <p-header [className]="'dashboard-unit-wrapper'">
-          <div [className]="'row dashboard-unit'">
-            <div class="col-sm-5 col-md-7 col-xl-9"><h3 class="unitName">{{ unit.unit.description}}</h3></div>
-            <div class="col-sm-7 col-md-5 col-xl-3 dashboard-unit-heading">
-              <button pButton type="button" label="Sensors graph" class="p-button-primary" icon="pi pi-chart-line" [id]="unit.unit.description"  [routerLink]="['/dashboard/unit', unit.unit.unitId]" [queryParams]="{unitDescription: unit.unit.description}"></button>
-              <div class="dashboard-button-separator"></div>
-              <button pButton type="button" class="p-button-warning" icon="pi pi-cog" [id]="'manipulation_'+unit.unit.unitId"  (click)="showItems($event, unit.unit); menu.toggle($event)"></button>
-              <p-menu #menu [popup]="true" [model]="items" [appendTo]="'body'" [baseZIndex]="50"></p-menu>
-            </div>
-          </div>
-        </p-header>
-        <div>
-          <ng-container *ngFor="let sensor of unit.sensors; let i = index">
-            <ng-container *ngIf="i > 0 && unit.sensors[i].sensorId.toString().slice(0, 5) !== unit.sensors[i-1].sensorId.toString().slice(0, 5)">
-              <hr>
-            </ng-container>
-            <app-sensors *ngIf="sensor" [sensor]="sensor" [unit]="unit.unit" [phenomenons]="phenomenons" [loggedUser]="loggedUser" (emitSensorDeletion)="deleteSensor(unit.unit.unitId, $event)" [sensorTypes]="sensorTypes"></app-sensors>
-          </ng-container>
-        </div>
-      </p-accordionTab>
-  </p-accordion>
+  <p-tabView>
+    <p-tabPanel header="Dashboard">
+      <unit-list [user]="loggedUser" [units]="units"></unit-list>
+    </p-tabPanel>
+    <p-tabPanel header="Map view">
+      <ng-template pTemplate="content">
+        <app-map [units]="units"></app-map>
+      </ng-template>
+    </p-tabPanel>
+  </p-tabView>
 </div>
 
 <app-unit-popup *ngIf="showEditUnitPopup" [(isVisible)]="showEditUnitPopup" [unit]="editedUnit"></app-unit-popup>

+ 12 - 0
src/app/dashboard/components/dashboard.component.scss

@@ -0,0 +1,12 @@
+::ng-deep .loading-progress {
+  min-height: 200px;
+}
+
+::ng-deep .p-tabview-nav li {
+  width: 50%;
+}
+/*::ng-deep .p-tabview {
+  color: #0B1226;
+  background-color: #F2F2F2;
+  padding: 0;
+}*/

+ 7 - 129
src/app/dashboard/components/dashboard.component.ts

@@ -26,7 +26,6 @@ import {Subscription} from 'rxjs';
 export class DashboardComponent implements OnInit, OnDestroy {
 
   loggedUser: User;
-  items: MenuItem[] = [];
   position: 'bottom';
   groups: Group[];
   units: Array<{ drivers?: Drivers; generalInfo?: GeneralInfo; holder?: any; lastpos?: Lastpos; sensors?: Array<Sensor>; unit?: Unit }>;
@@ -34,16 +33,16 @@ export class DashboardComponent implements OnInit, OnDestroy {
   showEditUnitPopup = false;
   showInsertSensorPopup = false;
   showInsertPositionPopup = false;
-  phenomenons: Phenomenon[];
+  //phenomenons: Phenomenon[];
   sensorTypes: SensorType[];
   subscription: Subscription[] = [];
 
   constructor(
     private dataService: DataService,
     private sensorService: SensorsService,
-    private confirmationService: ConfirmationService,
-    private messageService: MessageService,
-    private managementService: ManagementService,
+    //private confirmationService: ConfirmationService,
+    //private messageService: MessageService,
+    //private managementService: ManagementService,
     private toastService: ToastService,
     private authService: AuthService
   ) {
@@ -64,9 +63,9 @@ export class DashboardComponent implements OnInit, OnDestroy {
    * Get necessary data from backend
    */
   initData() {
-    this.sensorService.getPhenomenons().subscribe(
-      response => this.phenomenons = response
-    );
+    //this.sensorService.getPhenomenons().subscribe(
+    //  response => this.phenomenons = response
+    //);
     this.sensorService.getSensorTypes().subscribe(
       response => this.sensorTypes = response
     );
@@ -96,89 +95,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
   }
 
   /**
-   * Show edit unit
-   * @param $event click event
-   * @param unit edited unit
-   */
-  editUnitPopup($event: MouseEvent, unit: Unit) {
-    this.editedUnit = unit;
-    this.showEditUnitPopup = true;
-  }
-
-  /**
-   * Show insert unit
-   * @param $event click event
-   * @param unit unit for sensor insert
-   */
-  insertSensorPopup($event: any, unit: Unit) {
-    this.showInsertSensorPopup = true;
-    this.editedUnit = unit;
-  }
-
-  /**
-   * Detele unit confirmation
-   * @param $event click event
-   * @param unit unit to delete
-   */
-  deleteUnit($event: any, unit: Unit) {
-    this.confirmationService.confirm({
-      message: 'Do you want to delete this unit?',
-      header: 'Delete unit confirmation',
-      icon: 'pi pi-info-circle',
-      accept: () => {
-        this.processUnitDeletion(unit);
-      },
-      reject: () => {
-        this.toastService.operationRejected();
-      },
-      key: 'positionDialog'
-    });
-  }
-
-  /**
-   * Send delete unit request to backend
-   * @param unit to delete
-   */
-  processUnitDeletion(unit: Unit) {
-    this.managementService.deleteUnit$Response({body: {
-      unit: {
-        unit_id: unit.unitId
-      }}
-    }).pipe(
-      map((response: HttpResponse<any>) => {
-        if (response.status === 200) {
-          this.toastService.showSuccessMessage(response.body.message);
-          this.units = this.units.filter(testedUnit => testedUnit.unit.unitId !== unit.unitId);
-        } else {
-        }
-      })
-    ).toPromise().then().catch(err => this.toastService.showError(err.error.message));
-  }
-
-  /**
-   * Show menu items to manipulate with unit
-   * @param $event click event
-   * @param unit unit we want edit
-   */
-  showItems($event: any, unit: Unit) {
-    $event.stopPropagation();
-    this.items = [
-      {label: 'Edit unit', icon: 'pi pi-cog', command: () => {
-          this.editUnitPopup($event, unit);
-        }},
-      {label: 'Insert position', icon: 'pi pi-cog', command: () => {
-          this.insertPosition($event, unit);
-        }},
-      {label: 'Delete unit', icon: 'pi pi-times', command: () => {
-          this.deleteUnit($event, unit);
-        }},
-      {label: 'Add sensor', icon: 'pi pi-cog', command: () => {
-          this.insertSensorPopup($event, unit);
-        }}
-    ]
-  }
-
-  /**
    * Add created unit to memory so we do not need call backend
    * @param inserted unit
    */
@@ -202,42 +118,4 @@ export class DashboardComponent implements OnInit, OnDestroy {
       sensors
     })
   }
-
-  /**
-   * Add created sensors to unit in memory so we do not need call backend
-   * @param inserted sensors
-   */
-  addSensors(inserted: any) {
-    inserted.sensors.forEach(sens => {
-      this.units.find(un => un.unit.unitId === inserted.unit.unit_id).sensors.push({
-        sensorId: sens.sensor_id,
-        sensorType: sens.sensor_type,
-        sensorName: sens.sensor_name,
-        phenomenon: {
-          phenomenonId: sens.phenomenon.phenomenon_id.toString()
-        }
-      })
-    });
-  }
-
-  /**
-   * Delete sensor from memory
-   * @param unitId sensor unit
-   * @param sensor sensor to delete
-   */
-  deleteSensor(unitId: number, sensor: Sensor) {
-    this.units.find(unit => unit.unit.unitId === unitId).sensors =
-      this.units.find(unit => unit.unit.unitId === unitId).sensors.filter(testedSensor => testedSensor.sensorId !== sensor.sensorId);
-  }
-
-  /**
-   * Show insert position popup
-   * @param $event click event
-   * @param unit unit to insert position for
-   */
-  insertPosition($event: any, unit: Unit) {
-    $event.stopPropagation();
-    this.showInsertPositionPopup = true;
-    this.editedUnit = unit;
-  }
 }

+ 3 - 0
src/app/dashboard/components/map/map.component.html

@@ -0,0 +1,3 @@
+<div class="container map">
+  <hslayers></hslayers>
+</div>

+ 10 - 0
src/app/dashboard/components/map/map.component.scss

@@ -0,0 +1,10 @@
+hslayers {
+  display: block;
+  height: calc(100vh - 220px);
+  min-height: 300px;
+  width: 100%;
+}
+
+::ng-deep a.white, ::ng-deep a.white:hover {
+  color: #ffffff;
+}

+ 248 - 0
src/app/dashboard/components/map/map.component.ts

@@ -0,0 +1,248 @@
+import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
+import { ConfirmationService, MenuItem, MessageService } from 'primeng/api';
+import { Extent } from 'ol/extent';
+import { Group, Tile, Vector as VectorLayer } from 'ol/layer';
+import { OSM, TileWMS, Vector as VectorSource } from 'ol/source';
+import GeoJSON from 'ol/format/GeoJSON';
+import { transform, transformExtent } from 'ol/proj';
+import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';
+import View from 'ol/View';
+import { Unit } from '../../../shared/api/endpoints/models/unit';
+import { Lastpos } from '../../../shared/api/endpoints/models/lastpos';
+import { Drivers } from '../../../shared/api/endpoints/models/drivers';
+import { GeneralInfo } from '../../../shared/api/endpoints/models/general-info';
+import { Sensor } from '../../../shared/api/endpoints/models/sensor';
+import {
+  HsConfig,
+  HsMapService,
+  HsLayerManagerService,
+  HsLayerEditorService,
+  HsEventBusService
+} from 'hslayers-ng';
+import { environment } from '../../../../environments/environment';
+import Geometry from 'ol/geom/Geometry';
+
+@Component({
+  selector: 'app-map',
+  templateUrl: './map.component.html',
+  styleUrls: ['./map.component.scss']
+})
+export class MapComponent implements OnInit, OnDestroy, OnChanges {
+
+  @Input('units') units: Array<{ drivers?: Drivers; generalInfo?: GeneralInfo; holder?: any; lastpos?: Lastpos; sensors?: Array<Sensor>; unit?: Unit }>;
+  mapReady: boolean = false;
+  dataReady: boolean = false;
+  unitsLayer: VectorLayer<VectorSource<Geometry>>;
+
+  constructor(
+    private confirmationService: ConfirmationService,
+    private messageService: MessageService,
+    public HsConfig: HsConfig,
+    public HsMapService: HsMapService,
+    public HsLayerManagerService: HsLayerManagerService,
+    public HsLayerEditorService: HsLayerEditorService,
+    public HsEventBusService: HsEventBusService
+  ) {
+    this.HsConfig.update({
+      assetsPath: environment.hslayersAssetsPath,
+      popUpDisplay: 'click',
+      proxyPrefix: window.location.hostname.includes('localhost')
+        ? `${window.location.protocol}//${window.location.hostname}:8085/`
+        : '/proxy/',
+      box_layers: [
+        new Group({
+          properties: {
+            title: 'Base layer',
+          },
+          layers: [
+            new Tile({
+              source: new OSM(),
+              visible: true,
+              properties: {
+                title: 'OpenStreetMap',
+                base: true,
+                removable: false
+              }
+            })
+          ]
+        })
+      ],
+      //default_layers: this.loadLayers(),
+      componentsEnabled: {
+        sidebar: false,
+        toolbar: false,
+        guiOverlay: true,
+        drawToolbar: false,
+        searchToolbar: false,
+        measureToolbar: false,
+        sensors: false,
+        crossfilter: false,
+        geolocationButton: false,
+        tracking: false,
+        mapControls: true,
+        basemapGallery: true
+      },
+      panelsEnabled: {
+        info: false, //(true by default),
+        layermanager: false
+      },
+      sidebarPosition: 'invisible',
+      queryPoint: 'hidden',
+      default_view: new View({
+        maxZoom: 17
+      //    center: [1873444.3416929364, 6286508.646897761], // Latitude longitude    to Spherical Mercator
+      //    extent: [1871197.0792499082, 6282949.4051418, 1873509.6915773677, 6287134.61866585],
+      //    multiWorld: false,
+      //    zoom: 6
+      })
+    });
+  }
+
+  ngOnInit(): void {
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes['units']) {
+      //this.dataReady = true;
+
+      //if (this.mapReady)
+      this.HsMapService.loaded().then((map) => {
+        this.initMap();
+
+        let unitsExtent: Extent = this.unitsLayer.getSource().getExtent();
+        this.HsMapService.map.getView().fit(unitsExtent, { size: this.HsMapService.map.getSize() });
+
+        this.HsMapService.map.on("pointermove", function (evt) {
+          var hit = this.forEachFeatureAtPixel(evt.pixel, function (feature, layer) {
+            return true;
+          });
+          if (hit) {
+            this.getTargetElement().style.cursor = 'pointer';
+          } else {
+            this.getTargetElement().style.cursor = '';
+          }
+        });
+      });
+    }
+  }
+
+  /**
+   * Unsubscribe after leaving
+   */
+  ngOnDestroy(): void {
+  }
+
+  initMap(): void {
+    this.unitsLayer = this.initUnitsLayer(this.HsMapService.map);
+    this.HsMapService.map.addLayer(this.unitsLayer);
+
+  //  this.HsMapService.map.on('click', function (evt) {
+  //    var feature = this.HsMapService.map.forEachFeatureAtPixel(evt.pixel,
+  //      function (feature) {
+  //        return feature;
+  //      });
+
+  //    if (feature) {
+  //      let unitId = feature.getProperties()['unitId'];
+  //      window.open('/#/dashboard / unit /' + unitId);
+  //    }
+  //  })
+  }
+
+  /**
+   * Crete vector layer from units positions
+   */
+  initUnitsLayer(map: any): any {
+    var geoJsonUnits = {
+      'type': 'FeatureCollection',
+      'crs': {
+        'type': 'name',
+        'properties': {
+          'name': 'EPSG:3857',
+        }
+      }
+    };
+    geoJsonUnits['features'] = this.units ? this.units.filter(u => (u.lastpos.position.x > 0)).map(u => this.getUnitPos(u.lastpos, u.unit)) : [];
+
+    const vectorSource = new VectorSource({
+      features: new GeoJSON().readFeatures(geoJsonUnits),
+    });
+    vectorSource.on('addfeature', function () {
+      map.getView().fit(vectorSource.getExtent());
+    });
+
+    const vectorLayer = new VectorLayer({
+      source: vectorSource,
+      style: this.styleFunction,
+      properties: {
+        popUp: {
+          displayFunction: function (feature) {
+            return `<ul>
+              <li>Unit name &nbsp; ${feature.get('title')}</li>
+              <li>Unit ID &nbsp; ${feature.get('id')}</li>
+            </ul>
+            <button class="p-button-primary" style="display: block; margin: auto;"><a class="white" href="/dashboard/unit/${feature.get('id')}" target="_blank">Open Unit detail...</a></button>
+            `;
+          }
+        //  attributes: [
+        //    {
+        //      attribute: 'id',
+        //      label: 'Unit ID'
+        //    },
+        //    {
+        //      attribute: 'online',
+        //      label: 'Online',
+        //      displayFunction: function (val) {
+        //        return '<span class="' + (val ? 'far fa-check-square' : 'far fa-square') + '"></span>';
+        //      }
+        //    },
+        //    {
+        //      attribute: 'test',
+        //      label: 'test'
+        //    },
+        //    {
+        //      attribute: 'id',
+        //      label: 'Detail',
+        //      displayFunction: function (val) {
+        //        return '<a class="white" href="/#/dashboard/unit/' + val + '" target="_blank">open unit detail...</a>';
+        //      }
+        //    }
+        //  ]
+        }
+      }
+    });
+
+    return vectorLayer;
+  }
+
+  styleFunction(feature: any): Style {
+    return new Style({
+      image: new CircleStyle({
+        radius: 5,
+        fill: new Fill({
+          color: 'red'
+        }),
+        stroke: new Stroke({
+          color: 'blue',
+          width: 2
+        })
+      })
+    })
+  }
+
+  getUnitPos(pos: Lastpos, unit: Unit): any {
+    return {
+      "type": "Feature",
+      "geometry": {
+        "type": "Point",
+        "coordinates": [pos.position.x, pos.position.y]
+      },
+      "properties": {
+        "id": unit.unitId,
+        "title": unit.description,
+        "online": pos.attributes.is_online,
+        "moving": pos.attributes.is_moving
+      }
+    }
+  }
+}

+ 23 - 0
src/app/dashboard/components/map/map.module.ts

@@ -0,0 +1,23 @@
+////import { NgModule } from '@angular/core';
+////import { CommonModule } from '@angular/common';
+////import { MapComponent } from './map.component';
+////import { NavBarModule } from '../shared/nav-bar/nav-bar.module';
+////import { RouterModule } from '@angular/router';
+////import { ButtonModule } from 'primeng/button';
+////import { MenuModule } from 'primeng/menu';
+////import { HslayersModule } from 'hslayers-ng';
+
+
+
+////@NgModule({
+////  declarations: [MapComponent],
+////  imports: [
+////    HslayersModule,
+////    CommonModule,
+////    NavBarModule,
+////    RouterModule,
+////    ButtonModule,
+////    MenuModule
+////  ]
+////})
+////export class MapModule { }

+ 28 - 0
src/app/dashboard/components/unit-list/unit-list.component.html

@@ -0,0 +1,28 @@
+<div *ngIf="(units == null)" class="loading-progress">
+  <div>Loading sensor data</div>
+  <p-progressBar mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar>
+</div>
+
+<p-accordion>
+  <p-accordionTab *ngFor="let unit of units">
+    <p-header [className]="'dashboard-unit-wrapper'">
+      <div [className]="'row dashboard-unit'">
+        <div class="col-sm-5 col-md-7 col-xl-9"><h3 class="unitName">{{ unit.unit.description }}</h3></div>
+        <div class="col-sm-7 col-md-5 col-xl-3 dashboard-unit-heading">
+          <button pButton type="button" label="Sensors graph" class="p-button-primary" icon="pi pi-chart-line" [id]="unit.unit.description" [routerLink]="['/dashboard/unit', unit.unit.unitId]" [queryParams]="{unitDescription: unit.unit.description}"></button>
+          <div class="dashboard-button-separator"></div>
+          <button pButton type="button" class="p-button-warning" icon="pi pi-cog" [id]="'manipulation_'+unit.unit.unitId" (click)="showItems($event, unit.unit); menu.toggle($event)"></button>
+          <p-menu #menu [popup]="true" [model]="items" [appendTo]="'body'" [baseZIndex]="50"></p-menu>
+        </div>
+      </div>
+    </p-header>
+    <div>
+      <ng-container *ngFor="let sensor of unit.sensors; let i = index">
+        <ng-container *ngIf="i > 0 && unit.sensors[i].sensorId.toString().slice(0, 5) !== unit.sensors[i-1].sensorId.toString().slice(0, 5)">
+          <hr>
+        </ng-container>
+        <app-sensors *ngIf="sensor" [sensor]="sensor" [unit]="unit.unit" [phenomenons]="phenomenons" [loggedUser]="loggedUser" (emitSensorDeletion)="deleteSensor(unit.unit.unitId, $event)" [sensorTypes]="sensorTypes"></app-sensors>
+      </ng-container>
+    </div>
+  </p-accordionTab>
+</p-accordion>

+ 4 - 0
src/app/dashboard/components/unit-list/unit-list.component.scss

@@ -0,0 +1,4 @@
+::ng-deep .p-accordion.p-component {
+  max-height: calc(100vh - 220px);
+  overflow: auto;
+}

+ 256 - 0
src/app/dashboard/components/unit-list/unit-list.component.ts

@@ -0,0 +1,256 @@
+import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { Group } from '../../../shared/api/endpoints/models/group';
+import { Drivers } from '../../../shared/api/endpoints/models/drivers';
+import { GeneralInfo } from '../../../shared/api/endpoints/models/general-info';
+import { Lastpos } from '../../../shared/api/endpoints/models/lastpos';
+import { Sensor } from '../../../shared/api/endpoints/models/sensor';
+import { Unit } from '../../../shared/api/endpoints/models/unit';
+import { DataService } from '../../../shared/api/endpoints/services/data.service';
+import { Phenomenon } from '../../../shared/api/endpoints/models/phenomenon';
+import { SensorsService } from '../../../shared/api/endpoints/services/sensors.service';
+import { ConfirmationService, MenuItem, MessageService } from 'primeng/api';
+import { ManagementService } from '../../../shared/api/endpoints/services/management.service';
+import { ToastService } from '../../../shared/services/toast.service';
+import { map } from 'rxjs/operators';
+import { HttpResponse } from '@angular/common/http';
+import { AuthService } from '../../../auth/services/auth.service';
+import { User } from '../../../auth/models/user';
+import { SensorType } from '../../../shared/api/endpoints/models/sensor-type';
+import { Subscription } from 'rxjs';
+
+@Component({
+  selector: 'unit-list',
+  templateUrl: './unit-list.component.html',
+  styleUrls: ['./unit-list.component.scss']
+})
+export class UnitListComponent implements OnInit, OnDestroy {
+
+  @Input('user') loggedUser: User;
+  @Input('units') units: Array<{ drivers?: Drivers; generalInfo?: GeneralInfo; holder?: any; lastpos?: Lastpos; sensors?: Array<Sensor>; unit?: Unit }>;
+
+  phenomenons: Phenomenon[];
+  sensorTypes: SensorType[];
+
+  inProgress: Boolean = true;
+  items: MenuItem[] = [];
+  position: 'bottom';
+  groups: Group[];
+  editedUnit: Unit;
+  showEditUnitPopup = false;
+  showInsertSensorPopup = false;
+  showInsertPositionPopup = false;
+  subscription: Subscription[] = [];
+
+  constructor(
+    private dataService: DataService,
+    private sensorService: SensorsService,
+    private confirmationService: ConfirmationService,
+    private messageService: MessageService,
+    private managementService: ManagementService,
+    private toastService: ToastService,
+    private authService: AuthService
+  ) {
+    this.initData();
+  }
+
+  ngOnInit(): void {
+  }
+
+  /**
+   * Unsubscribe after leaving
+   */
+  ngOnDestroy(): void {
+    this.subscription.forEach(subs => subs.unsubscribe());
+  }
+
+  /**
+   * Get necessary data from backend
+   */
+  initData() {
+    this.sensorService.getPhenomenons().subscribe(
+      response => this.phenomenons = response
+    );
+  //  this.sensorService.getSensorTypes().subscribe(
+  //    response => this.sensorTypes = response
+  //  );
+  //  this.setUser();
+  //  this.getUnits();
+  }
+
+  /**
+   * Get user from user state
+   */
+  setUser() {
+    this.authService.getUserState().subscribe(res => {
+      if (res) {
+        this.loggedUser = res;
+      }
+    });
+  }
+
+  /**
+   * Get all units and theirs sensors from backend
+   */
+  //getUnits() {
+  //  this.dataService.getData().subscribe(data => {
+  //    this.units = data;
+  //    this.units.forEach(unit => unit.sensors.sort((a, b) => a.sensorId - b.sensorId));
+  //  }, err => this.toastService.showError(err.error.message));
+  //}
+
+  /**
+   * Show edit unit
+   * @param $event click event
+   * @param unit edited unit
+   */
+  editUnitPopup($event: MouseEvent, unit: Unit) {
+    this.editedUnit = unit;
+    this.showEditUnitPopup = true;
+  }
+
+  /**
+   * Show insert unit
+   * @param $event click event
+   * @param unit unit for sensor insert
+   */
+  insertSensorPopup($event: any, unit: Unit) {
+    this.showInsertSensorPopup = true;
+    this.editedUnit = unit;
+  }
+
+  /**
+   * Detele unit confirmation
+   * @param $event click event
+   * @param unit unit to delete
+   */
+  deleteUnit($event: any, unit: Unit) {
+    this.confirmationService.confirm({
+      message: 'Do you want to delete this unit?',
+      header: 'Delete unit confirmation',
+      icon: 'pi pi-info-circle',
+      accept: () => {
+        this.processUnitDeletion(unit);
+      },
+      reject: () => {
+        this.toastService.operationRejected();
+      },
+      key: 'positionDialog'
+    });
+  }
+
+  /**
+   * Send delete unit request to backend
+   * @param unit to delete
+   */
+  processUnitDeletion(unit: Unit) {
+    this.managementService.deleteUnit$Response({
+      body: {
+        unit: {
+          unit_id: unit.unitId
+        }
+      }
+    }).pipe(
+      map((response: HttpResponse<any>) => {
+        if (response.status === 200) {
+          this.toastService.showSuccessMessage(response.body.message);
+          this.units = this.units.filter(testedUnit => testedUnit.unit.unitId !== unit.unitId);
+        } else {
+        }
+      })
+    ).toPromise().then().catch(err => this.toastService.showError(err.error.message));
+  }
+
+  /**
+   * Show menu items to manipulate with unit
+   * @param $event click event
+   * @param unit unit we want edit
+   */
+  showItems($event: any, unit: Unit) {
+    $event.stopPropagation();
+    this.items = [
+      {
+        label: 'Edit unit', icon: 'pi pi-cog', command: () => {
+          this.editUnitPopup($event, unit);
+        }
+      },
+      {
+        label: 'Insert position', icon: 'pi pi-cog', command: () => {
+          this.insertPosition($event, unit);
+        }
+      },
+      {
+        label: 'Delete unit', icon: 'pi pi-times', command: () => {
+          this.deleteUnit($event, unit);
+        }
+      },
+      {
+        label: 'Add sensor', icon: 'pi pi-cog', command: () => {
+          this.insertSensorPopup($event, unit);
+        }
+      }
+    ]
+  }
+
+  /**
+   * Add created unit to memory so we do not need call backend
+   * @param inserted unit
+   */
+  addUnit(inserted: any) {
+    const sensors: Sensor[] = [];
+    inserted.sensors.forEach(sens => {
+      sensors.push({
+        sensorId: sens.sensor_id,
+        sensorType: sens.sensor_type,
+        sensorName: sens.sensor_name,
+        phenomenon: {
+          phenomenonId: sens.phenomenon.phenomenon_id.toString()
+        }
+      })
+    });
+    this.units.push({
+      unit: {
+        unitId: inserted.unit.unit_id,
+        description: inserted.unit.description
+      },
+      sensors
+    })
+  }
+
+  /**
+   * Add created sensors to unit in memory so we do not need call backend
+   * @param inserted sensors
+   */
+  addSensors(inserted: any) {
+    inserted.sensors.forEach(sens => {
+      this.units.find(un => un.unit.unitId === inserted.unit.unit_id).sensors.push({
+        sensorId: sens.sensor_id,
+        sensorType: sens.sensor_type,
+        sensorName: sens.sensor_name,
+        phenomenon: {
+          phenomenonId: sens.phenomenon.phenomenon_id.toString()
+        }
+      })
+    });
+  }
+
+  /**
+   * Delete sensor from memory
+   * @param unitId sensor unit
+   * @param sensor sensor to delete
+   */
+  deleteSensor(unitId: number, sensor: Sensor) {
+    this.units.find(unit => unit.unit.unitId === unitId).sensors =
+      this.units.find(unit => unit.unit.unitId === unitId).sensors.filter(testedSensor => testedSensor.sensorId !== sensor.sensorId);
+  }
+
+  /**
+   * Show insert position popup
+   * @param $event click event
+   * @param unit unit to insert position for
+   */
+  insertPosition($event: any, unit: Unit) {
+    $event.stopPropagation();
+    this.showInsertPositionPopup = true;
+    this.editedUnit = unit;
+  }
+}

+ 30 - 24
src/app/dashboard/dashboard.module.ts

@@ -1,37 +1,43 @@
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { DashboardComponent } from './components/dashboard.component';
-import {NavBarModule} from '../shared/nav-bar/nav-bar.module';
-import {RouterModule} from '@angular/router';
-import {ButtonModule} from 'primeng/button';
-import {AccordionModule} from 'primeng/accordion';
+import { NavBarModule } from '../shared/nav-bar/nav-bar.module';
+import { RouterModule } from '@angular/router';
+import { ButtonModule } from 'primeng/button';
+import { AccordionModule } from 'primeng/accordion';
 import { SensorsComponent } from './components/sensors/sensors.component';
 import { UnitPopupComponent } from './components/unit-popup/unit-popup.component';
-import {DialogModule} from 'primeng/dialog';
+import { DialogModule } from 'primeng/dialog';
 import { SensorPopupComponent } from './components/sensor-popup/sensor-popup.component';
-import {ReactiveFormsModule} from '@angular/forms';
+import { ReactiveFormsModule } from '@angular/forms';
 import { SensorInsertPopupComponent } from './components/sensor-insert-popup/sensor-insert-popup.component';
-import {ConfirmDialogModule} from 'primeng/confirmdialog';
-import {SplitButtonModule} from 'primeng/splitbutton';
+import { ConfirmDialogModule } from 'primeng/confirmdialog';
+import { SplitButtonModule } from 'primeng/splitbutton';
 import { PositionInsertPopupComponent } from './components/position-insert-popup/position-insert-popup.component';
-import {MenuModule} from 'primeng/menu';
-
-
+import { MenuModule } from 'primeng/menu';
+import { HslayersModule } from 'hslayers-ng';
+import { UnitListComponent } from './components/unit-list/unit-list.component';
+import { MapComponent } from './components/map/map.component';
+import { ProgressBarModule } from 'primeng/progressbar';
+import { TabViewModule } from 'primeng/tabview';
 
 @NgModule({
   declarations: [DashboardComponent, SensorsComponent, UnitPopupComponent, SensorPopupComponent, SensorInsertPopupComponent,
-    PositionInsertPopupComponent],
-    imports: [
-        CommonModule,
-        NavBarModule,
-        RouterModule,
-        ButtonModule,
-        AccordionModule,
-        DialogModule,
-        ReactiveFormsModule,
-        ConfirmDialogModule,
-        SplitButtonModule,
-        MenuModule
-    ]
+    PositionInsertPopupComponent, UnitListComponent, MapComponent],
+  imports: [
+    CommonModule,
+    NavBarModule,
+    RouterModule,
+    ButtonModule,
+    AccordionModule,
+    DialogModule,
+    ReactiveFormsModule,
+    ConfirmDialogModule,
+    SplitButtonModule,
+    MenuModule,
+    HslayersModule,
+    ProgressBarModule,
+    TabViewModule
+  ]
 })
 export class DashboardModule { }

+ 3 - 0
src/app/sensor/components/sensor.component.html

@@ -1,6 +1,9 @@
 <app-nav-bar></app-nav-bar>
 
 <div class="container graph">
+
+  <button pButton type="button" label="Back to Dashboard" class="p-button-primary" icon="pi pi-backward" [routerLink]="['/dashboard']" style="margin-bottom: 15px;"></button>
+
   <div class="row graph-information">
     <div class="graph-desc">
       <div>

+ 6 - 11
src/app/shared/nav-bar/components/data-download/data-download-popup.component.ts

@@ -107,18 +107,13 @@ export class DataDownloadPopupComponent implements OnInit {
     let evt = document.createEvent('MouseEvents');
     let link = document.createElement('a');
 
-    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
-      window.navigator.msSaveOrOpenBlob(file, filename);
-    }
-    else {
-      var e = document.createEvent('MouseEvents'),
+    var e = document.createEvent('MouseEvents'),
         a = document.createElement('a');
 
-      a.download = filename;
-      a.href = window.URL.createObjectURL(file);
-      a.dataset.downloadurl = ['text/plain', a.download, a.href].join(':');
-      e.initEvent('click', true, false);
-      a.dispatchEvent(e);
-    }
+    a.download = filename;
+    a.href = window.URL.createObjectURL(file);
+    a.dataset.downloadurl = ['text/plain', a.download, a.href].join(':');
+    e.initEvent('click', true, false);
+    a.dispatchEvent(e);
   }
 }

+ 3 - 0
src/app/unit/components/unit.component.html

@@ -1,6 +1,9 @@
 <app-nav-bar></app-nav-bar>
 
 <div class="container graph">
+
+  <button pButton type="button" label="Back to Dashboard" class="p-button-primary" icon="pi pi-backward" [routerLink]="['/dashboard']" style="margin-bottom: 15px;"></button>
+
   <div class="graph-information">
     <div class="graph-desc">
       <div>

+ 8 - 6
src/app/unit/unit.module.ts

@@ -6,18 +6,20 @@ import {CalendarModule} from 'primeng/calendar';
 import {FormsModule} from '@angular/forms';
 import {ListboxModule} from 'primeng/listbox';
 import {CheckboxModule} from 'primeng/checkbox';
+import { RouterModule } from '@angular/router';
 
 
 
 @NgModule({
   declarations: [UnitComponent],
     imports: [
-        CommonModule,
-        NavBarModule,
-        CalendarModule,
-        FormsModule,
-        ListboxModule,
-        CheckboxModule
+      CommonModule,
+      NavBarModule,
+      RouterModule,
+      CalendarModule,
+      FormsModule,
+      ListboxModule,
+      CheckboxModule
     ]
 })
 export class UnitModule { }

+ 38 - 7
src/assets/scss/_dashboard.scss

@@ -14,6 +14,7 @@
   }
 }
 
+
 .dashboard {
   padding: $grid-gutter-width 0;
 
@@ -50,10 +51,11 @@
   .p-accordion {
     .p-accordion-header {
       .p-accordion-header-link {
-        padding-top: $grid-gutter-width / 2;
-        padding-bottom: $grid-gutter-width / 2;
+        padding-top: math.div($grid-gutter-width, 2);
+        padding-bottom: math.div($grid-gutter-width, 2);
         color: #0B1226;
         background: #F2F2F2;
+        border: none;
       }
 
       &:not(.p-highlight):not(.p-disabled):hover .p-accordion-header-link {
@@ -61,15 +63,15 @@
         background: #BCBCBC;
       }
 
-      &:not(.p-disabled).p-highlight  {
+      &:not(.p-disabled).p-highlight {
         .p-accordion-header-link {
           color: #0B1226;
           background: #BCBCBC;
         }
       }
 
-      .p-splitbutton-defaultbutton{
-        margin-left: $grid-gutter-width  / 2;
+      .p-splitbutton-defaultbutton {
+        margin-left: math.div($grid-gutter-width, 2);
       }
     }
 
@@ -77,6 +79,7 @@
       padding: 0;
       color: #0B1226;
       background: #F2F2F2;
+      border: none;
 
       .ng-star-inserted {
         &:nth-child(2n) .row {
@@ -85,7 +88,7 @@
 
         .row {
           margin: 0;
-          padding: 5px 0;
+          padding: 5px 0 5px 50px;
 
           @include hover-focus() {
             cursor: pointer;
@@ -101,6 +104,34 @@
       }
     }
   }
+
+  .p-tabview {
+    .p-tabview-title {
+      font-size: 16pt;
+    }
+
+    .p-tabview-nav li .p-tabview-nav-link {
+      border-radius: 0;
+      border: none;
+    }
+
+    .p-tabview-nav-link {
+      background: #FFFFFF !important;
+      color: #0B1226 !important;
+    }
+
+    .p-highlight {
+      .p-tabview-nav-link {
+        color: #0B1226 !important;
+        background: #BCBCBC !important;
+      }
+    }
+
+    .p-tabview-panels {
+      background: #FFFFFF !important;
+      padding: 0;
+    }
+  }
 }
 
 .dashboard-sensor-heading-wrapper {
@@ -121,7 +152,7 @@
   display: inline-flex;
   align-items: center;
   vertical-align: bottom;
-  margin: 0 $grid-gutter-width / 2;
+  margin: 0 math.div($grid-gutter-width, 2);
   width: 2px;
   height: 100%;
   background: $secondary;

+ 2 - 2
src/assets/scss/_graph.scss

@@ -145,8 +145,8 @@
 }
 
 .graph-view-wrapper {
-  padding-left: $grid-gutter-width / 2;
-  padding-right: $grid-gutter-width / 2;
+  padding-left: math.div($grid-gutter-width, 2);
+  padding-right: math.div($grid-gutter-width, 2);
 
   @include media-breakpoint-down(lg) {
     overflow-x: scroll;

+ 1 - 1
src/assets/scss/_login.scss

@@ -85,7 +85,7 @@
 
   p {
     margin-bottom: 0;
-    padding: ($grid-gutter-width / 2) 0;
+    padding: math.div($grid-gutter-width, 2) 0;
   }
 
   a {

+ 4 - 2
src/assets/scss/_navbar.scss

@@ -99,7 +99,7 @@
 .navbar-toggler {
   position: absolute;
   top: 50%;
-  right: $grid-gutter-width / 2;
+  right: math.div($grid-gutter-width, 2);
   width: 60px;
   height: 45px;
   z-index: 1;
@@ -120,9 +120,11 @@
     &:nth-child(1) {
       top: 0;
     }
+
     &:nth-child(2) {
       top: 18px;
     }
+
     &:nth-child(3) {
       top: 36px;
     }
@@ -154,7 +156,7 @@
 
   @include media-breakpoint-down(md) {
     position: absolute;
-    left: -($grid-gutter-width / 2);
+    left: -(math.div($grid-gutter-width, 2));
     top: 90px;
     width: calc(100% + 20px);
     background: $primary;

+ 2 - 1
src/assets/scss/_popup-form.scss

@@ -3,6 +3,7 @@
     .p-dialog .p-dialog-footer .p-button {
       @extend .p-button-primary;
       @extend .dark;
+
       @include media-breakpoint-down(sm) {
         margin: 5px 10px;
       }
@@ -10,7 +11,7 @@
   }
 
   h3 {
-    margin-top: $grid-gutter-width / 2;
+    margin-top: math.div($grid-gutter-width, 2);
     text-align: center;
     font-size: 24px;
   }

+ 2 - 2
src/assets/scss/_themes.scss

@@ -19,8 +19,8 @@ html,body{
     }
 
     @include media-breakpoint-down(xs) {
-      padding-right: $grid-gutter-width / 2;
-      padding-left: $grid-gutter-width / 2;
+      padding-right: math.div($grid-gutter-width, 2);
+      padding-left: math.div($grid-gutter-width, 2);
     }
   }
 

+ 102 - 103
src/assets/scss/_variables.scss

@@ -1,10 +1,9 @@
-// Variables
-//
-// Variables should follow the `$component-state-property-size` formula for
-// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.
+/* Variables */
 
-// Color system
+/* Variables should follow the `$component-state-property-size` formula for
+   consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. */
 
+/* Color system */
 $white:    #fff !default;
 $gray-100: #f8f9fa !default;
 $gray-200: #e9ecef !default;
@@ -89,17 +88,17 @@ $theme-colors: map-merge(
     $theme-colors
 );
 
-// Set a specific jump point for requesting color jumps
+/* Set a specific jump point for requesting color jumps */
 $theme-color-interval:      8% !default;
 
-// The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255.
+/* The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. */
 $yiq-contrasted-threshold:  150 !default;
 
-// Customize the light and dark text colors for use in our YIQ color contrast function.
+/* Customize the light and dark text colors for use in our YIQ color contrast function. */
 $yiq-text-dark:             $gray-900 !default;
 $yiq-text-light:            $white !default;
 
-// Characters which are escaped by the escape-svg function
+/* Characters which are escaped by the escape-svg function */
 $escaped-characters: (
     ("<", "%3c"),
     (">", "%3e"),
@@ -109,9 +108,9 @@ $escaped-characters: (
 ) !default;
 
 
-// Options
-//
-// Quickly modify global styling by enabling or disabling optional features.
+/* Options */
+
+/* Quickly modify global styling by enabling or disabling optional features. */
 
 $enable-caret:                                true !default;
 $enable-rounded:                              true !default;
@@ -128,11 +127,11 @@ $enable-validation-icons:                     true !default;
 $enable-deprecation-messages:                 true !default;
 
 
-// Spacing
-//
-// Control the default styling of most Bootstrap elements by modifying these
-// variables. Mostly focused on spacing.
-// You can add more entries to the $spacers map, should you need more variation.
+/* Spacing */
+
+/* Control the default styling of most Bootstrap elements by modifying these
+   variables. Mostly focused on spacing.
+   You can add more entries to the $spacers map, should you need more variation. */
 
 $spacer: 1rem !default;
 $spacers: () !default;
@@ -148,7 +147,7 @@ $spacers: map-merge(
     $spacers
 );
 
-// This variable affects the `.h-*` and `.w-*` classes.
+/* This variable affects the `.h-*` and `.w-*` classes. */
 $sizes: () !default;
 $sizes: map-merge(
     (
@@ -162,36 +161,36 @@ $sizes: map-merge(
 );
 
 
-// Body
-//
-// Settings for the `<body>` element.
+/* Body */
+
+/* Settings for the `<body>` element. */
 
 $body-bg:                   $white !default;
 $body-color:                $gray-900 !default;
 
 
-// Links
-//
-// Style anchor elements.
+/* Links */
+
+/* Style anchor elements. */
 
 $link-color:                              theme-color("primary") !default;
 $link-decoration:                         none !default;
 $link-hover-color:                        darken($link-color, 15%) !default;
 $link-hover-decoration:                   none !default;
-// Darken percentage for links with `.text-*` class (e.g. `.text-success`)
+/* Darken percentage for links with `.text-*` class (e.g. `.text-success`)*/
 $emphasized-link-hover-darken-percentage: 15% !default;
 
-// Paragraphs
-//
-// Style p element.
+/* Paragraphs */
+
+/* Style p element. */
 
 $paragraph-margin-bottom:   1rem !default;
 
 
-// Grid breakpoints
-//
-// Define the minimum dimensions at which your layout will change,
-// adapting to different screen sizes, for use in media queries.
+/* Grid breakpoints */
+
+/* Define the minimum dimensions at which your layout will change,
+   adapting to different screen sizes, for use in media queries. */
 
 $grid-breakpoints: (
   xs: 0,
@@ -205,9 +204,9 @@ $grid-breakpoints: (
 @include _assert-starts-at-zero($grid-breakpoints, "$grid-breakpoints");
 
 
-// Grid containers
-//
-// Define the maximum width of `.container` for different screen sizes.
+/* Grid containers */
+
+/* Define the maximum width of `.container` for different screen sizes. */
 
 $container-max-widths: (
   sm: 540px,
@@ -219,18 +218,18 @@ $container-max-widths: (
 @include _assert-ascending($container-max-widths, "$container-max-widths");
 
 
-// Grid columns
-//
-// Set the number of columns and specify the width of the gutters.
+/* Grid columns */
+
+/* Set the number of columns and specify the width of the gutters. */
 
 $grid-columns:                12 !default;
 $grid-gutter-width:           20px !default;
 $grid-row-columns:            6 !default;
 
 
-// Components
-//
-// Define common padding and border radius sizes and more.
+/* Components */
+
+/* Define common padding and border radius sizes and more. */
 
 $line-height-lg:              1.5 !default;
 $line-height-sm:              1.5 !default;
@@ -270,15 +269,15 @@ $embed-responsive-aspect-ratios: join(
     $embed-responsive-aspect-ratios
 );
 
-// Typography
-//
-// Font, line-height, and color for body text, headings, and more.
+/* Typography */
+
+/* Font, line-height, and color for body text, headings, and more. */
 
-// stylelint-disable value-keyword-case
+/* stylelint-disable value-keyword-case */
 $font-family-sans-serif:      'Roboto', sans-serif !default;
 $font-family-monospace:       SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
 $font-family-base:            $font-family-sans-serif !default;
-// stylelint-enable value-keyword-case
+/* stylelint-enable value-keyword-case */
 
 $font-size-base:              1rem !default; // Assumes the browser default, typically `16px`
 $font-size-lg:                $font-size-base * 1.25 !default;
@@ -300,7 +299,7 @@ $h4-font-size:                16px !default;
 $h5-font-size:                $font-size-base * 1.25 !default;
 $h6-font-size:                $font-size-base !default;
 
-$headings-margin-bottom:      $spacer / 2 !default;
+$headings-margin-bottom:      math.div($spacer, 2) !default;
 $headings-font-family:        null !default;
 $headings-font-weight:        500 !default;
 $headings-line-height:        1.2 !default;
@@ -345,9 +344,9 @@ $mark-bg:                     #fcf8e3 !default;
 $hr-margin-y:                 $spacer !default;
 
 
-// Tables
-//
-// Customizes the `.table` component with basic values, each used across all table variations.
+/* Tables */
+
+/* Customizes the `.table` component with basic values, each used across all table variations. */
 
 $table-cell-padding:          .75rem !default;
 $table-cell-padding-sm:       .3rem !default;
@@ -381,9 +380,9 @@ $table-bg-level:              -9 !default;
 $table-border-level:          -6 !default;
 
 
-// Buttons + Forms
-//
-// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.
+/* Buttons + Forms */
+
+/* Shared variables that are reassigned to `$input-` and `$btn-` specific variables. */
 
 $input-btn-padding-y:         .375rem !default;
 $input-btn-padding-x:         .75rem !default;
@@ -408,9 +407,9 @@ $input-btn-line-height-lg:    $line-height-lg !default;
 $input-btn-border-width:      $border-width !default;
 
 
-// Buttons
-//
-// For each of Bootstrap's buttons, define text, background, and border color.
+/* Buttons */
+
+/* For each of Bootstrap's buttons, define text, background, and border color. */
 
 $btn-padding-y:               $input-btn-padding-y !default;
 $btn-padding-x:               $input-btn-padding-x !default;
@@ -442,7 +441,7 @@ $btn-link-disabled-color:     $gray-600 !default;
 
 $btn-block-spacing-y:         .5rem !default;
 
-// Allows for customizing button radius independently from global border radius
+/* Allows for customizing button radius independently from global border radius */
 $btn-border-radius:           $border-radius !default;
 $btn-border-radius-lg:        $border-radius-lg !default;
 $btn-border-radius-sm:        $border-radius-sm !default;
@@ -450,7 +449,7 @@ $btn-border-radius-sm:        $border-radius-sm !default;
 $btn-transition:              color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
 
 
-// Forms
+/* Forms */
 
 $label-margin-bottom:                   .5rem !default;
 
@@ -496,7 +495,7 @@ $input-height-border:                   $input-border-width * 2 !default;
 
 $input-height-inner:                    add($input-line-height * 1em, $input-padding-y * 2) !default;
 $input-height-inner-half:               add($input-line-height * .5em, $input-padding-y) !default;
-$input-height-inner-quarter:            add($input-line-height * .25em, $input-padding-y / 2) !default;
+$input-height-inner-quarter:            add($input-line-height * .25em, math.div($input-padding-y, 2)) !default;
 
 $input-height:                          add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;
 $input-height-sm:                       add($input-line-height-sm * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;
@@ -566,7 +565,7 @@ $custom-radio-indicator-border-radius:          50% !default;
 $custom-radio-indicator-icon-checked:           url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'><circle r='3' fill='#{$custom-control-indicator-checked-color}'/></svg>") !default;
 
 $custom-switch-width:                           $custom-control-indicator-size * 1.75 !default;
-$custom-switch-indicator-border-radius:         $custom-control-indicator-size / 2 !default;
+$custom-switch-indicator-border-radius:         math.div($custom-control-indicator-size, 2) !default;
 $custom-switch-indicator-size:                  subtract($custom-control-indicator-size, $custom-control-indicator-border-width * 4) !default;
 
 $custom-select-padding-y:           $input-padding-y !default;
@@ -651,7 +650,7 @@ $custom-file-text: (
 ) !default;
 
 
-// Form validation
+/* Form validation */
 
 $form-feedback-margin-top:          $form-text-margin-top !default;
 $form-feedback-font-size:           $small-font-size !default;
@@ -678,10 +677,10 @@ $form-validation-states: map-merge(
     $form-validation-states
 );
 
-// Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
+/* Z-index master list */
+
+/* Warning: Avoid customizing these values. They're used for a bird's eye view
+   of components dependent on the z-axis and are designed to all work together. */
 
 $zindex-dropdown:                   1000 !default;
 $zindex-sticky:                     1020 !default;
@@ -692,7 +691,7 @@ $zindex-popover:                    1060 !default;
 $zindex-tooltip:                    1070 !default;
 
 
-// Navs
+/* Navs */
 
 $nav-link-padding-y:                .5rem !default;
 $nav-link-padding-x:                1rem !default;
@@ -711,21 +710,21 @@ $nav-pills-link-active-color:       $component-active-color !default;
 $nav-pills-link-active-bg:          $component-active-bg !default;
 
 $nav-divider-color:                 $gray-200 !default;
-$nav-divider-margin-y:              $spacer / 2 !default;
+$nav-divider-margin-y:              math.div($spacer, 2) !default;
 
 
-// Navbar
+/* Navbar */
 
-$navbar-padding-y:                  $spacer / 2 !default;
+$navbar-padding-y:                  math.div($spacer, 2) !default;
 $navbar-padding-x:                  $spacer !default;
 
 $navbar-nav-link-padding-x:         .5rem !default;
 
 $navbar-brand-font-size:            $font-size-lg !default;
-// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link
+/* Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link */
 $nav-link-height:                   $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;
 $navbar-brand-height:               $navbar-brand-font-size * $line-height-base !default;
-$navbar-brand-padding-y:            ($nav-link-height - $navbar-brand-height) / 2 !default;
+$navbar-brand-padding-y:            math.div(($nav-link-height - $navbar-brand-height), 2) !default;
 
 $navbar-toggler-padding-y:          .25rem !default;
 $navbar-toggler-padding-x:          .75rem !default;
@@ -754,9 +753,9 @@ $navbar-dark-brand-color:                 $navbar-dark-active-color !default;
 $navbar-dark-brand-hover-color:           $navbar-dark-active-color !default;
 
 
-// Dropdowns
-//
-// Dropdown menu container and contents.
+/* Dropdowns */
+
+/* Dropdown menu container and contents. */
 
 $dropdown-min-width:                10rem !default;
 $dropdown-padding-x:                0 !default;
@@ -789,7 +788,7 @@ $dropdown-header-color:             $gray-600 !default;
 $dropdown-header-padding:           $dropdown-padding-y $dropdown-item-padding-x !default;
 
 
-// Pagination
+/* Pagination */
 
 $pagination-padding-y:              .5rem !default;
 $pagination-padding-x:              .75rem !default;
@@ -822,14 +821,14 @@ $pagination-disabled-border-color:  $gray-300 !default;
 $pagination-border-radius-sm:       $border-radius-sm !default;
 $pagination-border-radius-lg:       $border-radius-lg !default;
 
-// Jumbotron
+/* Jumbotron */
 
 $jumbotron-padding:                 2rem !default;
 $jumbotron-color:                   null !default;
 $jumbotron-bg:                      $gray-200 !default;
 
 
-// Cards
+/* Cards */
 
 $card-spacer-y:                     .75rem !default;
 $card-spacer-x:                     1.25rem !default;
@@ -841,11 +840,11 @@ $card-cap-bg:                       rgba($black, .03) !default;
 $card-cap-color:                    null !default;
 $card-height:                       null !default;
 $card-color:                        null !default;
-$card-bg:                           $white !default;
+$card-bg:                           $dark !default;
 
 $card-img-overlay-padding:          1.25rem !default;
 
-$card-group-margin:                 $grid-gutter-width / 2 !default;
+$card-group-margin:                 math.div($grid-gutter-width, 2) !default;
 $card-deck-margin:                  $card-group-margin !default;
 
 $card-columns-count:                3 !default;
@@ -853,7 +852,7 @@ $card-columns-gap:                  1.25rem !default;
 $card-columns-margin:               $card-spacer-y !default;
 
 
-// Tooltips
+/* Tooltips */
 
 $tooltip-font-size:                 $font-size-sm !default;
 $tooltip-max-width:                 200px !default;
@@ -869,7 +868,7 @@ $tooltip-arrow-width:               .8rem !default;
 $tooltip-arrow-height:              .4rem !default;
 $tooltip-arrow-color:               $tooltip-bg !default;
 
-// Form tooltips must come after regular tooltips
+/* Form tooltips must come after regular tooltips */
 $form-feedback-tooltip-padding-y:     $tooltip-padding-y !default;
 $form-feedback-tooltip-padding-x:     $tooltip-padding-x !default;
 $form-feedback-tooltip-font-size:     $tooltip-font-size !default;
@@ -878,7 +877,7 @@ $form-feedback-tooltip-opacity:       $tooltip-opacity !default;
 $form-feedback-tooltip-border-radius: $tooltip-border-radius !default;
 
 
-// Popovers
+/* Popovers */
 
 $popover-font-size:                 $font-size-sm !default;
 $popover-bg:                        $white !default;
@@ -905,7 +904,7 @@ $popover-arrow-color:               $popover-bg !default;
 $popover-arrow-outer-color:         fade-in($popover-border-color, .05) !default;
 
 
-// Toasts
+/* Toasts */
 
 $toast-max-width:                   350px !default;
 $toast-padding-x:                   .75rem !default;
@@ -923,7 +922,7 @@ $toast-header-background-color:     rgba($white, .85) !default;
 $toast-header-border-color:         rgba(0, 0, 0, .05) !default;
 
 
-// Badges
+/* Badges */
 
 $badge-font-size:                   75% !default;
 $badge-font-weight:                 $font-weight-bold !default;
@@ -935,17 +934,17 @@ $badge-transition:                  $btn-transition !default;
 $badge-focus-width:                 $input-btn-focus-width !default;
 
 $badge-pill-padding-x:              .6em !default;
-// Use a higher than normal value to ensure completely rounded edges when
-// customizing padding or font-size on labels.
+/* Use a higher than normal value to ensure completely rounded edges when
+   customizing padding or font-size on labels. */
 $badge-pill-border-radius:          10rem !default;
 
 
-// Modals
+/* Modals */
 
-// Padding applied to the modal body
+/* Padding applied to the modal body */
 $modal-inner-padding:               1rem !default;
 
-// Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding
+/* Margin between elements in footer, must be lower than or equal to 2 * $modal-inner-padding */
 $modal-footer-margin-between:       .5rem !default;
 
 $modal-dialog-margin:               .5rem !default;
@@ -983,9 +982,9 @@ $modal-transition:                  transform .3s ease-out !default;
 $modal-scale-transform:             scale(1.02) !default;
 
 
-// Alerts
-//
-// Define alert colors, border radius, and padding.
+/* Alerts */
+
+/* Define alert colors, border radius, and padding. */
 
 $alert-padding-y:                   .75rem !default;
 $alert-padding-x:                   1.25rem !default;
@@ -999,7 +998,7 @@ $alert-border-level:                -9 !default;
 $alert-color-level:                 6 !default;
 
 
-// Progress bars
+/* Progress bars */
 
 $progress-height:                   1rem !default;
 $progress-font-size:                $font-size-base * .75 !default;
@@ -1012,7 +1011,7 @@ $progress-bar-animation-timing:     1s linear infinite !default;
 $progress-bar-transition:           width .6s ease !default;
 
 
-// List group
+/* List group */
 
 $list-group-color:                  null !default;
 $list-group-bg:                     $white !default;
@@ -1038,7 +1037,7 @@ $list-group-action-active-color:    $body-color !default;
 $list-group-action-active-bg:       $gray-200 !default;
 
 
-// Image thumbnails
+/* Image thumbnails */
 
 $thumbnail-padding:                 .25rem !default;
 $thumbnail-bg:                      $body-bg !default;
@@ -1048,13 +1047,13 @@ $thumbnail-border-radius:           $border-radius !default;
 $thumbnail-box-shadow:              0 1px 2px rgba($black, .075) !default;
 
 
-// Figures
+/* Figures */
 
 $figure-caption-font-size:          90% !default;
 $figure-caption-color:              $gray-600 !default;
 
 
-// Breadcrumbs
+/* Breadcrumbs */
 
 $breadcrumb-font-size:              null !default;
 
@@ -1072,7 +1071,7 @@ $breadcrumb-divider:                quote("/") !default;
 $breadcrumb-border-radius:          $border-radius !default;
 
 
-// Carousel
+/* Carousel */
 
 $carousel-control-color:             $white !default;
 $carousel-control-width:             15% !default;
@@ -1099,7 +1098,7 @@ $carousel-transition-duration:       .6s !default;
 $carousel-transition:                transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)
 
 
-// Spinners
+/* Spinners */
 
 $spinner-width:         2rem !default;
 $spinner-height:        $spinner-width !default;
@@ -1110,7 +1109,7 @@ $spinner-height-sm:       $spinner-width-sm !default;
 $spinner-border-width-sm: .2em !default;
 
 
-// Close
+/* Close */
 
 $close-font-size:                   $font-size-base * 1.5 !default;
 $close-font-weight:                 $font-weight-bold !default;
@@ -1118,7 +1117,7 @@ $close-color:                       $black !default;
 $close-text-shadow:                 0 1px 0 $white !default;
 
 
-// Code
+/* Code */
 
 $code-font-size:                    87.5% !default;
 $code-color:                        $pink !default;
@@ -1133,7 +1132,7 @@ $pre-color:                         $gray-900 !default;
 $pre-scrollable-max-height:         340px !default;
 
 
-// Utilities
+/* Utilities */
 
 $displays: none, inline, inline-block, block, table, table-row, table-cell, flex, inline-flex !default;
 $overflows: auto, hidden !default;
@@ -1141,7 +1140,7 @@ $positions: static, relative, absolute, fixed, sticky !default;
 $user-selects: all, auto, none !default;
 
 
-// Printing
+/* Printing */
 
 $print-page-size:                   a3 !default;
 $print-body-min-width:              map-get($grid-breakpoints, "lg") !default;

+ 3 - 1
src/assets/scss/styles.scss

@@ -1,3 +1,5 @@
+@use 'sass:math';
+
 @import "node_modules/bootstrap/scss/functions";
 @import "variables";
 @import "node_modules/bootstrap/scss/mixins";
@@ -32,7 +34,7 @@
 @import "node_modules/bootstrap/scss/tooltip";
 @import "node_modules/bootstrap/scss/popover";
 @import "node_modules/bootstrap/scss/carousel";
-@import "node_modules/bootstrap/scss/spinners";
+/*@import "node_modules/bootstrap/scss/spinners";*/
 @import "node_modules/bootstrap/scss/utilities";
 @import "node_modules/bootstrap/scss/print";
 

+ 2 - 1
src/environments/environment.prod.ts

@@ -2,5 +2,6 @@ export const environment = {
   production: true,
   baseHref: '/',
   useMock: false,
-  sensLogBaseUrl: 'https://sensor.lesprojekt.cz'
+  sensLogBaseUrl: 'https://sensor.lesprojekt.cz',
+  hslayersAssetsPath: 'https://unpkg.com/hslayers-ng-app@6.1.0/assets'
 };

+ 2 - 1
src/environments/environment.ts

@@ -1,4 +1,5 @@
 export const environment = {
   production: false,
-  sensLogBaseUrl: ''
+  sensLogBaseUrl: '',
+  hslayersAssetsPath: './hslayers-ng/assets'
 };

+ 6 - 0
tsconfig.json

@@ -10,8 +10,14 @@
     "experimentalDecorators": true,
     "moduleResolution": "node",
     "importHelpers": true,
+    "skipLibCheck": true,
     "target": "es2015",
     "module": "es2020",
+    "paths": {
+      "@angular/*": [
+        "./node_modules/@angular/*"
+      ]
+    },
     "lib": [
       "es2018",
       "dom"

Деякі файли не було показано, через те що забагато файлів було змінено