🚀 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
This commit is contained in:
Michał Zieliński
2025-05-31 19:32:33 +02:00
parent 9d1adef629
commit bf4712823d
198 changed files with 0 additions and 11812 deletions

View 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

View 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
View 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
View File

132
src/Frontend/angular.json Normal file
View 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"
]
}
}

View 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
});
};

View 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
View 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"
}
}

View 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>

View File

View 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;
}
}
});
}
}
}

View 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)
},
]
}
];

View 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);
})
}
}

View 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
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -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
) {}
}

View 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();
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}

View File

@@ -0,0 +1,4 @@
export interface Deserializable {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deserialize(input: any): this;
}

View 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)
})
})
}
}

View 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;
}
}

View File

@@ -0,0 +1,4 @@
export interface Serializable {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serialize(input: this): any;
}

View 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)
}
}

View File

@@ -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
) { }
}

View File

@@ -0,0 +1,6 @@
import { Route } from "@angular/router";
import { BoardComponent } from "./board/board.component";
export const DASHBOARD_ROUTES: Route[] = [
{ path: '', component: BoardComponent },
];

View File

@@ -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>&nbsp;</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>

View File

@@ -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];
}
}

View File

@@ -0,0 +1 @@
EDIT in progress

View File

@@ -0,0 +1,3 @@
.file-input {
display: none;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
.search-field {
width: 95%;
margin: 5px;
}
.cancelled {
text-decoration: line-through;
opacity: 0.6;
}

View File

@@ -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');
}
}

View 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 }
];

View 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);
}

View 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()}`;
}
}

View 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;
}

View 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>

View 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;
}
}

View 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;
}
}
}

View 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>
&nbsp;{{appVersion}}
</small>
<br>
<small>
&nbsp;&copy;&nbsp;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>

View 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;
}

View 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();
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View 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}#"
}
}
};

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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&amp;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
View 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));

View 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"
}
]
}

View 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
*/

View 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
View 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(),
);

View 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"
]
}

View 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
}
}

View 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"
]
}

9052
src/Frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff