🚀 Major refactor: Reorganize project structure
- Move WebAPI/ → src/Backend/DiunaBI.WebAPI/ - Move Frontend/ → src/Frontend/ - Move Deployment/ → deploy/ - Add proper .NET 8 solution structure - Add plugin architecture with DiunaBI.Plugins.Morska - Clean
16
src/Frontend/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
46
src/Frontend/.eslintrc.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "diunabi",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "diunabi",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
42
src/Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
0
src/Frontend/README.md
Normal file
132
src/Frontend/angular.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"DiunaBI": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "ipms",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/diunaBI"
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"moment"
|
||||
],
|
||||
"serviceWorker": "ngsw-config.json",
|
||||
"browser": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "3mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "DiunaBI:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "DiunaBI:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "DiunaBI:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "9583f9e0-88e2-44e8-bc34-c8c6a9fddedd",
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
}
|
||||
}
|
||||
44
src/Frontend/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/diunaBI'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
30
src/Frontend/ngsw-config.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "diunabi",
|
||||
"installMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/favicon.ico",
|
||||
"/index.html",
|
||||
"/manifest.webmanifest",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/**",
|
||||
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
57
src/Frontend/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "diuna-bi",
|
||||
"version": "0.0.5",
|
||||
"azureBuild": "#{buildId}#",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.0.3",
|
||||
"@angular/cdk": "^18.0.3",
|
||||
"@angular/common": "^18.0.3",
|
||||
"@angular/compiler": "^18.0.3",
|
||||
"@angular/core": "^18.0.3",
|
||||
"@angular/forms": "^18.0.3",
|
||||
"@angular/material": "^18.0.3",
|
||||
"@angular/material-moment-adapter": "^18.0.3",
|
||||
"@angular/platform-browser": "^18.0.3",
|
||||
"@angular/platform-browser-dynamic": "^18.0.3",
|
||||
"@angular/pwa": "18.0.4",
|
||||
"@angular/router": "^18.0.3",
|
||||
"@angular/service-worker": "^18.0.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"rxjs": "^7.6.0",
|
||||
"uuid": "10.0.0",
|
||||
"zone.js": "0.14.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.0.4",
|
||||
"@angular-eslint/builder": "18.0.1",
|
||||
"@angular-eslint/eslint-plugin": "18.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "18.0.1",
|
||||
"@angular-eslint/schematics": "18.0.1",
|
||||
"@angular-eslint/template-parser": "18.0.1",
|
||||
"@angular/cli": "~18.0.4",
|
||||
"@angular/compiler-cli": "^18.0.3",
|
||||
"@types/jasmine": "5.1.4",
|
||||
"@types/uuid": "9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"eslint": "^9.5.0",
|
||||
"jasmine-core": "5.1.2",
|
||||
"karma": "~6.4.3",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"tslib": "2.6.3",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
8
src/Frontend/src/app/app.component.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- main app container -->
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
0
src/Frontend/src/app/app.component.scss
Normal file
48
src/Frontend/src/app/app.component.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { SwUpdate, VersionEvent } from '@angular/service-worker';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { NotificationsService } from './services/notifications.service';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RouterOutlet]
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(
|
||||
private readonly _swUpdate: SwUpdate,
|
||||
private _notifications: NotificationsService
|
||||
) {
|
||||
console.log('AppComponent');
|
||||
this.subscribeUpdates();
|
||||
}
|
||||
|
||||
subscribeUpdates() {
|
||||
if (this._swUpdate.isEnabled && environment.production) {
|
||||
this._swUpdate.versionUpdates.subscribe((evt: VersionEvent) => {
|
||||
switch (evt.type) {
|
||||
case 'VERSION_READY': {
|
||||
this._notifications.add({
|
||||
text: "New version available. DiunaBI will restart in 10 seconds.",
|
||||
duration: 10000,
|
||||
dismiss: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
btn: 'Cancel',
|
||||
action: () => {
|
||||
this._notifications.add({
|
||||
text: "Restart canceled.",
|
||||
btn: "Hide"
|
||||
});
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Frontend/src/app/app.routes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { AuthGuard } from './auth/auth.guard';
|
||||
import { LoginViewComponent } from './views/login/login-view.component';
|
||||
import { MainViewComponent } from './views/main/main-view.component';
|
||||
|
||||
export const APP_ROUTES: Route[] = [
|
||||
{
|
||||
path: '',
|
||||
component: LoginViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'app',
|
||||
component: MainViewComponent,
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./modules/dashboard/dashboard.routes').then(r => r.DASHBOARD_ROUTES)
|
||||
},
|
||||
{
|
||||
path: 'layers',
|
||||
loadChildren: () => import('./modules/layers/layers.routes').then(r=> r.LAYERS_ROUTES)
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
39
src/Frontend/src/app/auth/auth.guard.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, UrlTree } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard {
|
||||
loginUrl: string | null = null;
|
||||
constructor(
|
||||
private auth$: AuthService,
|
||||
private router$: Router,
|
||||
) {}
|
||||
canActivate(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
if (this.auth$.isAuthenticated()) {
|
||||
if (this.loginUrl) {
|
||||
this.router$.navigate([this.loginUrl]);
|
||||
this.loginUrl = null;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return this.tryWaitForAuthData();
|
||||
}
|
||||
}
|
||||
async tryWaitForAuthData(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (this.auth$.isAuthenticated()) {
|
||||
resolve(true);
|
||||
} else {
|
||||
this.loginUrl = window.location.pathname;
|
||||
this.router$.navigate(['']);
|
||||
resolve(false);
|
||||
}
|
||||
}, 100);
|
||||
})
|
||||
}
|
||||
}
|
||||
158
src/Frontend/src/app/auth/auth.service.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { User } from '../models/user.model';
|
||||
import { BehaviorSubject, from, Observable } from 'rxjs';
|
||||
import {jwtDecode} from "jwt-decode";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
|
||||
webCredentials?: string;
|
||||
apiToken?: string;
|
||||
user?: User;
|
||||
apiTokenExpirationTime?: Moment;
|
||||
|
||||
bc!: BroadcastChannel;
|
||||
|
||||
constructor(
|
||||
private http$: HttpClient,
|
||||
) {
|
||||
this.createBroadcastChannel();
|
||||
this.askForAuthData();
|
||||
}
|
||||
|
||||
ping() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http$.get<string>(`${environment.api.url}/ping/ping`).subscribe({
|
||||
next: (data) => {
|
||||
resolve(data);
|
||||
},
|
||||
error: (e) => {
|
||||
console.error('Ping error', e);
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
retrieveUserFromCredentials(credentials: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const responsePayload: any = jwtDecode(credentials);
|
||||
this.user = new User({
|
||||
userName: `${responsePayload.given_name} ${responsePayload.family_name}`,
|
||||
email: responsePayload.email,
|
||||
avatar: responsePayload.picture
|
||||
});
|
||||
}
|
||||
getAPIToken(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = new HttpHeaders().set('Content-type', 'application/json');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.http$.post<any>(`${environment.api.url}/auth/apiToken`, JSON.stringify(this.webCredentials), { headers: header }).subscribe({
|
||||
next: (data) => {
|
||||
this.user!.id = data.id;
|
||||
this.apiToken = data.token;
|
||||
this.apiTokenExpirationTime = moment(data.expirationTime);
|
||||
this.sendAuthData();
|
||||
resolve(data);
|
||||
},
|
||||
error: (e: HttpErrorResponse) => {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
getAPITokenObservable(): Observable<void> {
|
||||
return from(this.getAPIToken());
|
||||
}
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.apiToken &&
|
||||
!!this.webCredentials &&
|
||||
!!this.user &&
|
||||
this.apiTokenExpirationTime!.isAfter(moment());
|
||||
}
|
||||
logoutHandler() {
|
||||
this.logout();
|
||||
this.bc.postMessage({
|
||||
type: BroadcastType.LOGOUT
|
||||
});
|
||||
}
|
||||
logout() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
google.accounts.id.disableAutoSelect();
|
||||
this.apiToken = undefined;
|
||||
this.webCredentials = undefined;
|
||||
this.user = undefined;
|
||||
this.apiTokenExpirationTime = undefined;
|
||||
window.location.reload();
|
||||
}
|
||||
// broadcastChannel
|
||||
createBroadcastChannel() {
|
||||
this.bc = new BroadcastChannel('auth');
|
||||
this.bc.onmessage = (event: MessageEvent<AuthMessage>) => {
|
||||
this.broadcastListener(event);
|
||||
}
|
||||
}
|
||||
broadcastListener(event: MessageEvent<AuthMessage>) {
|
||||
console.log('BroadcastChannel message recieved', event.data.type);
|
||||
switch (event.data.type) {
|
||||
case BroadcastType.REQUEST_AUTH_DATA:
|
||||
this.sendAuthData();
|
||||
break;
|
||||
case BroadcastType.SEND_AUTH_DATA:
|
||||
this.recieveAuthData(event.data.data!);
|
||||
break;
|
||||
case BroadcastType.LOGOUT:
|
||||
this.logout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
askForAuthData() {
|
||||
this.bc.postMessage({
|
||||
type: BroadcastType.REQUEST_AUTH_DATA
|
||||
});
|
||||
}
|
||||
sendAuthData() {
|
||||
if (this.isAuthenticated()) {
|
||||
this.bc.postMessage({
|
||||
type: BroadcastType.SEND_AUTH_DATA,
|
||||
data: {
|
||||
webCredentials: this.webCredentials,
|
||||
apiToken: this.apiToken,
|
||||
apiTokenExpirationTime: this.apiTokenExpirationTime?.toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
recieveAuthData(data: AuthData) {
|
||||
this.webCredentials = data.webCredentials;
|
||||
this.apiToken = data.apiToken;
|
||||
this.apiTokenExpirationTime = moment(data.apiTokenExpirationTime);
|
||||
this.retrieveUserFromCredentials(this.webCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
enum BroadcastType {
|
||||
REQUEST_AUTH_DATA = 'request',
|
||||
SEND_AUTH_DATA = 'send',
|
||||
LOGOUT = 'logout'
|
||||
}
|
||||
interface AuthData {
|
||||
webCredentials: string,
|
||||
apiToken: string,
|
||||
apiTokenExpirationTime: string
|
||||
}
|
||||
interface AuthMessage {
|
||||
sender: string,
|
||||
type: BroadcastType,
|
||||
data?: AuthData
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<mat-nav-list>
|
||||
<a mat-list-item routerLink="/app/" routerLinkActive="active" [routerLinkActiveOptions]="{exact : true}">
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
Dashboard
|
||||
</a>
|
||||
<a mat-list-item routerLink="/app/layers" routerLinkActive="active" [routerLinkActiveOptions]="{exact : true}">
|
||||
<mat-icon>table</mat-icon>
|
||||
Layers
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-main-menu',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatSidenavModule, MatIconModule, MatListModule, RouterLink, RouterLinkActive],
|
||||
templateUrl: './main-menu.component.html',
|
||||
styleUrls: ['./main-menu.component.scss']
|
||||
})
|
||||
export class MainMenuComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<mat-card *ngFor="let msg of notifications$.messages">
|
||||
<mat-card-content>
|
||||
<span class="text">{{msg.text}}</span>
|
||||
<span class="btn" *ngIf="msg.btn">
|
||||
<a class="action-button" (click)="notifications$.doAction(msg)">{{msg.btn}}</a>
|
||||
</span>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,15 @@
|
||||
mat-card {
|
||||
margin-bottom: 3px;
|
||||
background-color: rgba(255, 145, 0, 0.4);
|
||||
}
|
||||
.action-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.text {
|
||||
float: left;
|
||||
}
|
||||
.btn {
|
||||
float: right;
|
||||
color: red;
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NotificationsComponent } from './notifications.component';
|
||||
|
||||
describe('NotificationsComponent', () => {
|
||||
let component: NotificationsComponent;
|
||||
let fixture: ComponentFixture<NotificationsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NotificationsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NotificationsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { NotificationsService } from 'src/app/services/notifications.service';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-notifications',
|
||||
templateUrl: './notifications.component.html',
|
||||
styleUrls: ['./notifications.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgFor,
|
||||
MatCardModule,
|
||||
NgIf,
|
||||
],
|
||||
})
|
||||
export class NotificationsComponent {
|
||||
constructor(
|
||||
public notifications$: NotificationsService
|
||||
) {}
|
||||
}
|
||||
43
src/Frontend/src/app/directives/scroll-end.directive.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
export enum SCROLLEND_DIRECTION {
|
||||
DOWN = 'down',
|
||||
UP = 'UP',
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[diunabiScrollEnd]',
|
||||
standalone: true,
|
||||
})
|
||||
export class ScrollEndDirective implements OnInit, OnDestroy {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Output() diunabiScrollEnd: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@Input() rootMargin = '0px 0px 0px 0px';
|
||||
@Input() desiredDirection: SCROLLEND_DIRECTION = SCROLLEND_DIRECTION.DOWN;
|
||||
|
||||
observer?: IntersectionObserver;
|
||||
previousEntry?: IntersectionObserverEntry;
|
||||
scrollDirection?: SCROLLEND_DIRECTION;
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
this.scrollDirection = this.previousEntry?.boundingClientRect.bottom ?? 0 > entry.boundingClientRect.bottom ? SCROLLEND_DIRECTION.DOWN : SCROLLEND_DIRECTION.UP;
|
||||
if (!this.previousEntry?.isIntersecting && entry.isIntersecting && this.scrollDirection === this.desiredDirection) {
|
||||
this.diunabiScrollEnd.emit();
|
||||
}
|
||||
this.previousEntry = entry;
|
||||
});
|
||||
}, {
|
||||
rootMargin: this.rootMargin,
|
||||
});
|
||||
this.observer.observe(this.el.nativeElement);
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
||||
45
src/Frontend/src/app/interceptors/auth.interceptor.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import moment from 'moment';
|
||||
import { catchError, mergeMap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../services/notifications.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
constructor(
|
||||
private auth$: AuthService,
|
||||
private notifications$: NotificationsService
|
||||
) { }
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
if (!request.url.includes('/auth/apiToken')) {
|
||||
if (this.auth$.apiTokenExpirationTime?.isBefore(moment.utc())) {
|
||||
return this.auth$.getAPITokenObservable().pipe(
|
||||
mergeMap(() => {
|
||||
return next.handle(request.clone({
|
||||
headers: request.headers.set('Authorization', `Bearer ${this.auth$.apiToken}`),
|
||||
}));
|
||||
}),
|
||||
catchError(() => {
|
||||
this.notifications$.add({
|
||||
text: "User session is expired and unable to restore. Please restart the app.",
|
||||
btn: "Restart",
|
||||
action: () => { window.location.reload(); },
|
||||
duration: 5000,
|
||||
});
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return next.handle(request.clone({
|
||||
headers: request.headers.set('Authorization', `Bearer ${this.auth$.apiToken}`),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
return next.handle(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
src/Frontend/src/app/interceptors/loader.interceptor.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
|
||||
import { finalize, Observable } from 'rxjs';
|
||||
import { DataService } from '../services/data.service';
|
||||
|
||||
@Injectable()
|
||||
export class LoaderInterceptor implements HttpInterceptor {
|
||||
|
||||
private tasks = 0;
|
||||
|
||||
constructor(
|
||||
private data$: DataService
|
||||
) { }
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
this.addTask();
|
||||
return next.handle(request).pipe(
|
||||
finalize(() => {
|
||||
this.removeTask();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addTask() {
|
||||
this.tasks++;
|
||||
if (this.tasks === 1) {
|
||||
this.data$.showLoader.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
removeTask() {
|
||||
this.tasks--;
|
||||
if (this.tasks === 0) {
|
||||
this.data$.showLoader.next(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Frontend/src/app/models/base.model.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Deserializable } from './deserializable.model';
|
||||
import { Serializable } from './serializable.model';
|
||||
|
||||
import { User } from './user.model';
|
||||
import moment, {Moment} from "moment";
|
||||
|
||||
export class Base implements Deserializable, Serializable {
|
||||
id?: string;
|
||||
createdAt?: Moment;
|
||||
modifiedAt?: Moment;
|
||||
createdById?: string;
|
||||
modifiedById?: string;
|
||||
createdBy?: User;
|
||||
modifiedBy?: User;
|
||||
|
||||
constructor(data: Partial<Base> = {}) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deserialize(input: any): this {
|
||||
if (input.createdAt) { input.createdAt = moment(input.createdAt).utc(true); }
|
||||
if (input.modifiedAt) { input.modifiedAt = moment(input.modifiedAt).utc(true); }
|
||||
if (input.createdBy) { input.createdBy = new User(input.createdBy); }
|
||||
if (input.modifiedBy) { input.modifiedBy = new User(input.modifiedBy); }
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serialize() : any {
|
||||
return Object.assign({}, this);
|
||||
}
|
||||
}
|
||||
4
src/Frontend/src/app/models/deserializable.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Deserializable {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deserialize(input: any): this;
|
||||
}
|
||||
146
src/Frontend/src/app/models/layer.model.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Base } from './base.model';
|
||||
import { UntypedFormBuilder, Validators, UntypedFormGroup } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { map } from 'rxjs';
|
||||
import { Record } from 'src/app/models/record.model';
|
||||
|
||||
export enum LayerType {
|
||||
Import,
|
||||
Processed,
|
||||
Administration,
|
||||
Dictionary
|
||||
}
|
||||
|
||||
export class Layer extends Base {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
number?: Number;
|
||||
source?: string;
|
||||
name?: string;
|
||||
records: Record[] = [];
|
||||
created?: string;
|
||||
modified?: string;
|
||||
type?: LayerType;
|
||||
isCancelled: boolean = false;
|
||||
|
||||
constructor(data: Partial<Layer> = {}) {
|
||||
super();
|
||||
Object.assign(this, data);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override deserialize(input: any): this {
|
||||
Object.assign(this, Object.assign(this, super.deserialize(input)));
|
||||
if (this.records) { this.records = this.records.map(x => new Record().deserialize(x)); }
|
||||
return this;
|
||||
}
|
||||
override serialize() {
|
||||
this.number = 0; // will be overrided in backend
|
||||
return Object.assign({}, this);
|
||||
}
|
||||
static getForm(fb: UntypedFormBuilder) {
|
||||
return fb.group({
|
||||
id: [null],
|
||||
name: ['', Validators.required],
|
||||
source: ['', Validators.required],
|
||||
sheetId: '',
|
||||
createdAt: '',
|
||||
modifiedAt: '',
|
||||
createdBy: '',
|
||||
modifiedBy: '',
|
||||
modified: '',
|
||||
created: ''
|
||||
});
|
||||
}
|
||||
fillForm(form: UntypedFormGroup) {
|
||||
form.patchValue(this);
|
||||
form.patchValue({
|
||||
createdBy: this.createdBy?.userName,
|
||||
modifiedBy: this.modifiedBy?.userName
|
||||
});
|
||||
}
|
||||
loadForm(form: UntypedFormGroup) {
|
||||
for (const field of Object.keys(form.controls)) {
|
||||
console.log(field);
|
||||
//this[field as keyof Layer] = form.controls[field].value;
|
||||
}
|
||||
this.createdBy = undefined;
|
||||
this.modifiedBy = undefined;
|
||||
}
|
||||
//API Actions
|
||||
static add(input: Layer, _http: HttpClient): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.post<string>(`${environment.api.url}/layers`, { ...input.serialize(), }).subscribe({
|
||||
next: (data) => resolve(data),
|
||||
error: (e) => reject(e)
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getList(_http: HttpClient, start: number, limit: number, name: string, type: LayerType | ''): any {
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.get<Layer[]>(`${environment.api.url}/layers?start=${start}&limit=${limit}&name=${name}&type=${type}`)
|
||||
.pipe(map(data => data.map(x => new Layer().deserialize(x))))
|
||||
.subscribe({
|
||||
next: (data) => resolve(data),
|
||||
error: (e) => reject(e)
|
||||
})
|
||||
});
|
||||
}
|
||||
static getById(id: string, _http: HttpClient): Promise<Layer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.get<Layer>(`${environment.api.url}/layers/${id}`).pipe(map(x => new Layer().deserialize(x))).subscribe({
|
||||
next: (data) => resolve(data),
|
||||
error: (e) => reject(e)
|
||||
})
|
||||
});
|
||||
}
|
||||
static parseFile(file: File, _http: HttpClient): Promise<Layer[]> {
|
||||
const formData = new FormData();
|
||||
formData.append(file.name, file);
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.post<Layer[]>(`${environment.api.url}/layers/parseFile`, formData,
|
||||
).pipe(map(data => data.map(x => new Layer().deserialize(x))))
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
resolve(data);
|
||||
},
|
||||
error: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
static parseGoogleSheet(sheetId: string, _http: HttpClient): Promise<Layer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.get<Layer>(`${environment.api.url}/layers/parseGoogleSheet/${sheetId}`,
|
||||
).pipe(map(data => new Layer().deserialize(data)))
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
resolve(data);
|
||||
},
|
||||
error: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
static exportToGoogleSheet(id: string, _http: HttpClient): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.get<boolean>(`${environment.api.url}/layers/exportToGoogleSheet/${id}`,
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
resolve(data);
|
||||
},
|
||||
error: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
static processLayer(id: string, _http: HttpClient): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
_http.get<boolean>(`${environment.api.url}/layers/processLayer/${id}`,
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
resolve(data);
|
||||
},
|
||||
error: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
49
src/Frontend/src/app/models/record.model.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Base } from './base.model';
|
||||
|
||||
export class Record extends Base {
|
||||
code?: string;
|
||||
value1?: number;
|
||||
value2?: number;
|
||||
value3?: number;
|
||||
value4?: number;
|
||||
value5?: number;
|
||||
value6?: number;
|
||||
value7?: number;
|
||||
value8?: number;
|
||||
value9?: number;
|
||||
value10?: number;
|
||||
value11?: number;
|
||||
value12?: number;
|
||||
value13?: number;
|
||||
value14?: number;
|
||||
value15?: number;
|
||||
value16?: number;
|
||||
value17?: number;
|
||||
value18?: number;
|
||||
value19?: number;
|
||||
value20?: number;
|
||||
value21?: number;
|
||||
value22?: number;
|
||||
value23?: number;
|
||||
value24?: number;
|
||||
value25?: number;
|
||||
value26?: number;
|
||||
value27?: number;
|
||||
value28?: number;
|
||||
value29?: number;
|
||||
value30?: number;
|
||||
value31?: number;
|
||||
value32?: number;
|
||||
desc1?: string;
|
||||
|
||||
constructor(data: Partial<Record> = {}) {
|
||||
super();
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override deserialize(input: any): this {
|
||||
Object.assign(this, Object.assign(this, super.deserialize(input)));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
4
src/Frontend/src/app/models/serializable.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Serializable {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serialize(input: this): any;
|
||||
}
|
||||
10
src/Frontend/src/app/models/user.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class User {
|
||||
id!: string;
|
||||
email!: string;
|
||||
userName!: string;
|
||||
avatar?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(input: any) {
|
||||
Object.assign(this, input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { DeviceService } from 'src/app/services/device.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-board',
|
||||
templateUrl: './board.component.html',
|
||||
styleUrls: ['./board.component.scss'],
|
||||
standalone: true
|
||||
})
|
||||
export class BoardComponent {
|
||||
constructor(
|
||||
public _device: DeviceService
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Route } from "@angular/router";
|
||||
import { BoardComponent } from "./board/board.component";
|
||||
|
||||
export const DASHBOARD_ROUTES: Route[] = [
|
||||
{ path: '', component: BoardComponent },
|
||||
];
|
||||
@@ -0,0 +1,258 @@
|
||||
<form [formGroup]="form" novalidate>
|
||||
<mat-card>
|
||||
<mat-card-header style="display: block;">
|
||||
<mat-card-title style="display:flex">
|
||||
Layer details
|
||||
<span style="flex: 1;"></span>
|
||||
<button mat-button (click)="export()">Export</button>
|
||||
<button mat-button *ngIf="document && document.type === LayerType.Administration"
|
||||
[routerLink]="['/app/layers/Edit/', document.id, 'duplicate']">Duplicate</button>
|
||||
<button mat-button *ngIf="document && document.type === LayerType.Administration"
|
||||
[routerLink]="['/app/layers/Edit/', document.id]">Edit</button>
|
||||
<button mat-button *ngIf="document && document.type === LayerType.Processed"
|
||||
(click)="processLayer()">Process layer</button>
|
||||
</mat-card-title>
|
||||
<mat-card-subtitle> </mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div *ngIf="document && document.isCancelled">
|
||||
This layer is cancelled. Will not be used in any further processing.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-form-field class="full-width" appearance="outline" *ngIf="document">
|
||||
<mat-label>Created</mat-label>
|
||||
<input matInput disabled [value]="document.created">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col">
|
||||
<mat-form-field class="full-width" appearance="outline" *ngIf="document">
|
||||
<mat-label>Modified</mat-label>
|
||||
<input matInput disabled [value]="document.modified">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort matSortActive="code" matSortDisableClear
|
||||
matSortDirection="desc">
|
||||
<ng-container matColumnDef="code">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Code</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.code}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef><b>Value1 sum</b></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value1">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value1</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value1 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef><b>{{valueSum | number:'1.2-2'}}</b></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value2">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value2</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value2 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value3">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value3</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value3 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value4">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value4</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value4 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value5">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value5</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value5 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value6">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value6</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value6 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value7">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value7</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value7 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value8">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value8</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value8 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value9">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value9</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value9 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value10">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value10</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value10 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value11">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value11</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value11 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value12">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value12</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value12 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value13">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value13</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value13 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value14">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value14</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value14 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value15">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value15</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value15 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value16">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value16</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value16 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value17">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value17</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value17 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value18">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value18</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value18 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value19">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value19</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value19 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value20">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value20</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value20 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value21">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value21</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value21 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value22">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value22</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value22 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value23">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value23</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value23 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value24">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value24</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value24 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value25">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value25</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value25 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value26">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value26</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value26 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value27">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value27</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value27 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value28">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value28</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value28 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value29">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value29</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value29 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value30">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value30</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value30 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value31">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value31</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value31 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value32">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value32</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.value32 | number:'1.2-2'}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="desc1">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description1</th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.desc1}} </td>
|
||||
<td mat-footer-cell *matFooterCellDef></td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns, sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
<tr mat-footer-row *matFooterRowDef="displayedColumns"></tr>
|
||||
</table>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</form>
|
||||
@@ -0,0 +1,151 @@
|
||||
import { DatePipe, NgIf, DecimalPipe, JsonPipe } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { Layer, LayerType } from 'src/app/models/layer.model';
|
||||
import { Record } from 'src/app/models/record.model';
|
||||
import { NotificationsService } from 'src/app/services/notifications.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-layer-detail',
|
||||
templateUrl: './layer-detail.component.html',
|
||||
styleUrls: ['./layer-detail.component.scss'],
|
||||
standalone: true,
|
||||
imports: [FormsModule, ReactiveFormsModule, MatCardModule,
|
||||
MatButtonModule, MatGridListModule, MatFormFieldModule, MatInputModule,
|
||||
NgIf, MatTableModule, MatSortModule, DecimalPipe, JsonPipe, RouterLink],
|
||||
providers: [DatePipe]
|
||||
})
|
||||
export class LayerDetailComponent implements OnInit {
|
||||
|
||||
public form!: UntypedFormGroup;
|
||||
public document!: Layer;
|
||||
|
||||
valueSum = 0;
|
||||
|
||||
displayedColumns = environment.views.layers.recordColumns.split("|");
|
||||
dataSource!: MatTableDataSource<Record>;
|
||||
|
||||
LayerType = LayerType;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
shortRecords: any = null;
|
||||
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
constructor(
|
||||
private fb$: UntypedFormBuilder,
|
||||
private http$: HttpClient,
|
||||
private route$: ActivatedRoute,
|
||||
private datePipe: DatePipe,
|
||||
private notifications$: NotificationsService
|
||||
) {
|
||||
this.form = Layer.getForm(this.fb$);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.document = await this.load();
|
||||
this.dataSource = new MatTableDataSource<Record>(this.document.records);
|
||||
this.dataSource.sort = this.sort;
|
||||
this.document.fillForm(this.form);
|
||||
this.form.disable();
|
||||
this.document.created = `${this.datePipe.transform(this.document.createdAt?.toDate(), 'short')}, ${this.document.createdBy?.userName}`;
|
||||
this.document.modified = `${this.datePipe.transform(this.document.modifiedAt?.toDate(), 'short')}, ${this.document.modifiedBy?.userName}`;
|
||||
this.valueSum = this.document.records.map(t => t.value1 || 0).reduce((acc, value) => acc + value, 0);
|
||||
this.createColumns();
|
||||
|
||||
this.prepareDataForAI();
|
||||
}
|
||||
private async load(): Promise<Layer> {
|
||||
return await Layer.getById(this.route$.snapshot.paramMap.get('id') || "", this.http$);
|
||||
}
|
||||
|
||||
createColumns() {
|
||||
this.displayedColumns = ['code'];
|
||||
for (let i = 1; i < 33; i++) {
|
||||
for (const record of this.document.records) {
|
||||
const propertyName = `value${i}` as keyof typeof record;
|
||||
if (record[propertyName] !== null) {
|
||||
this.displayedColumns.push(`value${i}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of this.document.records) {
|
||||
if (record.desc1 !== null) {
|
||||
this.displayedColumns.push(`desc1`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processLayer() {
|
||||
Layer.processLayer(this.document.id!, this.http$).then(() => {
|
||||
this.notifications$.add({
|
||||
text: "Layer processed",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async export() {
|
||||
if (await Layer.exportToGoogleSheet(this.document.id || "", this.http$)) {
|
||||
this.notifications$.add({
|
||||
text: "The file was saved on Google Drive",
|
||||
});
|
||||
} else {
|
||||
this.notifications$.add({
|
||||
text: "Save failed!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async prepareDataForAI() {
|
||||
const codes = this.document.records.map(x => x.code);
|
||||
const weatherURL = 'https://archive-api.open-meteo.com/v1/archive?latitude=54.36685&longitude=18.692&start_date=2023-12-01&end_date=2023-12-31&daily=temperature_2m_mean,rain_sum,snowfall_sum,wind_speed_10m_max&timezone=Europe%2FBerlin';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.http$.get(weatherURL).subscribe((data: any) => {
|
||||
console.log('pogoda', data);
|
||||
|
||||
// loop throught all days in december 2023
|
||||
const days = data['daily']['time'].length;
|
||||
console.log(days);
|
||||
this.shortRecords = [];
|
||||
for (let i = 0; i < days; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const res: any = {};
|
||||
res.day = i+1;
|
||||
res.code = 600;
|
||||
res.temperature = data['daily']['temperature_2m_mean'][i];
|
||||
res.rain = data['daily']['rain_sum'][i];
|
||||
res.snow = data['daily']['snowfall_sum'][i];
|
||||
res.wind = data['daily']['wind_speed_10m_max'][i];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const codeValues: any[] = [];
|
||||
codes.forEach(code => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const res: any = { code: code };
|
||||
res.value = this.getRecordValue(this.document.records.find(x => x.code === code)!, i+1);
|
||||
codeValues.push(res);
|
||||
});
|
||||
res.sell = codeValues.filter(x => x.value > 0);
|
||||
//= codes.map(code => this.getRecordValue(this.document.records.find(x => x.code === code)!, i+1));
|
||||
this.shortRecords.push(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRecordValue(record: Record, index: number) {
|
||||
const propertyName = `value${index}` as keyof typeof record;
|
||||
return record[propertyName];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
EDIT in progress
|
||||
@@ -0,0 +1,3 @@
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { AuthService } from 'src/app/auth/auth.service';
|
||||
import { Layer } from 'src/app/models/layer.model';
|
||||
import { Record } from 'src/app/models/record.model';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { NgIf, DecimalPipe } from '@angular/common';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-layer-edit',
|
||||
templateUrl: './layer-edit.component.html',
|
||||
styleUrls: ['./layer-edit.component.scss'],
|
||||
standalone: true,
|
||||
imports: [FormsModule, ReactiveFormsModule, MatCardModule, MatToolbarModule, MatButtonModule, RouterLink, MatGridListModule, MatFormFieldModule, MatInputModule, NgIf, MatSelectModule, MatOptionModule, MatIconModule, MatTableModule, MatSortModule, DecimalPipe]
|
||||
})
|
||||
export class LayerEditComponent implements OnInit {
|
||||
|
||||
public form!: UntypedFormGroup;
|
||||
private document!: Layer;
|
||||
|
||||
displayedColumns = environment.views.layers.recordColumns.split("|");
|
||||
dataSource!: MatTableDataSource<Record>;
|
||||
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
constructor(
|
||||
private fb$: UntypedFormBuilder,
|
||||
private router$: Router,
|
||||
private http$: HttpClient,
|
||||
private route$: ActivatedRoute,
|
||||
private auth$: AuthService
|
||||
) {
|
||||
this.form = Layer.getForm(this.fb$);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.document = await this.load();
|
||||
}
|
||||
|
||||
private async load(): Promise<Layer> {
|
||||
return await Layer.getById(this.route$.snapshot.paramMap.get('id') || "", this.http$);
|
||||
}
|
||||
async save() {
|
||||
if (this.form.invalid) {
|
||||
return;
|
||||
}
|
||||
this.document.loadForm(this.form);
|
||||
const id = await Layer.add(this.document, this.http$);
|
||||
this.router$.navigate(['../../Detail', id], { relativeTo: this.route$ });
|
||||
}
|
||||
|
||||
trackByUid(index: number, item: Record) {
|
||||
return item.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [(ngModel)]="name" (ngModelChange)="nameUpdate.next($event)">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col">
|
||||
<mat-form-field appearance="outline" class="search-field">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select (selectionChange)="loadList()" [(ngModel)]="type">
|
||||
<mat-option value="">All</mat-option>
|
||||
<mat-option [value]="LayerType.Import">Import</mat-option>
|
||||
<mat-option [value]="LayerType.Processed">Processed</mat-option>
|
||||
<mat-option [value]="LayerType.Administration">Administration</mat-option>
|
||||
<mat-option [value]="LayerType.Dictionary">Dictionary</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="number">
|
||||
<th mat-header-cell *matHeaderCellDef>Number</th>
|
||||
<td mat-cell *matCellDef="let element" [class.cancelled]="element.isCancelled">{{element.number}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||
<td mat-cell *matCellDef="let element" [class.cancelled]="element.isCancelled">{{element.name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>Type</th>
|
||||
<td mat-cell *matCellDef="let element">{{LayerType[element.type]}}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns, sticky: false"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['Detail/', row.id]"
|
||||
style="cursor: pointer" (contextmenu)="openInNewTab(row)"></tr>
|
||||
</table>
|
||||
<div (diunabiScrollEnd)="loadMore()"></div>
|
||||
@@ -0,0 +1,9 @@
|
||||
.search-field {
|
||||
width: 95%;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.cancelled {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { Layer, LayerType } from 'src/app/models/layer.model';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatChipsModule} from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { KeyValuePipe, NgFor } from '@angular/common';
|
||||
import { ScrollEndDirective } from 'src/app/directives/scroll-end.directive';
|
||||
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-layers-list',
|
||||
templateUrl: './layers-list.component.html',
|
||||
styleUrls: ['./layers-list.component.scss'],
|
||||
standalone: true,
|
||||
imports: [MatGridListModule, MatButtonModule, RouterLink, MatFormFieldModule,
|
||||
MatInputModule, MatTableModule, MatSortModule, MatSelectModule, FormsModule,
|
||||
MatChipsModule, MatIconModule, NgFor, ScrollEndDirective, KeyValuePipe]
|
||||
})
|
||||
export class LayersListComponent implements OnInit {
|
||||
displayedColumns = ['number', 'name', 'type'];
|
||||
dataSource!: Layer[];
|
||||
LayerType = LayerType;
|
||||
|
||||
start = 0;
|
||||
limit = 50;
|
||||
end: number = this.limit + this.start;
|
||||
loadingInProgress = false;
|
||||
|
||||
type: LayerType | '' = '';
|
||||
name: string = '';
|
||||
nameUpdate = new Subject<string>();
|
||||
|
||||
constructor(
|
||||
private _http: HttpClient,
|
||||
private _router: Router
|
||||
) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.nameUpdate.pipe(
|
||||
debounceTime(400),
|
||||
distinctUntilChanged())
|
||||
.subscribe(() => {
|
||||
this.loadList();
|
||||
});
|
||||
await this.loadList();
|
||||
}
|
||||
|
||||
async loadList() {
|
||||
this.start = 0;
|
||||
this.end = this.limit;
|
||||
this.dataSource = await Layer.getList(this._http, this.start, this.limit, this.name, this.type);
|
||||
}
|
||||
|
||||
async loadMore() {
|
||||
this.start = this.end;
|
||||
this.end += this.limit;
|
||||
this.dataSource = this.dataSource.concat(
|
||||
await Layer.getList(this._http, this.start, this.limit, this.name, this.type)
|
||||
);
|
||||
}
|
||||
|
||||
openInNewTab(element: Layer) {
|
||||
const url = this._router.serializeUrl(
|
||||
this._router.createUrlTree([`/app/layers/Detail/${element.id}`])
|
||||
);
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
11
src/Frontend/src/app/modules/layers/layers.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { LayerDetailComponent } from './layer-detail/layer-detail.component';
|
||||
import { LayerEditComponent } from './layer-edit/layer-edit.component';
|
||||
import { LayersListComponent } from './layers-list/layers-list.component';
|
||||
|
||||
export const LAYERS_ROUTES: Route[] = [
|
||||
{ path: '', component: LayersListComponent },
|
||||
{ path: 'Edit/:id', component: LayerEditComponent },
|
||||
{ path: 'Edit/:id/:duplicate', component: LayerEditComponent },
|
||||
{ path: 'Detail/:id', component: LayerDetailComponent }
|
||||
];
|
||||
9
src/Frontend/src/app/services/data.service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
public showLoader: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
}
|
||||
50
src/Frontend/src/app/services/device.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DeviceService {
|
||||
|
||||
private TABLET_WIDTH = 1000;
|
||||
private DESKTOP_WIDTH = 1400;
|
||||
|
||||
public orientation?: "landscape" | "portraid";
|
||||
public flipPhone = false;
|
||||
public width?: number;
|
||||
|
||||
constructor(
|
||||
private mediaMacher: MediaMatcher
|
||||
) {
|
||||
this.checkScreen();
|
||||
window.addEventListener("resize", () => {
|
||||
this.checkScreen();
|
||||
});
|
||||
}
|
||||
checkScreen() {
|
||||
const isPortrait = window.outerWidth < window.outerHeight;
|
||||
if (
|
||||
isPortrait &&
|
||||
window.outerWidth <this.TABLET_WIDTH
|
||||
) {
|
||||
this.flipPhone = true;
|
||||
} else {
|
||||
this.flipPhone = false;
|
||||
}
|
||||
this.orientation = isPortrait ? "portraid" : "landscape";
|
||||
this.width = window.outerWidth;
|
||||
}
|
||||
isMobile(): boolean {
|
||||
return window.outerWidth < this.TABLET_WIDTH;
|
||||
}
|
||||
isTablet(): boolean {
|
||||
return window.outerWidth > this.TABLET_WIDTH && window.outerWidth < this.DESKTOP_WIDTH;
|
||||
}
|
||||
isDesktop(): boolean {
|
||||
return window.outerWidth > this.DESKTOP_WIDTH;
|
||||
}
|
||||
toString() {
|
||||
return `Orientation: ${this.orientation}, Width: ${this.width}, Flip: ${this.flipPhone}, IsMobile: ${this.isMobile()}
|
||||
IsTablet: ${this.isTablet()}, IsDesktop: ${this.isDesktop()}`;
|
||||
}
|
||||
}
|
||||
75
src/Frontend/src/app/services/notifications.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet';
|
||||
import { NotificationsComponent } from '../components/notifications/notifications.component';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import moment, { Moment } from 'moment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NotificationsService {
|
||||
|
||||
public messages: IMessage[] = [];
|
||||
private handler?: MatBottomSheetRef;
|
||||
|
||||
constructor(
|
||||
private bottomSheet$: MatBottomSheet
|
||||
) { }
|
||||
public add(message: IMessage): string {
|
||||
message.id = uuidv4();
|
||||
message.createdAt = moment();
|
||||
this.messages.push(message);
|
||||
this.sortMessages();
|
||||
if (this.messages.length === 1) {
|
||||
this.handler = this.bottomSheet$.open(NotificationsComponent, {
|
||||
hasBackdrop: false,
|
||||
disableClose: true
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.remove(message.id);
|
||||
}, message.duration ? message.duration : 5000);
|
||||
// close parent
|
||||
if (message.parentId) {
|
||||
setTimeout(() => {
|
||||
this.remove(message.parentId);
|
||||
}, 500);
|
||||
}
|
||||
return message.id;
|
||||
}
|
||||
private remove(id: string | undefined, checkDismiss: boolean = true) {
|
||||
if (checkDismiss) {
|
||||
const message = this.messages.find(x => x.id === id);
|
||||
if (message && message.dismiss) {
|
||||
message.dismiss();
|
||||
}
|
||||
}
|
||||
this.messages = this.messages.filter(x => x.id !== id);
|
||||
this.sortMessages();
|
||||
if (this.messages.length === 0 && this.handler) {
|
||||
this.handler.dismiss();
|
||||
}
|
||||
}
|
||||
private sortMessages() {
|
||||
this.messages = this.messages.sort((a, b) => {
|
||||
return a.createdAt && a.createdAt.isAfter(b.createdAt) ? 1 : -1;
|
||||
})
|
||||
}
|
||||
doAction(message: IMessage) {
|
||||
if (message.action) { message.action(); }
|
||||
this.remove(message.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
interface IMessage {
|
||||
id?: string;
|
||||
text: string;
|
||||
duration?: number;
|
||||
btn?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
action?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
dismiss?: Function;
|
||||
createdAt?: Moment,
|
||||
parentId?: string;
|
||||
}
|
||||
14
src/Frontend/src/app/views/login/login-view.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<script src="https://accounts.google.com/gsi/client" async="" defer=""></script>
|
||||
<div class="loading-container" *ngIf="loading">
|
||||
<img class="loading-img" src="../../assets/loader.gif" />
|
||||
</div>
|
||||
<div class="logo"></div>
|
||||
<div class="bg">
|
||||
<div class="container">
|
||||
<mat-card appearance="outlined" class="form">
|
||||
<mat-card-content>
|
||||
<div class="" id="google-button"></div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
73
src/Frontend/src/app/views/login/login-view.component.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
.bg {
|
||||
background-image: url("../../../assets/bg.jpg");
|
||||
height: 70vh;
|
||||
background-size: cover;
|
||||
padding-top: 30vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: fit-content;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background-image: url('../../../assets/logo.png');
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.load {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(100, 100, 100, 0.3);
|
||||
z-index: 1400;
|
||||
}
|
||||
|
||||
.loading-img {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* for mobile */
|
||||
@media screen and (max-width: 700px) {
|
||||
.container {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
99
src/Frontend/src/app/views/login/login-view.component.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Component, NgZone, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from 'src/app/auth/auth.service';
|
||||
import { NotificationsService } from 'src/app/services/notifications.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-view-page',
|
||||
templateUrl: './login-view.component.html',
|
||||
styleUrls: ['./login-view.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, MatCardModule, MatBottomSheetModule]
|
||||
})
|
||||
|
||||
export class LoginViewComponent implements OnInit {
|
||||
constructor(
|
||||
private router$: Router,
|
||||
private auth$: AuthService,
|
||||
private ngZone$: NgZone,
|
||||
private notifications$: NotificationsService
|
||||
) { }
|
||||
|
||||
loading = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
setTimeout(() => {
|
||||
this.initGoogleLogin();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
initGoogleLogin() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
google.accounts.id.initialize({
|
||||
client_id: environment.google.clientId,
|
||||
callback: this.handleCredentialResponse.bind(this),
|
||||
auto_select: true,
|
||||
cancel_on_tap_outside: true
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
google.accounts.id.renderButton(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
document.getElementById("google-button"),
|
||||
{ theme: "outline", size: "large", width: "100%" }
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
google.accounts.id.prompt();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async handleCredentialResponse(response: any) {
|
||||
try {
|
||||
this.auth$.retrieveUserFromCredentials(response.credential);
|
||||
this.auth$.webCredentials = response.credential;
|
||||
this.auth$.sendAuthData();
|
||||
this.ngZone$.run(() => {
|
||||
this.loading = true;
|
||||
});
|
||||
await this.auth$.getAPIToken();
|
||||
this.ngZone$.run(() => {
|
||||
this.router$.navigate(['/app']);
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.error('handleCredentialResponse', e);
|
||||
this.ngZone$.run(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
if (e.status === 401) {
|
||||
this.ngZone$.run(() => {
|
||||
this.notifications$.add({
|
||||
text: "User not exists in DiunaBI database.",
|
||||
btn: "OK",
|
||||
duration: 15000
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.ngZone$.run(() => {
|
||||
this.notifications$.add({
|
||||
text: "DiunaBI server not responded.",
|
||||
btn: "OK",
|
||||
duration: 15000
|
||||
});
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
48
src/Frontend/src/app/views/main/main-view.component.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="loading-container" *ngIf="loading">
|
||||
<img class="loading-img" src="../../assets/loader.gif" />
|
||||
</div>
|
||||
<div class="flip-container" *ngIf="device$.flipPhone">
|
||||
<div class="flip-msg">
|
||||
Flip the device.<br>
|
||||
<mat-icon>screen_rotation</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-drawer-container fullscreen>
|
||||
<mat-drawer #drawer mode="side" opened>
|
||||
<diunabi-main-menu></diunabi-main-menu>
|
||||
<mat-divider></mat-divider>
|
||||
<small>
|
||||
{{appVersion}}
|
||||
</small>
|
||||
<br>
|
||||
<small>
|
||||
© DiunaBI {{currentDate | date: 'yyyy'}}
|
||||
</small>
|
||||
</mat-drawer>
|
||||
<mat-toolbar class="toolbar" color="primary">
|
||||
<button mat-icon-button aria-label="Menu" (click)="drawer.toggle()">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
{{environment.appName}}
|
||||
<div class="fill-space"></div>
|
||||
<a mat-icon-button [matMenuTriggerFor]="userMenu">
|
||||
<mat-icon *ngIf="!auth$.user?.avatar">account_circle</mat-icon>
|
||||
<img *ngIf="auth$.user?.avatar" [src]="auth$.user?.avatar" class="profile-photo-small"
|
||||
alt="Profile photo">
|
||||
</a>
|
||||
<mat-menu #userMenu="matMenu">
|
||||
<div class="profile-info">
|
||||
<img [src]="auth$.user?.avatar" class="profile-photo-big" alt="Profile photo">
|
||||
<p>{{auth$.user?.userName}}</p>
|
||||
</div>
|
||||
<button mat-menu-item (click)="auth$.logoutHandler()">
|
||||
<mat-icon>exit_to_app</mat-icon>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</mat-toolbar>
|
||||
<div>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-drawer-container>
|
||||
59
src/Frontend/src/app/views/main/main-view.component.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(100, 100, 100, 0.3);
|
||||
z-index: 1400;
|
||||
}
|
||||
|
||||
.loading-img {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.flip-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(100, 100, 100, 0.95);
|
||||
z-index: 1500;
|
||||
}
|
||||
|
||||
.flip-msg {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
padding-top: 45vh;
|
||||
color: rgb(205, 206, 177);
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 4vh;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.profile-photo-small {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-photo-big {
|
||||
width: 100px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
63
src/Frontend/src/app/views/main/main-view.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Component, NgZone, ViewChild } from '@angular/core';
|
||||
import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { NavigationStart, Router, RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import packageInfo from 'package.json';
|
||||
import { delay } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { AuthService } from '../../auth/auth.service';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { DeviceService } from '../../services/device.service';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { NgIf, DatePipe } from '@angular/common';
|
||||
import { MainMenuComponent } from 'src/app/components/main-menu/main-menu.component';
|
||||
import moment from "moment";
|
||||
|
||||
@Component({
|
||||
selector: 'diunabi-main-view',
|
||||
templateUrl: './main-view.component.html',
|
||||
styleUrls: ['./main-view.component.scss'],
|
||||
standalone: true,
|
||||
imports: [NgIf, MatIconModule, MatToolbarModule, MatButtonModule, MatSidenavModule,
|
||||
MatListModule, RouterLink, MatDividerModule, RouterOutlet, DatePipe, MatMenuModule, MainMenuComponent]
|
||||
})
|
||||
export class MainViewComponent {
|
||||
@ViewChild('snav') snav?: MatSidenav;
|
||||
appVersion = packageInfo.azureBuild;
|
||||
|
||||
currentDate = moment().toDate();
|
||||
flipPhone = false;
|
||||
loading = false;
|
||||
environment = environment;
|
||||
|
||||
constructor(
|
||||
public data$: DataService,
|
||||
public device$: DeviceService,
|
||||
private router$: Router,
|
||||
private ngZone$: NgZone,
|
||||
public auth$: AuthService,
|
||||
) {
|
||||
this.router$.events.subscribe((event) => {
|
||||
if (event instanceof NavigationStart && device$.isMobile()) {
|
||||
this.snav?.close();
|
||||
}
|
||||
});
|
||||
|
||||
//listen to loading subject
|
||||
data$.showLoader.pipe(delay(0)).subscribe(loading => {
|
||||
this.ngZone$.run(() => {
|
||||
this.loading = loading;
|
||||
});
|
||||
});
|
||||
}
|
||||
logout() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
google.accounts.id.disableAutoSelect();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
0
src/Frontend/src/assets/.gitkeep
Normal file
BIN
src/Frontend/src/assets/bg.jpg
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
src/Frontend/src/assets/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/Frontend/src/assets/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/Frontend/src/assets/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/Frontend/src/assets/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/Frontend/src/assets/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/Frontend/src/assets/icons/icon-48x48.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/Frontend/src/assets/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/Frontend/src/assets/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/Frontend/src/assets/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/Frontend/src/assets/loader.gif
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/Frontend/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
16
src/Frontend/src/environments/environment.prod.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const environment = {
|
||||
appEnvironment: "#{app-environment}#",
|
||||
appName: "DiunaBI #{app-environment}#",
|
||||
production: true,
|
||||
api: {
|
||||
url: "#{api-url}#"
|
||||
},
|
||||
google: {
|
||||
clientId: "#{google-frontend-client-id}#"
|
||||
},
|
||||
views: {
|
||||
layers: {
|
||||
recordColumns: "#{views-layers-recordColumns}#"
|
||||
}
|
||||
}
|
||||
};
|
||||
30
src/Frontend/src/environments/environment.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
appEnvironment: "local",
|
||||
appName: "LOCAL_DiunaBI",
|
||||
production: false,
|
||||
api: {
|
||||
//url: "http://localhost:5400/api"
|
||||
url: "https://diunabi-morska.bim-it.pl/api"
|
||||
},
|
||||
google: {
|
||||
clientId: "107631825312-bkfe438ehr9k9ecb2h76g802tj6advma.apps.googleusercontent.com"
|
||||
},
|
||||
views: {
|
||||
layers: {
|
||||
recordColumns: "code|value1|desc1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||
BIN
src/Frontend/src/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
21
src/Frontend/src/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DiunaBI</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<script src="https://accounts.google.com/gsi/client" async="" defer=""></script>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#FF9800">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/icons/icon-192x192.png">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<diunabi-root></diunabi-root>
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
</body>
|
||||
</html>
|
||||
50
src/Frontend/src/main.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { enableProdMode, LOCALE_ID, isDevMode, importProvidersFrom } from '@angular/core';
|
||||
import { environment } from './environments/environment';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { provideServiceWorker } from '@angular/service-worker';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AuthInterceptor } from './app/interceptors/auth.interceptor';
|
||||
import { LoaderInterceptor } from './app/interceptors/loader.interceptor';
|
||||
import { HTTP_INTERCEPTORS, withInterceptorsFromDi, provideHttpClient } from '@angular/common/http';
|
||||
import { MomentDateAdapter, MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter';
|
||||
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
import { DatePipe, registerLocaleData } from '@angular/common';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { APP_ROUTES } from './app/app.routes';
|
||||
registerLocaleData(localePl);
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideRouter(APP_ROUTES),
|
||||
importProvidersFrom(
|
||||
BrowserModule,
|
||||
MatBottomSheetModule),
|
||||
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS] },
|
||||
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } },
|
||||
{ provide: LOCALE_ID, useValue: 'pl' },
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: LoaderInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
multi: true
|
||||
},
|
||||
provideAnimations(),
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideServiceWorker('ngsw-worker.js', {
|
||||
enabled: !isDevMode(),
|
||||
registrationStrategy: 'registerWhenStable:30000'
|
||||
})
|
||||
]
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
60
src/Frontend/src/manifest.webmanifest
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "DiunaBI",
|
||||
"short_name": "DiunaBI",
|
||||
"theme_color": "#FF9800",
|
||||
"background_color": "#fafafa",
|
||||
"display": "standalone",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"orientation": "landscape",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
53
src/Frontend/src/polyfills.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
90
src/Frontend/src/styles.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
@use "@angular/material" as mat;
|
||||
|
||||
@include mat.core();
|
||||
|
||||
$my-app-primary: mat.m2-define-palette(mat.$m2-orange-palette);
|
||||
$my-app-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
|
||||
$my-app-warn: mat.m2-define-palette(mat.$m2-red-palette);
|
||||
|
||||
$my-app-theme: mat.m2-define-light-theme((color: (primary: $my-app-primary,
|
||||
accent: $my-app-accent,
|
||||
warn: $my-app-warn,
|
||||
),
|
||||
));
|
||||
|
||||
@include mat.all-component-themes($my-app-theme);
|
||||
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
/* default .loading styles, .loading should be invisible, opacity: 0, z-index: -1 */
|
||||
.AppLoading {
|
||||
margin-top: -10px;
|
||||
margin-left: -10px;
|
||||
opacity: 0;
|
||||
transition: opacity .8s ease-in-out;
|
||||
position: fixed;
|
||||
height: 105%;
|
||||
width: 105%;
|
||||
z-index: -1;
|
||||
background-color: lightgrey;
|
||||
background-image: url('./assets/loader.gif');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* .loading screen is visible when app is not bootstrapped yet, my-app is empty */
|
||||
app-root:empty+.AppLoading {
|
||||
opacity: 1;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
:root {
|
||||
--avatar-size: 30px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-color: #ccc;
|
||||
border-radius: 50%;
|
||||
height: var(--avatar-size);
|
||||
text-align: center;
|
||||
width: var(--avatar-size);
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
min-width: 120px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.col:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
color: black;
|
||||
-webkit-text-fill-color: black;
|
||||
}
|
||||
14
src/Frontend/src/test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting(),
|
||||
);
|
||||
15
src/Frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
33
src/Frontend/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"useDefineForClassFields": false,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
18
src/Frontend/tsconfig.spec.json
Normal file
@@ -0,0 +1,18 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||