chore: move projects to src dir

This commit is contained in:
Developer 02
2025-08-05 19:01:02 +02:00
parent cb446ef25a
commit 448ddf7eba
293 changed files with 2342 additions and 15917 deletions

View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.3",
"commands": [
"dotnet-ef"
]
}
}
}

View File

@@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-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

View File

@@ -0,0 +1,27 @@
# UserManagerUi
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.8.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,121 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"user_manager_ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/user_manager_ui",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss",
"src/dd-mat-theme.scss"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
],
"server": "src/main.server.ts",
"prerender": true,
"ssr": false
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "3mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "2.5mb"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "user_manager_ui:build:production"
},
"development": {
"proxyConfig": "proxy.conf.json",
"buildTarget": "user_manager_ui:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "user_manager_ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/dd-mat-theme.scss"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
}
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
{
"name": "user-manager-ui",
"version": "4.0.0",
"scripts": {
"ng": "ng",
"start:ssl": "ng serve --ssl -o",
"start": "ng serve -o",
"build": "ng build --configuration production",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:user_manager_ui": "node dist/user_manager_ui/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.0",
"@angular/cdk": "^17.3.10",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/material": "^17.3.10",
"@angular/material-moment-adapter": "^17.3.10",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/platform-server": "^17.3.0",
"@angular/router": "^17.3.0",
"@angular/ssr": "^17.3.8",
"@generic-ui/fabric": "^0.21.0",
"@generic-ui/hermes": "^0.21.0",
"@generic-ui/ngx-grid": "^0.21.0",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"express": "^4.18.2",
"rxjs": "~7.8.0",
"sweetalert2": "^11.12.3",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.8",
"@angular/cli": "^17.3.8",
"@angular/compiler-cli": "^17.3.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"@types/uuid": "^10.0.0",
"jasmine-core": "~5.1.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",
"typescript": "~5.4.2"
}
}

View File

@@ -0,0 +1,7 @@
{
"/api": {
"target": "https://localhost:7052",
"secure": false,
"changeOrigin": true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,56 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();

View File

@@ -0,0 +1,4 @@
<app-nav-menu></app-nav-menu>
<main class="container-fluid">
<router-outlet></router-outlet>
</main>

View File

@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'user_manager_ui' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('user_manager_ui');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, user_manager_ui');
});
});

View File

@@ -0,0 +1,53 @@
import { ChangeDetectionStrategy, Component, HostListener, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { NavMenuComponent } from './components/nav-menu/nav-menu.component'
import { TransferService } from './services/button/transfer.service';
import { UpdateService } from './services/button/update.service';
import { RefreshService } from './services/button/refresh.service';
import { DeletionService } from './services/button/deletion.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NavMenuComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent {
title = 'app';
protected transferService: TransferService = inject(TransferService)
protected updateService: UpdateService = inject(UpdateService)
protected refreshService: RefreshService = inject(RefreshService)
protected deletionService: DeletionService = inject(DeletionService)
@HostListener('window:keydown.control.s', ['$event'])
protected handleCtrlS(event: KeyboardEvent) {
event.preventDefault();
this.updateService.executeAllAsync().then(() => this.refreshService.executeAll());
}
@HostListener('window:keydown.control.r', ['$event'])
protected handleCtrlR(event: KeyboardEvent) {
event.preventDefault();
this.refreshService.executeAll();
}
@HostListener('window:keydown.delete', ['$event'])
protected handleDelete(event: KeyboardEvent) {
event.preventDefault();
this.deletionService.executeAll();
}
@HostListener('window:keydown.control.space', ['$event'])
protected handleCtrlSpace(event: KeyboardEvent) {
event.preventDefault();
this.transferService.executeAll();
}
@HostListener('window:keydown.control.l', ['$event'])
protected handleCtrlL(event: KeyboardEvent) {
event.preventDefault();
this.updateService.toggleEditability()
}
}

View File

@@ -0,0 +1,11 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering()
]
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

View File

@@ -0,0 +1,33 @@
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { APP_BASE_HREF } from '@angular/common';
import { UrlService } from './services/api/url.service';
import { API_URL } from './tokens';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideClientHydration(),
provideAnimationsAsync(),
provideHttpClient(withFetch()),
{
provide: APP_BASE_HREF,
useFactory: (urlService: UrlService) => urlService.getBaseHref(),
deps: [UrlService]
},
{
provide: API_URL,
useFactory: (urlService: UrlService) => urlService.getApiUrl(),
deps: [UrlService]
},
{
provide: API_URL,
useFactory: (urlService: UrlService) => urlService.getApiUrl(),
deps: [UrlService]
}
]
};

View File

@@ -0,0 +1,17 @@
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { AuthGuard } from './auth/auth.guard';
import { UserComponent } from './pages/user/user.component';
import { GroupComponent } from './pages/group/group.component';
import { ModuleComponent } from './pages/module/module.component';
import { UserAssignmentComponent } from './pages/user-assignment/user-assignment.component';
import { UserRepresentationComponent } from './pages/user-representation/user-representation.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'user-table', component: UserComponent, canActivate: [AuthGuard] },
{ path: 'group-table', component: GroupComponent, canActivate: [AuthGuard] },
{ path: 'module-table', component: ModuleComponent, canActivate: [AuthGuard] },
{ path: 'user-assignment', component: UserAssignmentComponent, canActivate: [AuthGuard] },
{ path: 'user-representation', component: UserRepresentationComponent, canActivate: [AuthGuard] }
];

View File

@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';
import { authGuard } from './auth.guard';
describe('authGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthenticationService } from '../services/api/authentication.service'; // Adjust the path as necessary
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { LoginComponent } from '../components/login/login.component';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(
public dialog: MatDialog,
public authService: AuthenticationService,
private router: Router
) {}
async canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> {
try {
const isAuthenticated = await this.authService.isAuthenticated();
if (!isAuthenticated) {
this.router.navigate(['/']);
}
return isAuthenticated;
} catch (error) {
return false;
}
}
openLogin(): MatDialogRef<LoginComponent, any> {
const dialogRef = this.dialog.open(LoginComponent, {
width: "35vw",
data: {
afterLogin: () => {
dialogRef.close();
}
}
});
return dialogRef;
}
}

View File

@@ -0,0 +1,9 @@
<div class="card">
<div *ngIf="!hideTitle" class="card-header">
<span *ngIf="title">{{ title }}</span>
<ng-content select=".header-content"></ng-content>
</div>
<div [ngClass]="'card-body p-' + padding">
<ng-content select=".body-content"></ng-content>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CardComponent } from './card.component';
describe('CardComponent', () => {
let component: CardComponent;
let fixture: ComponentFixture<CardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CardComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'card',
standalone: true,
imports: [CommonModule],
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class CardComponent {
@Input() title: string | null = null;
@Input() col: number | null = null;
@Input() padding: number = 0;
@Input() hideTitle: boolean = false;
}

View File

@@ -0,0 +1,8 @@
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.hide {
display: none;
}

View File

@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check2" viewBox="0 0 16 16">
<path
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</symbol>
<symbol id="circle-half" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
</symbol>
<symbol id="moon-stars-fill" viewBox="0 0 16 16">
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
<path
d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
</symbol>
<symbol id="sun-fill" viewBox="0 0 16 16">
<path
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
</symbol>
</svg>
<div class="dropdown bd-mode-toggle">
<button class="btn py-2 dropdown-toggle d-flex align-items-center" id="bd-theme" type="button" aria-expanded="false"
data-bs-toggle="dropdown" aria-label="Toggle theme (auto)">
<svg class="bi my-1 theme-icon-active" width="1em" height="1em" viewBox="0 0 16 16">
<use href="#circle-half" [class.hide]="theme !== Themes.Auto"></use>
<use href="#sun-fill" [class.hide]="theme !== Themes.Light"></use>
<use href="#moon-stars-fill" [class.hide]="theme !== Themes.Dark
"></use>
</svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
<li>
<button type="button" (click)="onClick(Themes.Light)"
[ngClass]="{'active': theme == Themes.Light, 'dropdown-item d-flex align-items-center': true}"
data-bs-theme-value="light" aria-pressed="false">
<svg class="bi me-2 opacity-50" width="1em" height="1em">
<use href="#sun-fill"></use>
</svg>
Light
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button type="button" (click)="onClick(Themes.Dark)"
[ngClass]="{'active': theme == Themes.Dark, 'dropdown-item d-flex align-items-center': true}"
data-bs-theme-value="dark" aria-pressed="false">
<svg class="bi me-2 opacity-50" width="1em" height="1em">
<use href="#moon-stars-fill"></use>
</svg>
Dark
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button type="button" (click)="onClick(Themes.Auto)"
[ngClass]="{'active': theme == Themes.Auto, 'dropdown-item d-flex align-items-center': true}"
data-bs-theme-value="auto" aria-pressed="true">
<svg class="bi me-2 opacity-50" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
Auto
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
</ul>
</div>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ColorModeBttnComponent } from './color-mode-bttn.component';
describe('ColorModeBttnComponent', () => {
let component: ColorModeBttnComponent;
let fixture: ComponentFixture<ColorModeBttnComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ColorModeBttnComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ColorModeBttnComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,29 @@
import { Component, OnInit } from '@angular/core';
import { ColorModeService, GetLocalTheme, Theme } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common'
@Component({
standalone: true,
imports: [CommonModule],
selector: 'app-color-mode-bttn',
templateUrl: './color-mode-bttn.component.html',
styleUrl: './color-mode-bttn.component.css'
})
export class ColorModeBttnComponent implements OnInit {
constructor(private cModeService: ColorModeService) {
this.theme = GetLocalTheme();
}
ngOnInit(): void {
this.cModeService.updateTheme();
}
readonly Themes = Theme;
theme: Theme;
onClick(theme: Theme) {
this.theme = theme;
let theTheme: Theme = theme;
this.cModeService.setTheme(theTheme);
}
}

View File

@@ -0,0 +1,50 @@
<table cdkDrag mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"
[aria-label]="checkboxLabel()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let row" [draggable]="true">
<mat-checkbox cdkDrag (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)">
</mat-checkbox>
</td>
</ng-container>
<!-- Position Column -->
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef [draggable]="true"> No. </th>
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef> Weight </th>
<td mat-cell *matCellDef="let element"> {{element.weight}} </td>
</ng-container>
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef> Symbol </th>
<td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
(click)="selection.toggle(row)">
</tr>
</table>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatTableComponent } from './mat-table.component';
describe('MatTableComponent', () => {
let component: MatTableComponent;
let fixture: ComponentFixture<MatTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MatTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MatTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,66 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {SelectionModel} from '@angular/cdk/collections';
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
import {MatCheckboxModule} from '@angular/material/checkbox';
export interface PeriodicElement {
name: string;
position: number;
weight: number;
symbol: string;
}
const ELEMENT_DATA: PeriodicElement[] = [
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];
/**
* @title Table with selection
*/
@Component({
selector: 'mt-table',
standalone: true,
imports: [CommonModule, MatTableModule, MatCheckboxModule],
templateUrl: './mat-table.component.html',
styleUrl: './mat-table.component.css'
})
export class MatTableComponent {
displayedColumns: string[] = ['select', 'position', 'name', 'weight', 'symbol'];
dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
selection = new SelectionModel<PeriodicElement>(true, []);
/** Whether the number of selected elements matches the total number of rows. */
isAllSelected() {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
/** Selects all rows if they are not all selected; otherwise clear selection. */
toggleAllRows() {
if (this.isAllSelected()) {
this.selection.clear();
return;
}
this.selection.select(...this.dataSource.data);
}
/** The label for the checkbox on the passed row */
checkboxLabel(row?: PeriodicElement): string {
if (!row) {
return `${this.isAllSelected() ? 'deselect' : 'select'} all`;
}
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`;
}
}

View File

@@ -0,0 +1,14 @@
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs"
[routerLinkActive]="['link-active']">
<li *ngFor="let item of tabItems" class="nav-item">
<a [ngClass]="item.title == activeTabTitle ? 'nav-link active' : 'nav-link'" aria-current="true"
[routerLink]="[item.routerLink]">{{ item.title }}</a>
</li>
</ul>
</div>
<div [ngClass]="'card-body p-' + padding">
<ng-content select=".body-content"></ng-content>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TabCardComponent } from './tab-card.component';
describe('TabCardComponent', () => {
let component: TabCardComponent;
let fixture: ComponentFixture<TabCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TabCardComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TabCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Component, Input, input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CardComponent } from '../card/card.component';
import { RouterModule } from '@angular/router';
@Component({
selector: 'tab-card',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './tab-card.component.html',
styleUrl: './tab-card.component.css'
})
export class TabCardComponent extends CardComponent {
@Input() tabItems: TabItem[] = []
@Input() activeTabTitle: string | null = null;
}
export interface TabItem {
title: string
routerLink: string
}

View File

@@ -0,0 +1,39 @@
<mat-tab-group>
<mat-tab label="Erstellen">
<div class="container my-3">
<div class="row">
<div [ngClass]="formFieldBSClass">
<mat-form-field>
<mat-label>Gruppenname</mat-label>
<input matInput [formControl]="groupname" (blur)="updateErrorMessage()" required />
@if (groupname.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<div [ngClass]="formFieldBSClass">
<div [ngClass]="buttonBSClass">
<button mat-fab extended (click)="create()">
<mat-icon>playlist_add</mat-icon>
Erstellen
</button>
</div>
</div>
</div>
<div class="row">
<div [ngClass]="formFieldBSClass">
<mat-checkbox [formControl]="active" [disabled]="true">Active</mat-checkbox>
</div>
<div [ngClass]="formFieldBSClass">
<button mat-fab extended (click)="delete()">
<mat-icon>delete</mat-icon>
Löschen
</button>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="Import über AD">
<app-group-dir-import></app-group-dir-import>
</mat-tab>
</mat-tab-group>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupFormComponent } from './group-form.component';
describe('GroupFormComponent', () => {
let component: GroupFormComponent;
let fixture: ComponentFixture<GroupFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GroupFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GroupFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,78 @@
import { ChangeDetectionStrategy, Component, model, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { merge } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { CommonModule } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { UserService } from '../../../services/api/user.service';
import { RefreshService } from '../../../services/button/refresh.service';
import { GroupDirImportComponent } from "../../group-dir-import/group-dir-import.component";
import { GroupService } from '../../../services/api/group.service';
import Swal from 'sweetalert2';
@Component({
selector: 'app-group-form',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule, MatIconModule, MatButtonModule, MatCheckboxModule, CommonModule, MatTabsModule, GroupDirImportComponent],
templateUrl: './group-form.component.html',
styleUrl: './group-form.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupFormComponent {
readonly groupname = new FormControl('', [Validators.required]);
readonly active = new FormControl<boolean>(true);
errorMessage = signal('');
public readonly formFieldBSClass: string = "col d-flex justify-content-center mx-1 my-2"
public readonly buttonBSClass: string = "d-flex justify-content-center mx-1 my-2"
//public readonly checkBoxBSClass: (colCount: number | undefined) => string = (colCount: number = 2) => `col-${colCount} d-flex justify-content-left mx-1 my-2`
readonly checked = model(true);
constructor(private uService: UserService, private rService: RefreshService, private gService: GroupService) {
merge(
this.groupname.statusChanges, this.groupname.valueChanges)
.pipe(takeUntilDestroyed())
.subscribe(() => this.updateErrorMessage());
}
updateErrorMessage() {
if (this.groupname.hasError('required')) {
this.errorMessage.set('Wert eingeben');
} else {
this.errorMessage.set('');
}
}
create() {
if (this.groupname.valid) {
this.gService.create({
name: this.groupname.value!,
adSync: false,
internal: true,
active: this.active.value!
}).subscribe({
next: () => {
this.delete()
this.rService.executeAll();
Swal.fire({
title: "Vorgang erfolgreich!",
text: "Gruppe erfolgreich erstellt!",
icon: "success"
});
}
})
}
}
delete() {
this.groupname.setValue('')
this.active.setValue(true)
}
}

View File

@@ -0,0 +1,64 @@
<div class="dd-container">
<!-- id, name -->
<div class="dd-row input-row">
<mat-form-field class="w20p">
<mat-label>Id</mat-label>
<input matInput readonly [value]="group.id" />
</mat-form-field>
<mat-form-field class="w80p">
<mat-label>Gruppe</mat-label>
<input matInput [formControl]="name" />
</mat-form-field>
</div>
<!-- comment -->
<div class="dd-row input-row">
<mat-form-field>
<mat-label>Kommentar</mat-label>
<textarea matInput [formControl]="comment"></textarea>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<!-- active, internal, async -->
<div class="dd-row input-row">
<mat-slide-toggle [(ngModel)]="group.active">
Aktiv
</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.internal" disabled>
Interne Gruppe
</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="group.adSync" disabled>
Mit Active Directory
</mat-slide-toggle>
</div>
<!-- addedWho, addedWhen, changedWho and changedWhen -->
<div class="dd-row input-row">
<mat-form-field>
<mat-label>Hinzugefügt wer</mat-label>
<input matInput readonly [value]="group.addedWho" />
</mat-form-field>
<mat-form-field>
<mat-label>Hinzugefügt wann</mat-label>
<input matInput readonly [value]="group.addedWhen | date:'dd.MM.yyyy'" />
</mat-form-field>
<mat-form-field>
<mat-label>Geändert wer</mat-label>
<input matInput readonly [value]="group.changedWho" />
</mat-form-field>
<mat-form-field>
<mat-label>Geändert wann</mat-label>
<input matInput readonly [value]="group.changedWhen | date:'dd.MM.yyyy'" />
</mat-form-field>
</div>
<mat-divider></mat-divider>
<!-- save-button, delete-button -->
<div class="dd-row button-row">
<button mat-fab extended (click)="update()">
<mat-icon>save</mat-icon>
Speichern
</button>
<button mat-fab extended (click)="delete()">
<mat-icon>delete</mat-icon>
Löschen
</button>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupUpdateFormComponent } from './group-update-form.component';
describe('GroupUpdateFormComponent', () => {
let component: GroupUpdateFormComponent;
let fixture: ComponentFixture<GroupUpdateFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GroupUpdateFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GroupUpdateFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,104 @@
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { Group } from '../../../services/api/api-models';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CommonModule } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { GroupService } from '../../../services/api/group.service';
import { RefreshService } from '../../../services/button/refresh.service';
import Swal from 'sweetalert2';
import { MatSelectModule } from '@angular/material/select';
import { env } from '../../../../environments/environment'
import { MatDividerModule } from '@angular/material/divider';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
@Component({
selector: 'app-group-update-form',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule, MatIconModule, MatButtonModule, CommonModule, MatTabsModule, MatSelectModule, MatDividerModule, MatCheckboxModule, MatSlideToggleModule],
templateUrl: './group-update-form.component.html',
styleUrl: './group-update-form.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupUpdateFormComponent {
readonly dialogRef: MatDialogRef<GroupUpdateFormComponent> = inject(MatDialogRef<GroupUpdateFormComponent>);
readonly group: Group = inject(MAT_DIALOG_DATA);
get allowedDateFormats(): Array<{ value: string, name: string }> {
return env.constants.date_formats;
}
get allowedLanguages(): Array<{ value: string, name: string }> {
return env.constants.languages;
}
readonly name = new FormControl(this.group.name);
readonly comment = new FormControl(this.group.comment);
mailErrorMessage = signal('');
errorMessage = signal('');
constructor(private uService: GroupService, private rService: RefreshService) {
}
update() {
this.group.name = this.name.value!;
this.group.comment = this.comment.value!;
this.uService.update(this.group).subscribe({
next: () => {
this.rService.executeAll();
},
error: err => {
console.error(err)
Swal.fire({
title: "Interner Dienstfehler",
text: "Bitte wenden Sie sich an das IT-Team, um den Fehler zu beheben.",
icon: "error"
});
}
})
}
delete() {
Swal.fire({
text: "Sind Sie sicher, dass Sie diesen Datensatz löschen wollen?",
icon: "question",
showDenyButton: true,
confirmButtonText: "Ja",
denyButtonText: `Nein`
}).then((result) => {
if (result.isConfirmed) {
if (this.group.id) {
this.uService.delete(this.group.id).subscribe({
next: () => {
this.rService.executeAll();
this.dialogRef.close();
},
error: err => {
Swal.fire({
title: "Interner Dienstfehler",
text: "Bitte wenden Sie sich an das IT-Team, um den Fehler zu beheben.",
icon: "error"
});
},
})
}
else
Swal.fire({
title: "Ein unerwarteter Fehler",
text: "Die Benutzer-ID existiert nicht (Nullwert).",
icon: "error"
});
}
});
}
}

View File

@@ -0,0 +1,28 @@
<div class="dd-container">
<div class="dd-row input-row">
<mat-form-field class="w40p">
<mat-label>Geben Sie einen Datumsbereich ein</mat-label>
<mat-date-range-input [formGroup]="range" [rangePicker]="picker" [disabled]="termless">
<input matStartDate formControlName="start" placeholder="Start date">
<input matEndDate formControlName="end" placeholder="End date">
</mat-date-range-input>
<mat-hint>{{dateFormatString()}}</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
@if (range.controls.start.hasError('matStartDateInvalid')) {
<mat-error>Ungültiges Startdatum</mat-error>
}
@if (range.controls.end.hasError('matEndDateInvalid')) {
<mat-error>Ungültiges Enddatum</mat-error>
}
</mat-form-field>
<mat-slide-toggle [(ngModel)]="termless" class="w20p">
Unbefristet
</mat-slide-toggle>
<button mat-fab extended (click)="create()" class="w20p">
<mat-icon>playlist_add</mat-icon>
Erstellen
</button>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.dd-row mat-form-field {
margin: 1rem 0rem 0rem 0rem;
}
.dd-row mat-slide-toggle {
margin: 1rem 0rem 1rem 0rem;
padding: 0;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RepCreateFormComponent } from './rep-create-form.component';
describe('RepCreateFormComponent', () => {
let component: RepCreateFormComponent;
let fixture: ComponentFixture<RepCreateFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RepCreateFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RepCreateFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,98 @@
import { Component, inject, ChangeDetectionStrategy, signal, OnInit, computed } from '@angular/core';
import { UserRep } from '../../../services/api/api-models';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserRepService } from '../../../services/api/user-representation.service';
import { GroupUpdateFormComponent } from '../group-update-form/group-update-form.component';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DateAdapter, MAT_DATE_LOCALE, provideNativeDateAdapter } from '@angular/material/core';
import { MatDatepickerIntl, MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import Swal from 'sweetalert2';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { provideMomentDateAdapter } from '@angular/material-moment-adapter';
import { MatInputModule } from '@angular/material/input';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import 'moment/locale/de';
@Component({
selector: 'app-rep-create-form',
standalone: true,
providers: [
provideNativeDateAdapter(),
{ provide: MAT_DATE_LOCALE, useValue: 'de-DE' },
provideMomentDateAdapter()
],
imports: [MatFormFieldModule, MatDatepickerModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatIconModule, MatInputModule, MatSlideToggleModule],
templateUrl: './rep-create-form.component.html',
styleUrl: './rep-create-form.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RepCreateFormComponent implements OnInit {
readonly userRep: UserRep;
readonly afterCreation: (any: any) => any;
readonly userRepService: UserRepService = inject(UserRepService);
readonly dialogRef: MatDialogRef<GroupUpdateFormComponent> = inject(MatDialogRef<GroupUpdateFormComponent>);
readonly range = new FormGroup({
start: new FormControl<Date | null>(null),
end: new FormControl<Date | null>(null),
});
private readonly _locale = signal(inject<unknown>(MAT_DATE_LOCALE));
private readonly _adapter = inject<DateAdapter<unknown, unknown>>(DateAdapter);
private readonly _intl = inject(MatDatepickerIntl);
termless: boolean = false;
constructor() {
const dialogData: { userRep: UserRep, afterCreation: (any: any) => any } = inject(MAT_DIALOG_DATA)
this.userRep = dialogData.userRep;
this.afterCreation = dialogData.afterCreation;
}
ngOnInit(): void {
//update close button label
this._intl.closeCalendarLabel = 'Kalender schließen';
this._intl.changes.next();
//set local
this._locale.set('de');
this._adapter.setLocale(this._locale());
}
readonly dateFormatString = computed(() => {
if (this._locale() === 'de-DE') {
return 'dd.mm.yyyy';
}
return '';
});
create() {
const validFrom = this.range.value.start;
const validTo = this.range.value.end;
if ((!validFrom || !validTo) && !this.termless) {
Swal.fire({
icon: "error",
title: "Oops...",
text: "Bitte geben Sie einen gültigen Datumsbereich ein oder wählen Sie unbefristet!",
});
return;
}
if (!this.termless) {
this.userRep.validFrom = validFrom!;
this.userRep.validTo = validTo!;
}
this.userRepService.create(this.userRep).subscribe({
next: (res) => {
this.afterCreation({ successful: res });
this.dialogRef.close();
},
error: (error) => {
this.afterCreation({ error: error });
}
});
}
}

View File

@@ -0,0 +1,79 @@
<mat-tab-group>
<mat-tab label="Erstellen">
<div class="container my-3">
<div class="row">
<div [ngClass]="formFieldBSClass">
<mat-form-field>
<mat-label>Benutzername</mat-label>
<input
matInput
[formControl]="username"
(blur)="updateErrorMessage()"
required />
@if (email.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<div [ngClass]="formFieldBSClass">
<mat-form-field>
<mat-label>E-Mail</mat-label>
<input
matInput placeholder="user@example.com"
[formControl]="email"
(blur)="updateMailErrorMessage()"
required />
@if (email.invalid) {
<mat-error>{{mailErrorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<div [ngClass]="formFieldBSClass">
<div [ngClass]="buttonBSClass">
<button mat-fab extended (click)="create()">
<mat-icon>playlist_add</mat-icon>
Erstellen
</button>
</div>
</div>
</div>
<div class="row">
<div [ngClass]="formFieldBSClass">
<mat-form-field>
<mat-label>Vorname</mat-label>
<input
matInput
[formControl]="name"
(blur)="updateErrorMessage()"
required />
@if (email.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<div [ngClass]="formFieldBSClass">
<mat-form-field>
<mat-label>Nachname</mat-label>
<input
matInput
[formControl]="surname"
(blur)="updateErrorMessage()"
required />
@if (email.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<div [ngClass]="formFieldBSClass">
<button mat-fab extended (click)="delete()">
<mat-icon>delete</mat-icon>
Löschen
</button>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="Import über AD">
<app-user-group-dir-import></app-user-group-dir-import>
</mat-tab>
</mat-tab-group>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserFormComponent } from './user-form.component';
describe('UserFormComponent', () => {
let component: UserFormComponent;
let fixture: ComponentFixture<UserFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,95 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { merge } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CommonModule } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { UserGroupDirImportComponent } from "../../user-group-dir-import/user-group-dir-import.component";
import { UserService } from '../../../services/api/user.service';
import { RefreshService } from '../../../services/button/refresh.service';
import Swal from 'sweetalert2';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule, MatIconModule, MatButtonModule, CommonModule, MatTabsModule, UserGroupDirImportComponent],
templateUrl: './user-create-form.component.html',
styleUrl: './user-create-form.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCreateFormComponent {
readonly email = new FormControl('', [Validators.required, Validators.email]);
readonly username = new FormControl('', [Validators.required]);
readonly name = new FormControl('', [Validators.required]);
readonly surname = new FormControl('', [Validators.required]);
mailErrorMessage = signal('');
errorMessage = signal('');
public readonly formFieldBSClass: string = "col d-flex justify-content-center mx-1 my-2"
public readonly buttonBSClass: string = "d-flex justify-content-center mx-1 my-2"
constructor(private uService: UserService, private rService: RefreshService) {
merge(
this.email.statusChanges, this.email.valueChanges)
.pipe(takeUntilDestroyed())
.subscribe(() => this.updateMailErrorMessage());
merge(
this.username.statusChanges, this.username.valueChanges,
this.name.statusChanges, this.name.valueChanges,
this.surname.statusChanges, this.surname.valueChanges)
.pipe(takeUntilDestroyed())
.subscribe(() => this.updateErrorMessage());
}
updateMailErrorMessage() {
if (this.email.hasError('required')) {
this.mailErrorMessage.set('Wert eingeben');
} else if (this.email.hasError('email')) {
this.mailErrorMessage.set('Ungültige E-Mail');
} else {
this.mailErrorMessage.set('');
}
}
updateErrorMessage() {
if (this.email.hasError('required')) {
this.errorMessage.set('Wert eingeben');
} else {
this.errorMessage.set('');
}
}
create() {
if (this.email.valid && this.username.valid && this.name.valid && this.surname.valid) {
this.uService.create({
email: this.email.value!,
prename: this.name.value!,
username: this.username.value!,
name: this.surname.value!,
}).subscribe({
next: () => {
this.delete()
this.rService.executeAll();
Swal.fire({
title: "Vorgang erfolgreich!",
text: "Benutzer erfolgreich erstellt!",
icon: "success"
});
}
})
}
}
delete(){
this.email.setValue('')
this.username.setValue('')
this.name.setValue('')
this.surname.setValue('')
}
}

View File

@@ -0,0 +1,103 @@
<div class="dd-container">
<!-- id, username, e-mail -->
<div class="dd-row input-row">
<mat-form-field class="w10p">
<mat-label>Id</mat-label>
<input matInput readonly [value]="user.id" />
</mat-form-field>
<mat-form-field class="w30p">
<mat-label>Benutzername</mat-label>
<input matInput [formControl]="username" (blur)="updateErrorMessage()" required />
@if (email.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
<mat-form-field class="w60p">
<mat-label>E-Mail</mat-label>
<input matInput placeholder="user@example.com" [formControl]="email" (blur)="updateMailErrorMessage()"
required />
@if (email.invalid) {
<mat-error>{{mailErrorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<!-- firstname, surname -->
<div class="dd-row input-row">
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput [formControl]="name" (blur)="updateErrorMessage()" required />
@if (email.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput [formControl]="surname" (blur)="updateErrorMessage()" required />
@if (email.invalid) {
<mat-error>{{errorMessage()}}</mat-error>
}
</mat-form-field>
</div>
<!-- shortname -->
<div class="dd-row input-row">
<mat-form-field>
<mat-label>Kürzel</mat-label>
<input matInput [formControl]="shortname" />
</mat-form-field>
<mat-form-field>
<mat-label>Datumsformat</mat-label>
<mat-select [(value)]="user.dateFormat" [(ngModel)]="user.dateFormat">
@for (format of allowedDateFormats; track format) {
<mat-option [value]="format.value">{{format.name}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Sprache</mat-label>
<mat-select [(value)]="user.language" [(ngModel)]="user.language">
@for (language of allowedLanguages; track language) {
<mat-option [value]="language.value">{{language.name}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<!-- comment -->
<div class="dd-row input-row">
<mat-form-field>
<mat-label>Kommentar</mat-label>
<textarea matInput [formControl]="comment"></textarea>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<!-- addedWho, addedWhen, changedWho and changedWhen -->
<div class="dd-row input-row">
<mat-form-field>
<mat-label>Hinzugefügt wer</mat-label>
<input matInput readonly [value]="user.addedWho" />
</mat-form-field>
<mat-form-field>
<mat-label>Hinzugefügt wann</mat-label>
<input matInput readonly [value]="user.addedWhen | date:'dd.MM.yyyy'" />
</mat-form-field>
<mat-form-field>
<mat-label>Geändert wer</mat-label>
<input matInput readonly [value]="user.changedWho" />
</mat-form-field>
<mat-form-field>
<mat-label>Geändert wann</mat-label>
<input matInput readonly [value]="user.changedWhen | date:'dd.MM.yyyy'" />
</mat-form-field>
</div>
<mat-divider></mat-divider>
<!-- save-button, delete-button -->
<div class="dd-row button-row">
<button mat-fab extended (click)="update()">
<mat-icon>save</mat-icon>
Speichern
</button>
<button mat-fab extended (click)="delete()">
<mat-icon>delete</mat-icon>
Löschen
</button>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserUpdateFormComponent } from './user-update-form.component';
describe('UserUpdateFormComponent', () => {
let component: UserUpdateFormComponent;
let fixture: ComponentFixture<UserUpdateFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserUpdateFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserUpdateFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,142 @@
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { User } from '../../../services/api/api-models';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { merge } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CommonModule } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { UserService } from '../../../services/api/user.service';
import { RefreshService } from '../../../services/button/refresh.service';
import Swal from 'sweetalert2';
import { MatSelectModule } from '@angular/material/select';
import { env } from '../../../../environments/environment'
import {MatDividerModule} from '@angular/material/divider';
@Component({
selector: 'app-user-update-form',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule, MatIconModule, MatButtonModule, CommonModule, MatTabsModule, MatSelectModule, MatDividerModule],
templateUrl: './user-update-form.component.html',
styleUrl: './user-update-form.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserUpdateFormComponent {
readonly dialogRef: MatDialogRef<UserUpdateFormComponent> = inject(MatDialogRef<UserUpdateFormComponent>);
readonly user: User = inject(MAT_DIALOG_DATA);
get allowedDateFormats(): Array<{ value: string, name: string }> {
return env.constants.date_formats;
}
get allowedLanguages(): Array<{ value: string, name: string }> {
return env.constants.languages;
}
readonly username = new FormControl(this.user.username, [Validators.required]);
readonly email = new FormControl(this.user.email, [Validators.required, Validators.email]);
readonly name = new FormControl(this.user.prename, [Validators.required]);
readonly surname = new FormControl(this.user.name, [Validators.required]);
readonly shortname = new FormControl(this.user.shortname);
readonly comment = new FormControl(this.user.comment);
mailErrorMessage = signal('');
errorMessage = signal('');
constructor(private uService: UserService, private rService: RefreshService) {
merge(
this.email.statusChanges, this.email.valueChanges)
.pipe(takeUntilDestroyed())
.subscribe(() => this.updateMailErrorMessage());
merge(
this.username.statusChanges, this.username.valueChanges,
this.name.statusChanges, this.name.valueChanges,
this.surname.statusChanges, this.surname.valueChanges)
.pipe(takeUntilDestroyed())
.subscribe(() => this.updateErrorMessage());
}
updateMailErrorMessage() {
if (this.email.hasError('required')) {
this.mailErrorMessage.set('Wert eingeben');
} else if (this.email.hasError('email')) {
this.mailErrorMessage.set('Ungültige E-Mail');
} else {
this.mailErrorMessage.set('');
}
}
updateErrorMessage() {
if (this.email.hasError('required')) {
this.errorMessage.set('Wert eingeben');
} else {
this.errorMessage.set('');
}
}
update() {
if (this.email.valid && this.username.valid && this.name.valid && this.surname.valid) {
this.user.email = this.email.value!;
this.user.prename = this.name.value!;
this.user.username = this.username.value!;
this.user.name = this.surname.value!;
this.user.shortname = this.shortname.value!;
this.user.comment = this.comment.value!;
this.uService.update(this.user).subscribe({
next: () => {
this.rService.executeAll();
},
error: err => {
console.error(err)
Swal.fire({
title: "Interner Dienstfehler",
text: "Bitte wenden Sie sich an das IT-Team, um den Fehler zu beheben.",
icon: "error"
});
}
})
}
}
delete() {
Swal.fire({
text: "Sind Sie sicher, dass Sie diesen Datensatz löschen wollen?",
icon: "question",
showDenyButton: true,
confirmButtonText: "Ja",
denyButtonText: `Nein`
}).then((result) => {
if (result.isConfirmed) {
if (this.user.id) {
this.uService.delete(this.user.id).subscribe({
next: () => {
this.rService.executeAll();
this.dialogRef.close();
},
error: err => {
Swal.fire({
title: "Interner Dienstfehler",
text: "Bitte wenden Sie sich an das IT-Team, um den Fehler zu beheben.",
icon: "error"
});
},
})
}
else
Swal.fire({
title: "Ein unerwarteter Fehler",
text: "Die Benutzer-ID existiert nicht (Nullwert).",
icon: "error"
});
}
});
}
}

View File

@@ -0,0 +1,15 @@
<div class="row p-0 m-0">
<div class="col p-0 m-0">
<nav class="navbar bg-body-tertiary">
<form class="container-fluid justify-content-start">
<button class="btn btn-outline-success me-2" type="button"
(click)="addSelectedGroups()">Gruppen<br>Hinzufügen</button>
</form>
</nav>
</div>
</div>
<div class="row align-items-start p-0 m-0">
<div class="col p-0 m-0">
<app-dir-group-table #dirGroups [rowSelection]="dirGroupsRowSelection"></app-dir-group-table>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupDirImportComponent } from './group-dir-import.component';
describe('GroupDirImportComponent', () => {
let component: GroupDirImportComponent;
let fixture: ComponentFixture<GroupDirImportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GroupDirImportComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GroupDirImportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,78 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewChild } from '@angular/core';
import { GuiRowSelection, GuiRowSelectionMode, GuiRowSelectionType } from '@generic-ui/ngx-grid';
import Swal from 'sweetalert2';
import { GroupService } from '../../services/api/group.service';
import { Observable, forkJoin, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { DirGroupTableComponent } from '../tables/dir-group-table/dir-group-table.component';
import { DirGroup } from '../../services/api/api-models';
@Component({
standalone: true,
imports: [DirGroupTableComponent],
selector: 'app-group-dir-import',
templateUrl: './group-dir-import.component.html',
styleUrl: './group-dir-import.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupDirImportComponent implements OnInit {
initWithoutData = () => { }
constructor(private gService: GroupService) {
}
ngOnInit(): void {
}
@ViewChild('dirGroups') dirGroups!: DirGroupTableComponent;
dirGroupsRowSelection: GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE
}
dirUsersRowSelection: GuiRowSelection = {
enabled: true,
type: GuiRowSelectionType.CHECKBOX,
mode: GuiRowSelectionMode.MULTIPLE
}
addSelectedGroups() {
let requests = new Array<Observable<DirGroup | null>>();
let numAdded: number = 0;
for (let row of this.dirGroups.selectedRows) {
requests.push(
this.gService.createByDir({ samaccountname: row?.source?.samaccountname }).pipe(
catchError((err) => {
return of(null);
})
)
);
}
forkJoin(requests).pipe(
// finalize is executed after all requests are completed or when an error occurs
finalize(() => {
Swal.fire({
icon: "success",
title: "Abgeschlossen",
text: `${numAdded} neue Gruppen hinzugefügt`,
position: "center",
showConfirmButton: false,
timer: 3000
});
this.dirGroups.safelyUnselectAll();
})
).subscribe({
next: (results) => {
numAdded += results.filter(result => result !== null).length;
},
error: (err) => {
}
});
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InfoComponent } from './info.component';
describe('InfoComponent', () => {
let component: InfoComponent;
let fixture: ComponentFixture<InfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [InfoComponent]
})
.compileComponents();
fixture = TestBed.createComponent(InfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-info',
standalone: true,
imports: [],
templateUrl: './info.component.html',
styleUrl: './info.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InfoComponent {
}

View File

@@ -0,0 +1,4 @@
i {
margin-left: -30px;
cursor: pointer;
}

View File

@@ -0,0 +1,26 @@
<div class="container p-0 m-0">
<div class="row justify-content-center p-0 m-0">
<div class="col p-0 m-0">
<div class="card px-5">
<div class="card-body mx-5 px-5">
<form (ngSubmit)="login()">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" [(ngModel)]="username" name="Username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Kennwort</label>
<div class="d-flex">
<input type="password" [type]="IsPwdHidden?'password':'text'" class="form-control" [(ngModel)]="password" name="Password" required>
<i [ngClass]="'bi '+ (IsPwdHidden?'bi-eye-slash':'bi-eye') + ' mt-2'" (click)="onPasswordEyeClicked()"></i>
</div>
</div>
<button type="submit" class="btn btn-primary">
<span [class.spinner-border]="waitRes" [class.spinner-border-sm]="waitRes"
aria-hidden="true"></span>Anmeldung</button>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,50 @@
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { AuthenticationService } from '../../services/api/authentication.service';
import Swal from 'sweetalert2';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [CommonModule, FormsModule],
selector: 'app-login',
templateUrl: './login.component.html',
styleUrl: './login.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoginComponent {
username: string = '';
password: string = '';
waitRes: boolean = false;
IsPwdHidden: boolean = true;
constructor(private authService: AuthenticationService, @Inject(MAT_DIALOG_DATA) public data: any) {
if (typeof (this.afterLogin) == typeof (data.afterLogin))
this.afterLogin = data.afterLogin;
}
@Input() afterLogin: () => void = () => { }
login(): void {
this.waitRes = true;
this.authService.login(this.username, this.password).subscribe({
next: () => this.afterLogin(),
error: (err) => {
this.waitRes = false;
Swal.fire({
icon: "error",
title: "Ungültiger Benutzername oder Passwort",
text: "Bitte überprüfen Sie Ihre Anmeldedaten und versuchen Sie es erneut.",
});
},
complete: () => this.waitRes = false
})
}
onPasswordEyeClicked() {
this.IsPwdHidden = !this.IsPwdHidden;
}
}

View File

@@ -0,0 +1,142 @@
a.navbar-brand {
text-align: center;
word-break: break-all;
}
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
width: 100%;
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.btn-bd-primary {
--bd-violet-bg: #712cf9;
--bd-violet-rgb: 112.520718, 44.062154, 249.437846;
--bs-btn-font-weight: 600;
--bs-btn-color: var(--bs-white);
--bs-btn-bg: var(--bd-violet-bg);
--bs-btn-border-color: var(--bd-violet-bg);
--bs-btn-hover-color: var(--bs-white);
--bs-btn-hover-bg: #6528e0;
--bs-btn-hover-border-color: #6528e0;
--bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
--bs-btn-active-color: var(--bs-btn-hover-color);
--bs-btn-active-bg: #5a23c8;
--bs-btn-active-border-color: #5a23c8;
}
.bd-mode-toggle {
z-index: 1500;
}
.bd-mode-toggle .dropdown-menu .active .bi {
display: block !important;
}
.turn-360:hover {
animation: rotate 1s ease forwards;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.scale-pulse:hover {
animation: pulse 1s ease forwards;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.move-left-right:hover {
animation: move 0.8s ease forwards;
}
@keyframes move {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}

View File

@@ -0,0 +1,66 @@
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary fs-5">
<div class="container-fluid">
<!-- DD Logo -->
<a class="logo" href="https://digitaldata.works/">
<img fetchpriority="high" width="200vw"
[src]="(isDarkTheme)?'../../assets/img/DD_white.svg':'../../assets/img/digital_data.svg'">
</a>
<!-- Navbars -->
<div *ngIf="isLogedIn()" class="navbar-collapse collapse d-sm-inline-flex justify-content-center"
[ngClass]="{ show: isExpanded }">
<ul class=" navbar-nav flex-grow">
<li class="nav-item" [routerLinkActive]="['link-active']" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" [routerLink]="['/user-assignment']" [routerLinkActive]="'active'">Autorisierung</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" [routerLink]="['/user-table']" [routerLinkActive]="'active'">Benutzer</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" [routerLink]="['/group-table']" [routerLinkActive]="'active'">Gruppen</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" [routerLink]="['/user-representation']" [routerLinkActive]="'active'">Vertretung</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" [routerLink]="['/module-table']" [routerLinkActive]="'active'">Module</a>
</li>
</ul>
</div>
<!-- Right menu -->
<div class="navbar-collapse justify-content-end me-5">
<a class="navbar-brand" [routerLink]="['/']">User Manager Portal</a>
<button *ngIf="isLogedIn()" class="btn" (click)="creationService.openDialog()" [ngStyle]="{ 'visibility': creationService.isVisible ? 'visible' : 'hidden' }" matTooltip="strg + C" matTooltipPosition="below" matTooltipClass="pt-3" [matTooltipDisabled]="!creationService.isVisible">
<mat-icon class="scale-pulse">add_to_photos</mat-icon>
</button>
<button *ngIf="isLogedIn()" class="btn" (click)="this.updateService.toggleEditability()" [ngStyle]="{ 'visibility': updateService.isVisible ? 'visible' : 'hidden' }" matTooltip="strg + L" matTooltipPosition="below" [matTooltipDisabled]="!updateService.isVisible">
<mat-icon class="scale-pulse">{{ updateService.isEditable ? 'lock_open' : 'lock' }}</mat-icon>
</button>
<button *ngIf="isLogedIn()" class="btn" (click)="saveAsync()" [ngStyle]="{ 'visibility': updateService.isVisible ? 'visible' : 'hidden' }" matTooltip="strg + S" matTooltipPosition="below" matTooltipClass="pt-3" [matTooltipDisabled]="!updateService.isVisible">
<mat-icon class="scale-pulse" [matBadge]="updateActCount === 0 ? '' : updateActCount">save</mat-icon>
</button>
<button *ngIf="isLogedIn()" class="btn" (click)="deletionService.executeAll()" [ngStyle]="{ 'visibility': updateService.isVisible ? 'visible' : 'hidden' }" matTooltip="entf" matTooltipPosition="below" matTooltipClass="pt-3" [matTooltipDisabled]="!deletionService.isVisible">
<mat-icon class="scale-pulse">delete_forever</mat-icon>
</button>
<button *ngIf="isLogedIn()" class="btn" (click)="transferService.executeAll()" [ngStyle]="{ 'visibility': transferService.isVisible ? 'visible' : 'hidden' }" matTooltip="strg + ␣" matTooltipPosition="below" [matTooltipDisabled]="!transferService.isVisible">
<mat-icon class="move-left-right">swap_horiz</mat-icon>
</button>
<button *ngIf="isLogedIn()" class="btn" (click)="refreshService.executeAll()" [ngStyle]="{ 'visibility': refreshService.isVisible ? 'visible' : 'hidden' }" matTooltip="strg + R" matTooltipPosition="below" matTooltipClass="pt-3" [matTooltipDisabled]="!refreshService.isVisible">
<mat-icon class="turn-360">sync</mat-icon>
</button>
<button *ngIf="isLogedIn()" [ngStyle]="{ 'visibility': buttonVisibilityService.anyVisible ? 'visible' : 'hidden' }" class="btn" (click)="showInfo()">
<mat-icon class="scale-pulse">contact_support</mat-icon>
</button>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse"
aria-label="Toggle navigation" [attr.aria-expanded]="isExpanded" (click)="toggle()">
<span class="navbar-toggler-icon"></span>
</button>
<app-color-mode-bttn></app-color-mode-bttn>
<button class="fs-5 btn d-flex align-items-center ms-2 me-0 pe-0" type="button" (click)="auth()">
<img fetchpriority="high" src="../../assets/img/login_logo.svg" alt="" style="stroke: #a9a8ad;">
{{isLogedIn() ? "Log out" : "Log in"}}
</button>
</div>
</div>
</nav>
</header>

View File

@@ -0,0 +1,98 @@
import { ChangeDetectionStrategy, Component, QueryList, ViewChildren } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AuthenticationService, IsLogedIn } from '../../services/api/authentication.service';
import { LoginComponent } from '../login/login.component';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { ColorModeBttnComponent } from '../common/color-mode-bttn/color-mode-bttn.component';
import { MatIconModule } from '@angular/material/icon';
import { RefreshService } from '../../services/button/refresh.service';
import { CreationService } from '../../services/button/creation.service';
import { DeletionService } from '../../services/button/deletion.service';
import { ButtonVisibilityService } from '../../services/button/button-visibility.service';
import { UpdateService, UpdateEvent } from '../../services/button/update.service';
import { TransferService } from '../../services/button/transfer.service';
import { MatBadgeModule } from '@angular/material/badge';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
import { MatButtonModule } from '@angular/material/button';
@Component({
standalone: true,
imports: [RouterModule, CommonModule, ColorModeBttnComponent, MatIconModule, MatBadgeModule, MatSlideToggleModule, FormsModule, MatButtonModule, MatTooltipModule],
selector: 'app-nav-menu',
templateUrl: './nav-menu.component.html',
styleUrls: ['./nav-menu.component.css'],
changeDetection: ChangeDetectionStrategy.Default
})
export class NavMenuComponent {
isLogedIn() {
return IsLogedIn();
}
isExpanded = false;
updateActCount: number;
isChecked = true;
constructor(private dialog: MatDialog, private authService: AuthenticationService, public refreshService: RefreshService, public creationService: CreationService, public updateService: UpdateService, public transferService: TransferService, public buttonVisibilityService: ButtonVisibilityService, public deletionService: DeletionService) {
this.authService.isAuthenticated()
this.updateActCount = this.updateService.totalCount;
this.updateService.addChangeListener(UpdateEvent.CountChange, () => {
this.updateActCount = updateService.totalCount;
})
}
get isDarkTheme(): boolean {
return (typeof window !== 'undefined') ? (localStorage.getItem('theme') === 'dark') : true;
}
collapse() {
this.isExpanded = false;
}
toggle() {
this.isExpanded = !this.isExpanded;
}
async auth() {
const isLoggedIn = await this.authService.isAuthenticated();
if (isLoggedIn)
this.authService.logout().subscribe();
else {
const dialogRef = this.dialog.open(LoginComponent, {
width: "35vw",
data: {
afterLogin: () => {
dialogRef.close();
}
}
});
}
}
@ViewChildren(MatTooltip) tooltips: QueryList<MatTooltip> | undefined;
private __tooltip_timeout_set = false;
showInfo() {
this.tooltips?.forEach(t => {
t.show();
});
if(!this.__tooltip_timeout_set){
this.__tooltip_timeout_set = true;
setTimeout(() => {
this.__tooltip_timeout_set = false;
this.tooltips?.forEach(t => {
t.hide();
});
}, 3000);
}
}
async saveAsync() {
await this.updateService.executeAllAsync().then(() => this.refreshService.executeAll())
}
}

View File

@@ -0,0 +1,23 @@
<h2 class="card-header">Benutzer-Stammdaten</h2>
<div class="card-body">
<table class="table table-striped table-hover">
<tbody>
<tr>
<th scope="row">DatumsFormat</th>
<td>{{user.dateFormat}}</td>
</tr>
<tr>
<th scope="row">Kommentar</th>
<td>{{user.comment}}</td>
</tr>
<tr>
<th scope="row">Hinzugefügt wer</th>
<td colspan="2">{{user.addedWho}}</td>
</tr>
<tr>
<th scope="row">Geändert wenn</th>
<td colspan="2">{{user.changedWho}}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserSummaryComponent } from './user-summary.component';
describe('UserSummaryComponent', () => {
let component: UserSummaryComponent;
let fixture: ComponentFixture<UserSummaryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserSummaryComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserSummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,18 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MatListModule } from '@angular/material/list';
import { User } from '../../../services/api/api-models'
import { MAT_BOTTOM_SHEET_DATA } from '@angular/material/bottom-sheet';
@Component({
selector: 'user-summary',
standalone: true,
imports: [MatListModule],
templateUrl: './user-summary.component.html',
styleUrl: './user-summary.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserSummaryComponent {
constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public user: User) {
}
}

View File

@@ -0,0 +1,20 @@
<gui-grid #grid
[columns]="columns"
[columnMenu]="columnMenu"
[sorting]="sorting"
[loading]="loading"
[rowSelection] = "rowSelection"
[rowDetail]="rowDetail"
[autoResizeWidth]="autoResizeWidth"
[paging]="paging"
[searching]="searching"
[cellEditing]="cellEditing"
[virtualScroll]="true"
[infoPanel]="infoPanel"
[titlePanel]="titlePanel"
[theme]="theme"
[rowStyle] = "rowStyle"
[rowClass] = "rowClass"
(selectedRows)="onSelectedRows($event)"
(click)="click && click(this)">
</gui-grid>

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BaseTableComponent } from './base-table.component';
import { ApiService } from '../../../services/api/api-service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('BaseTableComponent', () => {
let component: BaseTableComponent<any, any>;
let fixture: ComponentFixture<BaseTableComponent<any, any>>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BaseTableComponent ],
providers: [
{
provide: ApiService,
useValue: jasmine.createSpyObj('ApiService', ['getAll', 'update', 'delete'])
}
],
// Ignore any unknown elements and attributes
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BaseTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// Add more tests here to cover other functionalities
});

View File

@@ -0,0 +1,172 @@
import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, OnInit, ViewChild, input } from '@angular/core';
import { ApiService } from '../../../services/api/api-service';
import { GuiGridModule, GuiColumn, GuiColumnMenu, GuiSorting, GuiRowDetail, GuiPaging, GuiPagingDisplay, GuiSearching, GuiCellEdit, GuiInfoPanel, GuiTitlePanel, GuiRowSelection, GuiSelectedRow, GuiGridComponent, GuiGridApi, GuiTheme, GuiRowStyle, GuiRowClass } from '@generic-ui/ngx-grid';
import { Subscription } from 'rxjs/internal/Subscription';
import { ColorModeService, Theme } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule],
selector: 'app-base-table',
templateUrl: './base-table.component.html',
styleUrl: './base-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BaseTableComponent<TModel, TApiService extends ApiService<TModel>> implements OnInit, OnDestroy {
service: TApiService;
columnMenu: GuiColumnMenu = {
enabled: true,
sort: true,
columnsManager: false,
filter: false
};
sorting: GuiSorting = {
enabled: true,
multiSorting: true
};
loading: boolean = false;
autoResizeWidth: boolean = true;
rowDetail: GuiRowDetail = {
enabled: true,
template: (item: TModel) => {
return `
<div></div>`;
}
};
paging: GuiPaging = {
enabled: true,
page: 1,
pageSize: 15,
pageSizes: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
pagerTop: true,
pagerBottom: false,
display: GuiPagingDisplay.ADVANCED
};
searching: GuiSearching = {
enabled: true,
placeholder: 'Suche'
};
maxHeight: any = 400;
infoPanel: GuiInfoPanel = {
enabled: true,
infoDialog: false,
columnsManager: true,
schemaManager: true
};
titlePanel: GuiTitlePanel = {
enabled: false,
template: () => {
return `
<div class='title-panel-example' >List of contract workers</div>
`;
}
};
theme: GuiTheme = typeof window !== 'undefined' ? (localStorage.getItem('theme') === 'dark' ? GuiTheme.DARK : GuiTheme.FABRIC) : GuiTheme.DARK;
private themeSubscription: Subscription = new Subscription();
private static count: number = 0;
public readonly id: number = (BaseTableComponent.count++)
constructor(@Inject(ApiService<TModel>) service: TApiService, columns: Array<GuiColumn>, private cModeService: ColorModeService) {
this.service = service;
if (this.columns.length == 0)
this.columns = columns;
//assign row details
if (this.rowDetailTemplate === null || this.rowDetailTemplate === undefined)
this.rowDetail = {
enabled: false,
};
else
this.rowDetail = {
enabled: true,
template: (this.rowDetailTemplate)
};
}
@Input() rowDetailTemplate: null | ((item: TModel, index: number) => string) = null;
@Input() isCellEditable = false;
@Input() cellEditing: GuiCellEdit = {
enabled: this.isCellEditable,
rowEdit: (value: any, item: any, index: number) => {
return Boolean(index % 2);
},
cellEdit: (value: any, item: any, index: number) => {
return Boolean(index % 5);
}
}
@Input() rowSelection: boolean | GuiRowSelection = true;
@Input() onSelectedRows: (rows: Array<GuiSelectedRow>) => void = (rows) => { };
@Input() initData: () => void = this.fetchData;
@Input() columns: Array<GuiColumn> = [];
@Input() rowStyle: GuiRowStyle = {}
@Input() rowClass: GuiRowClass = {}
@Input() click: ((table: BaseTableComponent<TModel, TApiService>) => void) | undefined;
selected: boolean = false;
safelyUnselectAll() {
this.selected = true
if (this.api?.getSelectedRows() != undefined)
if ((this.api?.getSelectedRows().length ?? 0 > 0) && this.selected) {
this.api?.unselectAll()
this.selected = false
}
}
@ViewChild('grid', { static: true }) mainGrid!: GuiGridComponent;
private get api(): GuiGridApi {
return this.mainGrid.api;
}
set source(data: any[]) {
this.api.setSource(data)
}
get selectedRows(): Array<GuiSelectedRow> {
return this.api.getSelectedRows();
}
ngOnInit(): void {
const subscription = this.cModeService.themeChanges$.subscribe((theme: Theme) => {
this.theme = theme === 'dark' ? GuiTheme.DARK : GuiTheme.FABRIC;
});
this.themeSubscription.add(subscription);
this.initData()
}
ngOnDestroy(): void {
this.themeSubscription.unsubscribe();
}
fetchData(): void {
this.service.getAll().subscribe({
next: (response) => {
this.source = response;
this.loading = false;
},
error: (error) => { }
});
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DirGroupTableComponent } from './dir-group-table.component';
describe('AdGroupUserTableComponent', () => {
let component: DirGroupTableComponent;
let fixture: ComponentFixture<DirGroupTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DirGroupTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DirGroupTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { BaseTableComponent } from '../base-table/base-table.component';
import { DirGroupService } from '../../../services/api/dir-group.service';
import { DirGroup } from '../../../services/api/api-models';
import { GuiGridModule } from '@generic-ui/ngx-grid';
import { ColorModeService } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
import { GroupService } from '../../../services/api/group.service';
import { firstValueFrom } from 'rxjs';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule ],
selector: 'app-dir-group-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './dir-group-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DirGroupTableComponent extends BaseTableComponent<DirGroup, DirGroupService> {
constructor(service: DirGroupService, cModeService: ColorModeService, private gService: GroupService) {
super(service, env.columnNames.dirGroup, cModeService)
}
override fetchData(): void {
this.service.getAll().subscribe({
next: async (response) => {
const group_names = (await firstValueFrom(this.gService.getAll())).map(g => g.name);
this.source = response.filter(dGroup => dGroup.samaccountname?.length && !group_names.includes(dGroup.samaccountname[0]));
this.loading = false;
},
error: (error) => { }
});
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DirUserTableComponent } from './dir-user-table.component';
describe('DirUserTableComponent', () => {
let component: DirUserTableComponent;
let fixture: ComponentFixture<DirUserTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DirUserTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DirUserTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { DirUser } from '../../../services/api/api-models';
import { DirUserService } from '../../../services/api/dir-user.service';
import { BaseTableComponent } from '../base-table/base-table.component';
import { GuiGridModule, GuiColumn } from '@generic-ui/ngx-grid';
import { ColorModeService } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
import { UserService } from '../../../services/api/user.service';
import { firstValueFrom } from 'rxjs/internal/firstValueFrom';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule],
selector: 'app-dir-user-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './dir-user-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DirUserTableComponent extends BaseTableComponent<DirUser, DirUserService> {
constructor(service: DirUserService, cModeService: ColorModeService, private uService: UserService) {
super(service, env.columnNames.dirUser, cModeService)
}
fetchDataByGroupName(groupName: string): void {
this.service.getAll(groupName).subscribe({
next: async (response: DirUser[]) => {
const usernames = (await firstValueFrom(this.uService.getAll())).map(u => u.username)
this.source = response.filter(user => user.samaccountname?.length && !usernames.includes(user.samaccountname[0]));
this.loading = false;
},
error: (error: any) => { }
})
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupTableComponent } from './group-table.component';
describe('GroupTableComponent', () => {
let component: GroupTableComponent;
let fixture: ComponentFixture<GroupTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GroupTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GroupTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { GroupService } from '../../../services/api/group.service';
import { Group } from '../../../services/api/api-models';
import { GuiGridModule, GuiColumn } from '@generic-ui/ngx-grid';
import { BaseTableComponent } from '../base-table/base-table.component';
import { ColorModeService } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
import { GroupOfUserService } from '../../../services/api/group-of-user.service';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule],
selector: 'app-group-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './group-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupTableComponent extends BaseTableComponent<Group, GroupService> {
constructor(service: GroupService, cModeService: ColorModeService, private gouService: GroupOfUserService) {
super(service, env.columnNames.group.basic, cModeService)
}
fetchDataByUsername(username: string) {
this.gouService.getByUsername(username)
.then(gos_list => gos_list.map(gos => gos.group))
.then(groups => this.source = groups)
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupUserTableComponent } from './group-user-table.component';
describe('GroupUserTableComponent', () => {
let component: GroupUserTableComponent;
let fixture: ComponentFixture<GroupUserTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GroupUserTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GroupUserTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { GroupOfUserService } from '../../../services/api/group-of-user.service';
import { GroupOfUser } from '../../../services/api/api-models';
import { GuiGridModule } from '@generic-ui/ngx-grid';
import { BaseTableComponent } from '../base-table/base-table.component';
import { ColorModeService } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule ],
selector: 'app-group-user-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './group-user-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupUserTableComponent extends BaseTableComponent<GroupOfUser, GroupOfUserService> {
constructor(service: GroupOfUserService, cModeService: ColorModeService) {
super(service, env.columnNames.groupOfUser, cModeService)
this.initData = () => this.fetchDataWith(true,true);
this.loading = false;
}
fetchDataWith(withUser: boolean, withGroup: boolean){
this.service.getAll(withUser, withGroup).subscribe ({
next: (response) => {
this.source = response;
},
error: (error) => {}
});
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModuleTableComponent } from './module-table.component';
describe('ModuleTableComponent', () => {
let component: ModuleTableComponent;
let fixture: ComponentFixture<ModuleTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModuleTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ModuleTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Module } from '../../../services/api/api-models';
import { GuiGridModule } from '@generic-ui/ngx-grid';
import { BaseTableComponent } from '../base-table/base-table.component';
import { ModuleService } from '../../../services/api/module.service'
import { ColorModeService } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
import { ModuleOfUserService } from '../../../services/api/module-of-user.service';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule],
selector: 'app-module-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './module-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModuleTableComponent extends BaseTableComponent<Module, ModuleService> {
constructor(
service: ModuleService, cModeService: ColorModeService, private mouService: ModuleOfUserService) {
super(service, env.columnNames.module, cModeService)
}
fetchDataByUsername(username: string) {
this.mouService.getByUsername(username)
.then(mou_list => mou_list.map(mou => mou.module))
.then(modules => this.source = modules)
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserRepTableComponent } from './user-rep-table.component';
describe('UserRepTableComponent', () => {
let component: UserRepTableComponent;
let fixture: ComponentFixture<UserRepTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserRepTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserRepTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,43 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { UserRep } from '../../../services/api/api-models';
import { UserRepService } from '../../../services/api/user-representation.service';
import { BaseTableComponent } from '../base-table/base-table.component';
import { GuiGridModule } from '@generic-ui/ngx-grid';
import { ColorModeService } from '../../../services/button/color-mode.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule],
selector: 'app-user-rep-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './user-rep-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserRepTableComponent extends BaseTableComponent<UserRep, UserRepService> {
constructor(service: UserRepService, cModeService: ColorModeService) {
super(service, env.columnNames.userRep, cModeService)
this.loading = false
}
override fetchData(userId?: number, groupId?: number): void {
this.service.getAll({ withUser: false, withRepGroup: true, withGroup: false, withRepUser: true, userId: userId, groupId: groupId }).subscribe({
next: (response: UserRep[]) => {
this.source = response;
this.loading = false;
},
error: (error: any) => { }
});
}
public fetchByUser(id: number): void {
this.fetchData(id, undefined);
}
public fetchByGroup(id: number): void {
this.fetchData(undefined, id);
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserTableComponent } from './user-table.component';
describe('UserTableComponent', () => {
let component: UserTableComponent;
let fixture: ComponentFixture<UserTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,90 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { UserService } from '../../../services/api/user.service';
import { ModuleOfUserService } from '../../../services/api/module-of-user.service';
import { GroupOfUserService } from '../../../services/api/group-of-user.service';
import { User } from '../../../services/api/api-models';
import { GuiGridModule } from '@generic-ui/ngx-grid';
import { BaseTableComponent } from '../base-table/base-table.component';
import { ColorModeService } from '../../../services/button/color-mode.service'
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { env } from '../../../../environments/environment';
@Component({
standalone: true,
imports: [CommonModule, FormsModule, GuiGridModule],
selector: 'app-user-table',
templateUrl: '../base-table/base-table.component.html',
styleUrl: './user-table.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserTableComponent extends BaseTableComponent<User, UserService> {
mosService: ModuleOfUserService;
gosService: GroupOfUserService;
constructor(mosService: ModuleOfUserService,
gosService: GroupOfUserService,
service: UserService,
cModeService: ColorModeService) {
super(service, env.columnNames.user.basic, cModeService)
this.mosService = mosService;
this.gosService = gosService;
}
fetchDataByModuleId(moduleId: number, assigned: boolean = true): void {
this.service.getByModuleId(moduleId, assigned).subscribe({
next: (users) => {
this.source = users;
},
error: (error) => {}
});
}
fetchDataByGroupId(groupId: number, assigned: boolean = true): void {
this.service.getByGroupId(groupId, assigned).subscribe({
next: (users) => {
this.source = users;
},
error: (error) => {}
});
}
async createModuleOfUsers(moduleId: number, users: User[]): Promise<any[]> {
const creationPromises = users
.filter(user => user.id && user.id != null)
.map(user => this.mosService.create({ moduleId: moduleId, userId: user.id ?? -1, addedWho: "DEFAULT" }).toPromise());
return Promise.all(creationPromises);
}
async createGroupOfUsers(groupId: number, users: User[]): Promise<any[]> {
const creationPromises = users
.filter(user => user.id && user.id != null)
.map(user => this.gosService.create({ groupId: groupId, userId: user.id ?? -1, addedWho: "DEFAULT" }).toPromise());
return Promise.all(creationPromises);
}
async deleteModuleOfUsers(moduleId: number, users: User[]): Promise<void> {
const deletionPromises = users
.filter(user => user.id)
.map(user => this.mosService.deleteByModuleGroupId(moduleId, user.id ?? -1).toPromise());
try {
const responses = await Promise.all(deletionPromises);
} catch (error) {
}
}
async deleteGroupOfUsers(groupId: number, users: User[]): Promise<void> {
const deletionPromises = users
.filter(user => user.id)
.map(user => this.gosService.deleteByGroupUserId(groupId, user.id ?? -1).toPromise());
try {
const responses = await Promise.all(deletionPromises);
} catch (error) {
}
}
}

View File

@@ -0,0 +1,18 @@
<div class="row p-0 m-0">
<div class="col p-0 m-0">
<nav class="navbar bg-body-tertiary">
<form class="container-fluid justify-content-start">
<button class="btn btn-outline-success me-2" type="button"
(click)="addSelectedUsers()">Benutzer<br>Hinzufügen</button>
</form>
</nav>
</div>
</div>
<div class="row align-items-start p-0 m-0">
<div class="col-6 p-0 m-0">
<app-dir-group-table #dirGroups [rowSelection]="dirGroupsRowSelection" [onSelectedRows]="dirGroupOnSelectedRows"></app-dir-group-table>
</div>
<div class="col-6 p-0 m-0">
<app-dir-user-table #dirUsers [initData]="initWithoutData" [rowSelection]="dirUsersRowSelection"></app-dir-user-table>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserGroupDirImportComponent } from './user-group-dir-import.component';
describe('UserGroupDirImportComponent', () => {
let component: UserGroupDirImportComponent;
let fixture: ComponentFixture<UserGroupDirImportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserGroupDirImportComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UserGroupDirImportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More