This commit is contained in:
parent
a6cf0429aa
commit
eeeb3b45b2
36 changed files with 18814 additions and 1 deletions
21
.forgejo/workflows/qs.yml
Normal file
21
.forgejo/workflows/qs.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: "Quality Check"
|
||||
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
linting:
|
||||
name: "Linting"
|
||||
runs-on: "ubuntu-latest"
|
||||
container:
|
||||
image: "git.euph.dev/actions/runner-basic:latest"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "https://git.euph.dev/actions/checkout@v3"
|
||||
- name: "Linting TS"
|
||||
run: npm run lint:ts
|
||||
- name: "Linting SCSS"
|
||||
run: npm run lint:scss
|
||||
|
||||
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
.angular/
|
||||
|
||||
.idea/
|
||||
.vscode/
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
*
|
3
.stylelintignore
Normal file
3
.stylelintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.*
|
||||
!*.css
|
||||
!*.scss
|
14
.stylelintrc.json
Normal file
14
.stylelintrc.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "stylelint-config-standard-scss",
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"rules": {
|
||||
"custom-property-empty-line-before": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"media-feature-range-notation": null,
|
||||
"import-notation": "string",
|
||||
"scss/no-global-function-names": null,
|
||||
"no-empty-source": null
|
||||
}
|
||||
}
|
13
Justfile
Normal file
13
Justfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
_choose:
|
||||
just --choose
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
ng serve
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
lint:
|
||||
-npm run lint:ts
|
||||
-npm run lint:scss
|
17
README.md
17
README.md
|
@ -1,3 +1,18 @@
|
|||
![QS Badge](https://git.euph.dev//SZUT/EMS//actions/workflows/qs.yml/badge.svg?branch=trunk)
|
||||
|
||||
# EMS
|
||||
|
||||
https://hb.itslearning.com/LearningToolElement/ViewLearningToolElement.aspx?LearningToolElementId=21843391
|
||||
## Links
|
||||
|
||||
- [Ausgangslage](https://hb.itslearning.com/LearningToolElement/ViewLearningToolElement.aspx?LearningToolElementId=21843391)
|
||||
|
||||
## Setup
|
||||
|
||||
> [!NOTE]
|
||||
> Dieses Projekt hat ein [Justfile](https://just.systems)
|
||||
|
||||
1. Projekt Clonen
|
||||
1. NPM install
|
||||
1. Docker Container Hochfahren
|
||||
1. Angular App starten
|
||||
|
||||
|
|
102
angular.json
Normal file
102
angular.json
Normal file
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"a": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/a",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "a:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "a:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
compose.yml
Normal file
27
compose.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
services:
|
||||
postgres:
|
||||
container_name: ems_postgres
|
||||
image: postgres:17.2
|
||||
volumes:
|
||||
- ems_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: employee_db
|
||||
POSTGRES_USER: employee
|
||||
POSTGRES_PASSWORD: secret
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
api:
|
||||
container_name: ems_api
|
||||
image: berndheidemann/employee-management-service:1.1.3.1
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
spring.datasource.url: jdbc:postgresql://ems_postgres:5432/employee_db
|
||||
spring.datasource.username: employee
|
||||
spring.datasource.password: secret
|
||||
ports:
|
||||
- "8080:8089"
|
||||
|
||||
volumes:
|
||||
ems_data:
|
63
eslint.config.mjs
Normal file
63
eslint.config.mjs
Normal file
|
@ -0,0 +1,63 @@
|
|||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import {fileURLToPath} from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import {FlatCompat} from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["src/app/core/ems"],
|
||||
},
|
||||
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/strict"),
|
||||
{
|
||||
plugins: {
|
||||
"simple-import-sort": simpleImportSort,
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
indent: ["error", 4],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
quotes: ["error", "single"],
|
||||
semi: ["error", "always"],
|
||||
strict: "error",
|
||||
"array-bracket-newline": "error",
|
||||
yoda: "error",
|
||||
|
||||
"@typescript-eslint/array-type": [
|
||||
"error",
|
||||
{
|
||||
default: "generic",
|
||||
},
|
||||
],
|
||||
|
||||
"@typescript-eslint/ban-tslint-comment": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-extraneous-class": "off",
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"no-mixed-spaces-and-tabs": "off",
|
||||
},
|
||||
},
|
||||
];
|
4
getBearerToken.http
Normal file
4
getBearerToken.http
Normal file
|
@ -0,0 +1,4 @@
|
|||
POST http://authproxy.szut.dev
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=password&client_id=employee-management-service&username=user&password=test
|
16934
package-lock.json
generated
Normal file
16934
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
55
package.json
Normal file
55
package.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "a",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"lint:ts": "eslint src",
|
||||
"lint:scss": "stylelint src",
|
||||
"lint:ts:fix": "eslint src --fix",
|
||||
"lint:scss:fix": "stylelint src --fix"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.0.0",
|
||||
"@angular/common": "^19.0.0",
|
||||
"@angular/compiler": "^19.0.0",
|
||||
"@angular/core": "^19.0.0",
|
||||
"@angular/forms": "^19.0.0",
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.2",
|
||||
"@angular/cli": "^19.0.2",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@hey-api/openapi-ts": "^0.52.9",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-autofix": "^2.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.13.0",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"stylelint": "^16.12.0",
|
||||
"stylelint-config-standard-scss": "^14.0.0",
|
||||
"stylelint-scss": "^6.10.0",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.12.0"
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
8
src/app/app.component.html
Normal file
8
src/app/app.component.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
@for (employee of $employees | async ; track employee.id) {
|
||||
<code style="white-space: pre-wrap">
|
||||
{{ employee | json}}
|
||||
</code>
|
||||
<hr>
|
||||
}
|
||||
|
||||
<router-outlet />
|
0
src/app/app.component.scss
Normal file
0
src/app/app.component.scss
Normal file
20
src/app/app.component.ts
Normal file
20
src/app/app.component.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { EmployeeControllerService, FindAll1Response } from '@core/ems/index';
|
||||
import { Observable } from 'rxjs';
|
||||
import {AsyncPipe, JsonPipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, JsonPipe, AsyncPipe],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
export class AppComponent{
|
||||
title = 'EMS';
|
||||
readonly $employees: Observable<FindAll1Response>
|
||||
|
||||
constructor(private apiClient: EmployeeControllerService) {
|
||||
this.$employees = this.apiClient.findAll1();
|
||||
}
|
||||
}
|
17
src/app/app.config.ts
Normal file
17
src/app/app.config.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { routes } from '@app/app.routes';
|
||||
import { OpenAPI } from '@core/ems/core/OpenAPI';
|
||||
OpenAPI.BASE = 'http://localhost:8080';
|
||||
OpenAPI.TOKEN = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzUFQ0dldiNno5MnlQWk1EWnBqT1U0RjFVN0lwNi1ELUlqQWVGczJPbGU0In0.eyJleHAiOjE3MzQ1MTY5MTQsImlhdCI6MTczNDUxMzMxNCwianRpIjoiMjk2MzAwNTQtMWI2Ni00ODIyLWFiMGItNmI5NjlkZDg4MTRkIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5zenV0LmRldi9hdXRoL3JlYWxtcy9zenV0IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU1NDZjZDIxLTk4NTQtNDMyZi1hNDY3LTRkZTNlZWRmNTg4OSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImVtcGxveWVlLW1hbmFnZW1lbnQtc2VydmljZSIsInNlc3Npb25fc3RhdGUiOiJhMDIzMmNhMy0yZjM3LTQ3OTctYThkMC0zNjJhNzBlODY1MDMiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NDIwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsicHJvZHVjdF9vd25lciIsIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1zenV0IiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIifQ.evYkQetnwJWgzHjSRrZyN1Ls-fpdBsM0KYwp0RKcsgrg4w4p-WMnFGDch_1kFWflMq9i3J_sAhsPcFhvCiKEpo5xca9yj0-20GMUZXH7aNwCneqGur2f9wh3k8nQdoYsuwKTfKpGSsNWTQ20rV7CqHIne-lD2rayhb2wHwOsbklsSf0K9s37EKPwASfln4g1KGTbBpwFDz8p75Vhr5HqlaCJUxN-NMYKM41_2ao8ykhCVkKRk7cZwjKfB3MOk5Xk8l0NHcPBp8G_pOyrjyqC-fGekcxEF1ucYSWqw4r_PsUTeLkO8M-RK0h798JWq7l-OcODgxr4qhx-ungUPnGFxw';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
provideAnimationsAsync()
|
||||
],
|
||||
};
|
3
src/app/app.routes.ts
Normal file
3
src/app/app.routes.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [];
|
11
src/app/core/ems/config.ts
Normal file
11
src/app/core/ems/config.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {defineConfig} from '@hey-api/openapi-ts';
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/app/core/ems/ems.yml',
|
||||
output: 'src/app/core/ems',
|
||||
schemas: false,
|
||||
services: {
|
||||
asClass: true
|
||||
},
|
||||
client: 'angular',
|
||||
});
|
21
src/app/core/ems/core/ApiError.ts
Normal file
21
src/app/core/ems/core/ApiError.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: unknown;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||
super(message);
|
||||
|
||||
this.name = 'ApiError';
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.body = response.body;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
21
src/app/core/ems/core/ApiRequestOptions.ts
Normal file
21
src/app/core/ems/core/ApiRequestOptions.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
export type ApiRequestOptions<T = unknown> = {
|
||||
readonly body?: any;
|
||||
readonly cookies?: Record<string, unknown>;
|
||||
readonly errors?: Record<number | string, string>;
|
||||
readonly formData?: Record<string, unknown> | any[] | Blob | File;
|
||||
readonly headers?: Record<string, unknown>;
|
||||
readonly mediaType?: string;
|
||||
readonly method:
|
||||
| 'DELETE'
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'OPTIONS'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT';
|
||||
readonly path?: Record<string, unknown>;
|
||||
readonly query?: Record<string, unknown>;
|
||||
readonly responseHeader?: string;
|
||||
readonly responseTransformer?: (data: unknown) => Promise<T>;
|
||||
readonly url: string;
|
||||
};
|
7
src/app/core/ems/core/ApiResult.ts
Normal file
7
src/app/core/ems/core/ApiResult.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type ApiResult<TData = any> = {
|
||||
readonly body: TData;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly url: string;
|
||||
};
|
55
src/app/core/ems/core/OpenAPI.ts
Normal file
55
src/app/core/ems/core/OpenAPI.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import type { HttpResponse } from '@angular/common/http';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
|
||||
type Headers = Record<string, string>;
|
||||
type Middleware<T> = (value: T) => T | Promise<T>;
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||
|
||||
export class Interceptors<T> {
|
||||
_fns: Middleware<T>[];
|
||||
|
||||
constructor() {
|
||||
this._fns = [];
|
||||
}
|
||||
|
||||
eject(fn: Middleware<T>): void {
|
||||
const index = this._fns.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
|
||||
}
|
||||
}
|
||||
|
||||
use(fn: Middleware<T>): void {
|
||||
this._fns = [...this._fns, fn];
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
interceptors: {
|
||||
response: Interceptors<HttpResponse<any>>;
|
||||
};
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: '',
|
||||
CREDENTIALS: 'include',
|
||||
ENCODE_PATH: undefined,
|
||||
HEADERS: undefined,
|
||||
PASSWORD: undefined,
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
VERSION: '1.1.2',
|
||||
WITH_CREDENTIALS: false,
|
||||
interceptors: {
|
||||
response: new Interceptors(),
|
||||
},
|
||||
};
|
337
src/app/core/ems/core/request.ts
Normal file
337
src/app/core/ems/core/request.ts
Normal file
|
@ -0,0 +1,337 @@
|
|||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import type { HttpResponse, HttpErrorResponse } from '@angular/common/http';
|
||||
import { forkJoin, of, throwError } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import { ApiError } from './ApiError';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import type { OpenAPIConfig } from './OpenAPI';
|
||||
|
||||
export const isString = (value: unknown): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: unknown): value is string => {
|
||||
return isString(value) && value !== '';
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return value instanceof Blob;
|
||||
};
|
||||
|
||||
export const isFormData = (value: unknown): value is FormData => {
|
||||
return value instanceof FormData;
|
||||
};
|
||||
|
||||
export const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString('base64');
|
||||
}
|
||||
};
|
||||
|
||||
export const getQueryString = (params: Record<string, unknown>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: unknown) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const encodePair = (key: string, value: unknown) => {
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
append(key, value.toISOString());
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach(v => encodePair(key, v));
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
|
||||
|
||||
return qs.length ? `?${qs.join('&')}` : '';
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace('{api-version}', config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
}
|
||||
return substring;
|
||||
});
|
||||
|
||||
const url = config.BASE + path;
|
||||
return options.query ? url + getQueryString(options.query) : url;
|
||||
};
|
||||
|
||||
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: unknown) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(options: ApiRequestOptions<T>, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = <T>(config: OpenAPIConfig, options: ApiRequestOptions<T>): Observable<HttpHeaders> => {
|
||||
return forkJoin({
|
||||
// @ts-ignore
|
||||
token: resolve(options, config.TOKEN),
|
||||
// @ts-ignore
|
||||
username: resolve(options, config.USERNAME),
|
||||
// @ts-ignore
|
||||
password: resolve(options, config.PASSWORD),
|
||||
// @ts-ignore
|
||||
additionalHeaders: resolve(options, config.HEADERS),
|
||||
}).pipe(
|
||||
map(({ token, username, password, additionalHeaders }) => {
|
||||
const headers = Object.entries({
|
||||
Accept: 'application/json',
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.reduce((headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}), {} as Record<string, string>);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||
} else if (isString(options.body)) {
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
return new HttpHeaders(headers);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): unknown => {
|
||||
if (options.body) {
|
||||
if (options.mediaType?.includes('application/json') || options.mediaType?.includes('+json')) {
|
||||
return JSON.stringify(options.body);
|
||||
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
|
||||
return options.body;
|
||||
} else {
|
||||
return JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
http: HttpClient,
|
||||
url: string,
|
||||
body: unknown,
|
||||
formData: FormData | undefined,
|
||||
headers: HttpHeaders
|
||||
): Observable<HttpResponse<T>> => {
|
||||
return http.request<T>(options.method, url, {
|
||||
headers,
|
||||
body: body ?? formData,
|
||||
withCredentials: config.WITH_CREDENTIALS,
|
||||
observe: 'response',
|
||||
});
|
||||
};
|
||||
|
||||
export const getResponseHeader = <T>(response: HttpResponse<T>, responseHeader?: string): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const value = response.headers.get(responseHeader);
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getResponseBody = <T>(response: HttpResponse<T>): T | undefined => {
|
||||
if (response.status !== 204 && response.body !== null) {
|
||||
return response.body;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
402: 'Payment Required',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
405: 'Method Not Allowed',
|
||||
406: 'Not Acceptable',
|
||||
407: 'Proxy Authentication Required',
|
||||
408: 'Request Timeout',
|
||||
409: 'Conflict',
|
||||
410: 'Gone',
|
||||
411: 'Length Required',
|
||||
412: 'Precondition Failed',
|
||||
413: 'Payload Too Large',
|
||||
414: 'URI Too Long',
|
||||
415: 'Unsupported Media Type',
|
||||
416: 'Range Not Satisfiable',
|
||||
417: 'Expectation Failed',
|
||||
418: 'Im a teapot',
|
||||
421: 'Misdirected Request',
|
||||
422: 'Unprocessable Content',
|
||||
423: 'Locked',
|
||||
424: 'Failed Dependency',
|
||||
425: 'Too Early',
|
||||
426: 'Upgrade Required',
|
||||
428: 'Precondition Required',
|
||||
429: 'Too Many Requests',
|
||||
431: 'Request Header Fields Too Large',
|
||||
451: 'Unavailable For Legal Reasons',
|
||||
500: 'Internal Server Error',
|
||||
501: 'Not Implemented',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
504: 'Gateway Timeout',
|
||||
505: 'HTTP Version Not Supported',
|
||||
506: 'Variant Also Negotiates',
|
||||
507: 'Insufficient Storage',
|
||||
508: 'Loop Detected',
|
||||
510: 'Not Extended',
|
||||
511: 'Network Authentication Required',
|
||||
...options.errors,
|
||||
}
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? 'unknown';
|
||||
const errorStatusText = result.statusText ?? 'unknown';
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
throw new ApiError(options, result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param http The Angular HTTP client
|
||||
* @param options The request options from the service
|
||||
* @returns Observable<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(config: OpenAPIConfig, http: HttpClient, options: ApiRequestOptions<T>): Observable<T> => {
|
||||
const url = getUrl(config, options);
|
||||
const formData = getFormData(options);
|
||||
const body = getRequestBody(options);
|
||||
|
||||
return getHeaders(config, options).pipe(
|
||||
switchMap(headers => {
|
||||
return sendRequest<T>(config, options, http, url, body, formData, headers);
|
||||
}),
|
||||
switchMap(async response => {
|
||||
for (const fn of config.interceptors.response._fns) {
|
||||
response = await fn(response);
|
||||
}
|
||||
const responseBody = getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||
|
||||
let transformedBody = responseBody;
|
||||
if (options.responseTransformer && response.ok) {
|
||||
transformedBody = await options.responseTransformer(responseBody)
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? transformedBody,
|
||||
} as ApiResult;
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (!error.status) {
|
||||
return throwError(() => error);
|
||||
}
|
||||
return of({
|
||||
url,
|
||||
ok: error.ok,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
body: error.error ?? error.statusText,
|
||||
} as ApiResult);
|
||||
}),
|
||||
map(result => {
|
||||
catchErrorCodes(options, result);
|
||||
return result.body as T;
|
||||
}),
|
||||
catchError((error: ApiError) => {
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
};
|
493
src/app/core/ems/ems.yml
Normal file
493
src/app/core/ems/ems.yml
Normal file
|
@ -0,0 +1,493 @@
|
|||
openapi: 3.0.1
|
||||
info:
|
||||
title: Employees Management Micro-Service
|
||||
description: "\n## Overview\n\nEmployees Management Service API manages the employees\
|
||||
\ of HighTec Gmbh including their qualifications. It offers the possibility to\
|
||||
\ create, read, update and delete employees and qualifications. Existing employees\
|
||||
\ can be assigned new qualifications or have them withdrawn. \nThe API is organized\
|
||||
\ around REST. It has predictable resource-oriented URLs, accepts JSON-encoded\
|
||||
\ request bodies, returns JSON-encoded responses, uses standard HTTP response\
|
||||
\ codes and authentication.\n"
|
||||
version: 1.1.2
|
||||
servers:
|
||||
- url: ""
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/qualifications/{id}:
|
||||
put:
|
||||
tags:
|
||||
- qualification-controller
|
||||
summary: updates a qualification
|
||||
operationId: updateQualification
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualificationPostDTO'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: updated qualification
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualificationPostDTO'
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
"400":
|
||||
description: invalid JSON posted
|
||||
delete:
|
||||
tags:
|
||||
- qualification-controller
|
||||
summary: deletes a qualification by id
|
||||
operationId: deleteQualificationByDesignation
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
"204":
|
||||
description: delete successful
|
||||
"403":
|
||||
description: qualification is in use
|
||||
/employees/{id}:
|
||||
get:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: find employee by id
|
||||
operationId: findById
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"200":
|
||||
description: employee
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeResponseDTO'
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
put:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: updates employee by id
|
||||
operationId: updateEmployee
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeRequestPutDTO'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: employee
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeResponseDTO'
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
delete:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: deletes a employee by id
|
||||
operationId: deleteCustomer
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
"204":
|
||||
description: delete successful
|
||||
patch:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: updates employee by id
|
||||
operationId: patchEmployee
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeRequestPutDTO'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: employee
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeResponseDTO'
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
/qualifications:
|
||||
get:
|
||||
tags:
|
||||
- qualification-controller
|
||||
summary: delivers a list of all available qualifications
|
||||
operationId: findAll
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"200":
|
||||
description: list of qualifications
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualificationGetDTO'
|
||||
post:
|
||||
tags:
|
||||
- qualification-controller
|
||||
summary: creates a new qualification with its id and designation
|
||||
operationId: createQualification
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualificationPostDTO'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
description: created qualification
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualificationPostDTO'
|
||||
"401":
|
||||
description: not authorized
|
||||
"400":
|
||||
description: invalid JSON posted
|
||||
/employees:
|
||||
get:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: delivers a list of all employees
|
||||
operationId: findAll_1
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"200":
|
||||
description: list of employees
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EmployeeResponseDTO'
|
||||
post:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: creates a new employee
|
||||
operationId: createEmployee
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeRequestDTO'
|
||||
required: true
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"201":
|
||||
description: created employee
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeResponseDTO'
|
||||
"400":
|
||||
description: invalid JSON posted
|
||||
/employees/{id}/qualifications:
|
||||
get:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: finds all qualifications of an employee by id
|
||||
operationId: findAllQualificationOfAEmployeeById
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
"200":
|
||||
description: employee with a list of his qualifications
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeNameAndSkillDataDTO'
|
||||
post:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: adds a qualification to an employee by id
|
||||
operationId: addQualificationToEmployeeById
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualificationPostDTO'
|
||||
required: true
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
"400":
|
||||
description: invalid JSON posted or employee already has this qualification
|
||||
"200":
|
||||
description: employee with a list of his qualifications
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeNameAndSkillDataDTO'
|
||||
/qualifications/{id}/employees:
|
||||
get:
|
||||
tags:
|
||||
- qualification-controller
|
||||
summary: find employees by qualification id
|
||||
operationId: findAllEmployeesByQualification
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"200":
|
||||
description: List of employees who have the desired qualification
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeesForAQualificationDTO'
|
||||
"404":
|
||||
description: qualification id does not exist
|
||||
/employees/{eid}/qualifications/{qid}:
|
||||
delete:
|
||||
tags:
|
||||
- employee-controller
|
||||
summary: deletes a qualification of an employee by id
|
||||
operationId: removeQualificationFromEmployee
|
||||
parameters:
|
||||
- name: eid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: qid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"401":
|
||||
description: not authorized
|
||||
"404":
|
||||
description: resource not found
|
||||
"200":
|
||||
description: employee with a list of his qualifications
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmployeeNameAndSkillDataDTO'
|
||||
components:
|
||||
schemas:
|
||||
QualificationPostDTO:
|
||||
required:
|
||||
- skill
|
||||
type: object
|
||||
properties:
|
||||
skill:
|
||||
type: string
|
||||
EmployeeRequestPutDTO:
|
||||
type: object
|
||||
properties:
|
||||
lastName:
|
||||
type: string
|
||||
firstName:
|
||||
type: string
|
||||
street:
|
||||
type: string
|
||||
postcode:
|
||||
type: string
|
||||
city:
|
||||
type: string
|
||||
phone:
|
||||
type: string
|
||||
skillSet:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
EmployeeResponseDTO:
|
||||
required:
|
||||
- city
|
||||
- firstName
|
||||
- lastName
|
||||
- phone
|
||||
- postcode
|
||||
- street
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
lastName:
|
||||
type: string
|
||||
firstName:
|
||||
type: string
|
||||
street:
|
||||
type: string
|
||||
postcode:
|
||||
maxLength: 5
|
||||
minLength: 5
|
||||
type: string
|
||||
city:
|
||||
type: string
|
||||
phone:
|
||||
type: string
|
||||
skillSet:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QualificationGetDTO'
|
||||
QualificationGetDTO:
|
||||
type: object
|
||||
properties:
|
||||
skill:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
EmployeeRequestDTO:
|
||||
required:
|
||||
- city
|
||||
- firstName
|
||||
- lastName
|
||||
- phone
|
||||
- postcode
|
||||
- street
|
||||
type: object
|
||||
properties:
|
||||
lastName:
|
||||
type: string
|
||||
firstName:
|
||||
type: string
|
||||
street:
|
||||
type: string
|
||||
postcode:
|
||||
maxLength: 5
|
||||
minLength: 5
|
||||
type: string
|
||||
city:
|
||||
type: string
|
||||
phone:
|
||||
type: string
|
||||
skillSet:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
EmployeeNameAndSkillDataDTO:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
lastName:
|
||||
type: string
|
||||
firstName:
|
||||
type: string
|
||||
skillSet:
|
||||
uniqueItems: true
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QualificationGetDTO'
|
||||
EmployeeNameDataDTO:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
lastName:
|
||||
type: string
|
||||
firstName:
|
||||
type: string
|
||||
EmployeesForAQualificationDTO:
|
||||
type: object
|
||||
properties:
|
||||
qualification:
|
||||
$ref: '#/components/schemas/QualificationGetDTO'
|
||||
employees:
|
||||
uniqueItems: true
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EmployeeNameDataDTO'
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
name: bearerAuth
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
5
src/app/core/ems/index.ts
Normal file
5
src/app/core/ems/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
|
||||
export * from './services.gen';
|
||||
export * from './types.gen';
|
321
src/app/core/ems/services.gen.ts
Normal file
321
src/app/core/ems/services.gen.ts
Normal file
|
@ -0,0 +1,321 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { OpenAPI } from './core/OpenAPI';
|
||||
import { request as __request } from './core/request';
|
||||
import type { UpdateQualificationData, UpdateQualificationResponse, DeleteQualificationByDesignationData, DeleteQualificationByDesignationResponse, FindAllResponse, CreateQualificationData, CreateQualificationResponse, FindAllEmployeesByQualificationData, FindAllEmployeesByQualificationResponse, FindByIdData, FindByIdResponse, UpdateEmployeeData, UpdateEmployeeResponse, DeleteCustomerData, DeleteCustomerResponse, PatchEmployeeData, PatchEmployeeResponse, FindAll1Response, CreateEmployeeData, CreateEmployeeResponse, FindAllQualificationOfAemployeeByIdData, FindAllQualificationOfAemployeeByIdResponse, AddQualificationToEmployeeByIdData, AddQualificationToEmployeeByIdResponse, RemoveQualificationFromEmployeeData, RemoveQualificationFromEmployeeResponse } from './types.gen';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class QualificationControllerService {
|
||||
constructor(public readonly http: HttpClient) { }
|
||||
|
||||
/**
|
||||
* updates a qualification
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @param data.requestBody
|
||||
* @returns QualificationPostDTO updated qualification
|
||||
* @throws ApiError
|
||||
*/
|
||||
public updateQualification(data: UpdateQualificationData): Observable<UpdateQualificationResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'PUT',
|
||||
url: '/qualifications/{id}',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
400: 'invalid JSON posted',
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a qualification by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @returns void delete successful
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deleteQualificationByDesignation(data: DeleteQualificationByDesignationData): Observable<DeleteQualificationByDesignationResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'DELETE',
|
||||
url: '/qualifications/{id}',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
403: 'qualification is in use',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* delivers a list of all available qualifications
|
||||
* @returns QualificationGetDTO list of qualifications
|
||||
* @throws ApiError
|
||||
*/
|
||||
public findAll(): Observable<FindAllResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'GET',
|
||||
url: '/qualifications',
|
||||
errors: {
|
||||
401: 'not authorized'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new qualification with its id and designation
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns QualificationPostDTO created qualification
|
||||
* @throws ApiError
|
||||
*/
|
||||
public createQualification(data: CreateQualificationData): Observable<CreateQualificationResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'POST',
|
||||
url: '/qualifications',
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
400: 'invalid JSON posted',
|
||||
401: 'not authorized'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* find employees by qualification id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @returns EmployeesForAQualificationDTO List of employees who have the desired qualification
|
||||
* @throws ApiError
|
||||
*/
|
||||
public findAllEmployeesByQualification(data: FindAllEmployeesByQualificationData): Observable<FindAllEmployeesByQualificationResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'GET',
|
||||
url: '/qualifications/{id}/employees',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'qualification id does not exist'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class EmployeeControllerService {
|
||||
constructor(public readonly http: HttpClient) { }
|
||||
|
||||
/**
|
||||
* find employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @returns EmployeeResponseDTO employee
|
||||
* @throws ApiError
|
||||
*/
|
||||
public findById(data: FindByIdData): Observable<FindByIdResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'GET',
|
||||
url: '/employees/{id}',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* updates employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @param data.requestBody
|
||||
* @returns EmployeeResponseDTO employee
|
||||
* @throws ApiError
|
||||
*/
|
||||
public updateEmployee(data: UpdateEmployeeData): Observable<UpdateEmployeeResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'PUT',
|
||||
url: '/employees/{id}',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @returns void delete successful
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deleteCustomer(data: DeleteCustomerData): Observable<DeleteCustomerResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'DELETE',
|
||||
url: '/employees/{id}',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* updates employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @param data.requestBody
|
||||
* @returns EmployeeResponseDTO employee
|
||||
* @throws ApiError
|
||||
*/
|
||||
public patchEmployee(data: PatchEmployeeData): Observable<PatchEmployeeResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'PATCH',
|
||||
url: '/employees/{id}',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* delivers a list of all employees
|
||||
* @returns EmployeeResponseDTO list of employees
|
||||
* @throws ApiError
|
||||
*/
|
||||
public findAll1(): Observable<FindAll1Response> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'GET',
|
||||
url: '/employees',
|
||||
errors: {
|
||||
401: 'not authorized'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new employee
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns EmployeeResponseDTO created employee
|
||||
* @throws ApiError
|
||||
*/
|
||||
public createEmployee(data: CreateEmployeeData): Observable<CreateEmployeeResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'POST',
|
||||
url: '/employees',
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
400: 'invalid JSON posted',
|
||||
401: 'not authorized'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* finds all qualifications of an employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @returns EmployeeNameAndSkillDataDTO employee with a list of his qualifications
|
||||
* @throws ApiError
|
||||
*/
|
||||
public findAllQualificationOfAemployeeById(data: FindAllQualificationOfAemployeeByIdData): Observable<FindAllQualificationOfAemployeeByIdResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'GET',
|
||||
url: '/employees/{id}/qualifications',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* adds a qualification to an employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.id
|
||||
* @param data.requestBody
|
||||
* @returns EmployeeNameAndSkillDataDTO employee with a list of his qualifications
|
||||
* @throws ApiError
|
||||
*/
|
||||
public addQualificationToEmployeeById(data: AddQualificationToEmployeeByIdData): Observable<AddQualificationToEmployeeByIdResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'POST',
|
||||
url: '/employees/{id}/qualifications',
|
||||
path: {
|
||||
id: data.id
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
400: 'invalid JSON posted or employee already has this qualification',
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a qualification of an employee by id
|
||||
* @param data The data for the request.
|
||||
* @param data.eid
|
||||
* @param data.qid
|
||||
* @returns EmployeeNameAndSkillDataDTO employee with a list of his qualifications
|
||||
* @throws ApiError
|
||||
*/
|
||||
public removeQualificationFromEmployee(data: RemoveQualificationFromEmployeeData): Observable<RemoveQualificationFromEmployeeResponse> {
|
||||
return __request(OpenAPI, this.http, {
|
||||
method: 'DELETE',
|
||||
url: '/employees/{eid}/qualifications/{qid}',
|
||||
path: {
|
||||
eid: data.eid,
|
||||
qid: data.qid
|
||||
},
|
||||
errors: {
|
||||
401: 'not authorized',
|
||||
404: 'resource not found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
140
src/app/core/ems/types.gen.ts
Normal file
140
src/app/core/ems/types.gen.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type QualificationPostDTO = {
|
||||
skill: string;
|
||||
};
|
||||
|
||||
export type EmployeeRequestPutDTO = {
|
||||
lastName?: string;
|
||||
firstName?: string;
|
||||
street?: string;
|
||||
postcode?: string;
|
||||
city?: string;
|
||||
phone?: string;
|
||||
skillSet?: Array<(number)>;
|
||||
};
|
||||
|
||||
export type EmployeeResponseDTO = {
|
||||
id?: number;
|
||||
lastName: string;
|
||||
firstName: string;
|
||||
street: string;
|
||||
postcode: string;
|
||||
city: string;
|
||||
phone: string;
|
||||
skillSet?: Array<QualificationGetDTO>;
|
||||
};
|
||||
|
||||
export type QualificationGetDTO = {
|
||||
skill?: string;
|
||||
id?: number;
|
||||
};
|
||||
|
||||
export type EmployeeRequestDTO = {
|
||||
lastName: string;
|
||||
firstName: string;
|
||||
street: string;
|
||||
postcode: string;
|
||||
city: string;
|
||||
phone: string;
|
||||
skillSet?: Array<(number)>;
|
||||
};
|
||||
|
||||
export type EmployeeNameAndSkillDataDTO = {
|
||||
id?: number;
|
||||
lastName?: string;
|
||||
firstName?: string;
|
||||
skillSet?: Array<QualificationGetDTO>;
|
||||
};
|
||||
|
||||
export type EmployeeNameDataDTO = {
|
||||
id?: number;
|
||||
lastName?: string;
|
||||
firstName?: string;
|
||||
};
|
||||
|
||||
export type EmployeesForAQualificationDTO = {
|
||||
qualification?: QualificationGetDTO;
|
||||
employees?: Array<EmployeeNameDataDTO>;
|
||||
};
|
||||
|
||||
export type UpdateQualificationData = {
|
||||
id: number;
|
||||
requestBody: QualificationPostDTO;
|
||||
};
|
||||
|
||||
export type UpdateQualificationResponse = (QualificationPostDTO);
|
||||
|
||||
export type DeleteQualificationByDesignationData = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type DeleteQualificationByDesignationResponse = (void);
|
||||
|
||||
export type FindAllResponse = (QualificationGetDTO);
|
||||
|
||||
export type CreateQualificationData = {
|
||||
requestBody: QualificationPostDTO;
|
||||
};
|
||||
|
||||
export type CreateQualificationResponse = (QualificationPostDTO);
|
||||
|
||||
export type FindAllEmployeesByQualificationData = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type FindAllEmployeesByQualificationResponse = (EmployeesForAQualificationDTO);
|
||||
|
||||
export type FindByIdData = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type FindByIdResponse = (EmployeeResponseDTO);
|
||||
|
||||
export type UpdateEmployeeData = {
|
||||
id: number;
|
||||
requestBody: EmployeeRequestPutDTO;
|
||||
};
|
||||
|
||||
export type UpdateEmployeeResponse = (EmployeeResponseDTO);
|
||||
|
||||
export type DeleteCustomerData = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type DeleteCustomerResponse = (void);
|
||||
|
||||
export type PatchEmployeeData = {
|
||||
id: number;
|
||||
requestBody: EmployeeRequestPutDTO;
|
||||
};
|
||||
|
||||
export type PatchEmployeeResponse = (EmployeeResponseDTO);
|
||||
|
||||
export type FindAll1Response = (Array<EmployeeResponseDTO>);
|
||||
|
||||
export type CreateEmployeeData = {
|
||||
requestBody: EmployeeRequestDTO;
|
||||
};
|
||||
|
||||
export type CreateEmployeeResponse = (EmployeeResponseDTO);
|
||||
|
||||
export type FindAllQualificationOfAemployeeByIdData = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type FindAllQualificationOfAemployeeByIdResponse = (EmployeeNameAndSkillDataDTO);
|
||||
|
||||
export type AddQualificationToEmployeeByIdData = {
|
||||
id: number;
|
||||
requestBody: QualificationPostDTO;
|
||||
};
|
||||
|
||||
export type AddQualificationToEmployeeByIdResponse = (EmployeeNameAndSkillDataDTO);
|
||||
|
||||
export type RemoveQualificationFromEmployeeData = {
|
||||
eid: number;
|
||||
qid: number;
|
||||
};
|
||||
|
||||
export type RemoveQualificationFromEmployeeResponse = (EmployeeNameAndSkillDataDTO);
|
0
src/app/types/.gitkeep
Normal file
0
src/app/types/.gitkeep
Normal file
13
src/index.html
Normal file
13
src/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>A</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
7
src/main.ts
Normal file
7
src/main.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
1
src/styles.scss
Normal file
1
src/styles.scss
Normal file
|
@ -0,0 +1 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
15
tsconfig.app.json
Normal file
15
tsconfig.app.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
46
tsconfig.json
Normal file
46
tsconfig.json
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
"@app/*": [
|
||||
"./src/app/*"
|
||||
],
|
||||
"@dt/*": [
|
||||
"./src/app/types/*"
|
||||
],
|
||||
"@core/*": [
|
||||
"./src/app/core/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
15
tsconfig.spec.json
Normal file
15
tsconfig.spec.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue