Compare commits

..

10 Commits

Author SHA1 Message Date
Bastian Wagner
b4826cab2c mods
Some checks failed
Run Unit-Tests / test_frontend (push) Has been cancelled
Run Unit-Tests / test_backend (push) Has been cancelled
2026-02-16 13:07:31 +01:00
Bastian Wagner
9ea2229f5a Logging & Styling 2026-02-16 12:11:30 +01:00
Bastian Wagner
2eafa21baf unit tests 2026-02-13 15:12:54 +01:00
Bastian Wagner
ea947caf54 update AG Grid 2026-02-13 14:29:45 +01:00
Bastian Wagner
22796196a7 Material to 21 2026-02-13 14:21:41 +01:00
Bastian Wagner
fe64d141b7 Angular to 21 2026-02-13 14:18:34 +01:00
Bastian Wagner
007d9a9d5b Jest upadte 2026-02-13 14:11:39 +01:00
Bastian Wagner
50c43c3da2 Jest preset Update 2026-02-13 14:09:08 +01:00
Bastian Wagner
5a7ef383fe Angular Material to 20 2026-02-13 14:06:23 +01:00
Bastian Wagner
1ff84df9b2 Angular 20 2026-02-13 14:00:07 +01:00
63 changed files with 4814 additions and 12462 deletions

View File

@@ -1,3 +1,6 @@
# Application
DEVELOP_MODE=false
# Database # Database
MYSQL_USER=db_user MYSQL_USER=db_user
MYSQL_PASSWORD=PAssword123 MYSQL_PASSWORD=PAssword123

View File

@@ -5,6 +5,7 @@ import { DatabaseModule } from 'src/shared/database/database.module';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { JwtModule, JwtService } from '@nestjs/jwt'; import { JwtModule, JwtService } from '@nestjs/jwt';
import { LogModule } from '../log/log.module';
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
@@ -21,6 +22,7 @@ import { JwtModule, JwtService } from '@nestjs/jwt';
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') }, signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') },
}), }),
}), }),
LogModule
], ],
exports: [JwtModule, AuthService], exports: [JwtModule, AuthService],
}) })

View File

@@ -7,6 +7,7 @@ import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { IExternalAccessPayload, IPayload } from 'src/model/interface'; import { IExternalAccessPayload, IPayload } from 'src/model/interface';
import { User } from 'src/model/entitites'; import { User } from 'src/model/entitites';
import { LogService, LogType } from '../log/log.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@@ -15,9 +16,12 @@ export class AuthService {
private readonly http: HttpService, private readonly http: HttpService,
private configService: ConfigService, private configService: ConfigService,
private jwt: JwtService, private jwt: JwtService,
private log: LogService
) {} ) {}
register(register: CreateUserDto) { register(register: CreateUserDto) {
return this.userRepo.createUser(register); const user = this.userRepo.createUser(register);
this.log.log(LogType.Auth, user);
} }
async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise<User> { async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise<User> {
@@ -31,6 +35,7 @@ export class AuthService {
return resolve(null); return resolve(null);
} }
this.generateTokens(user); this.generateTokens(user);
this.log.log(LogType.Auth, user);
resolve(user); resolve(user);
}, },
error: () => { error: () => {
@@ -116,6 +121,7 @@ export class AuthService {
} }
getUserById(id: string): Promise<User> { getUserById(id: string): Promise<User> {
this.log.log(LogType.Auth, null);
return this.userRepo.findById(id); return this.userRepo.findById(id);
} }

View File

@@ -4,10 +4,11 @@ import { CylinderService } from './cylinder.service';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { DatabaseModule } from 'src/shared/database/database.module'; import { DatabaseModule } from 'src/shared/database/database.module';
import { SharedServiceModule } from 'src/shared/service/shared.service.module'; import { SharedServiceModule } from 'src/shared/service/shared.service.module';
import { ConfigService } from '@nestjs/config';
@Module({ @Module({
controllers: [CylinderController], controllers: [CylinderController],
providers: [CylinderService], providers: [CylinderService, ConfigService],
imports: [AuthModule, DatabaseModule, SharedServiceModule], imports: [AuthModule, DatabaseModule, SharedServiceModule],
}) })
export class CylinderModule {} export class CylinderModule {}

View File

@@ -1,4 +1,5 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cylinder, User } from 'src/model/entitites'; import { Cylinder, User } from 'src/model/entitites';
import { ActivityRepository, CylinderRepository, KeyRepository } from 'src/model/repositories'; import { ActivityRepository, CylinderRepository, KeyRepository } from 'src/model/repositories';
import { HelperService } from 'src/shared/service/system.helper.service'; import { HelperService } from 'src/shared/service/system.helper.service';
@@ -9,15 +10,24 @@ export class CylinderService {
private readonly cylinderRepo: CylinderRepository, private readonly cylinderRepo: CylinderRepository,
private readonly keyRepo: KeyRepository, private readonly keyRepo: KeyRepository,
private systemActivityRepo: ActivityRepository, private systemActivityRepo: ActivityRepository,
private readonly helper: HelperService private readonly helper: HelperService,
private readonly configService: ConfigService
) {} ) {}
get isDevelopMode(): boolean {
return (this.configService.get('DEVELOP_MODE') || '').toLowerCase() == 'true';
}
async getCylinders(user: User): Promise<Cylinder[]> { async getCylinders(user: User): Promise<Cylinder[]> {
const c = await this.cylinderRepo.find({ let c = await this.cylinderRepo.find({
where: { system: { managers: { id: user.id } } }, where: { system: { managers: { id: user.id } } },
order: { name: { direction: 'ASC' } }, order: { name: { direction: 'ASC' } },
relations: ['system', 'keys'], relations: ['system', 'keys'],
}); });
if (this.isDevelopMode) {
c = c.filter(c => c.name.toLowerCase().includes('develop') || c.system.name.toLocaleLowerCase().includes('develop'))
}
return c; return c;
} }

View File

@@ -4,10 +4,11 @@ import { KeyService } from './key.service';
import { DatabaseModule } from 'src/shared/database/database.module'; import { DatabaseModule } from 'src/shared/database/database.module';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { SharedServiceModule } from 'src/shared/service/shared.service.module'; import { SharedServiceModule } from 'src/shared/service/shared.service.module';
import { ConfigService } from '@nestjs/config';
@Module({ @Module({
controllers: [KeyController], controllers: [KeyController],
providers: [KeyService], providers: [KeyService, ConfigService],
imports: [DatabaseModule, AuthModule, SharedServiceModule], imports: [DatabaseModule, AuthModule, SharedServiceModule],
}) })
export class KeyModule {} export class KeyModule {}

View File

@@ -8,7 +8,9 @@ import {
import { KeyHandoutRepository } from 'src/model/repositories/key-handout.repository'; import { KeyHandoutRepository } from 'src/model/repositories/key-handout.repository';
import { ActivityHelperService } from 'src/shared/service/activity.logger.service'; import { ActivityHelperService } from 'src/shared/service/activity.logger.service';
import { HelperService } from 'src/shared/service/system.helper.service'; import { HelperService } from 'src/shared/service/system.helper.service';
import { IsNull, Not } from 'typeorm'; import { FindOperator, IsNull, Not } from 'typeorm';
import { faker } from '@faker-js/faker';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class KeyService { export class KeyService {
@@ -18,27 +20,39 @@ export class KeyService {
private readonly handoverRepo: KeyHandoutRepository, private readonly handoverRepo: KeyHandoutRepository,
private readonly activityService: ActivityHelperService, private readonly activityService: ActivityHelperService,
private readonly helper: HelperService, private readonly helper: HelperService,
private readonly configService: ConfigService
) {} ) {}
get isDevelopMode(): boolean {
return (this.configService.get('DEVELOP_MODE') || '').toLowerCase() == 'true';
}
async getUsersKeys(user: User): Promise<Key[]> { async getUsersKeys(user: User): Promise<Key[]> {
const keys = await this.keyrepository.find({ let keys = await this.keyrepository.find({
where: { cylinder: { system: { managers: { id: user.id } } }, keyLost: IsNull() }, where: { cylinder: { system: { managers: { id: user.id } } }, keyLost: IsNull() },
relations: ['cylinder', 'cylinder.system', 'customer'], relations: ['cylinder', 'cylinder.system', 'customer'],
}); });
for (let k of keys) { for (let k of keys) {
k.customer = await this.getCustomerOfLastHandout(user, k.id); k.customer = await this.getCustomerOfLastHandout(user, k.id);
} }
if (this.isDevelopMode) {
keys = keys.filter(k => k.cylinder.some(c => c.name.toLowerCase().includes('develop') || c.system.name.toLowerCase().includes('develop')))
}
return keys; return keys;
} }
async getLostKeys(user: User): Promise<Key[]> { async getLostKeys(user: User): Promise<Key[]> {
const keys = await this.keyrepository.find({ let keys = await this.keyrepository.find({
where: { cylinder: { system: { managers: { id: user.id } } }, keyLost: Not(IsNull()) }, where: { cylinder: { system: { managers: { id: user.id } } }, keyLost: Not(IsNull()) },
relations: ['cylinder', 'cylinder.system', 'customer'], relations: ['cylinder', 'cylinder.system', 'customer'],
}); });
for (let k of keys) { for (let k of keys) {
k.customer = await this.getCustomerOfLastHandout(user, k.id); k.customer = await this.getCustomerOfLastHandout(user, k.id);
} }
if (this.isDevelopMode) {
keys = keys.filter(k => k.cylinder.some(c => c.name.toLowerCase().includes('develop') || c.system.name.toLowerCase().includes('develop')))
}
return keys; return keys;
} }

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { User } from 'src/model/entitites';
import { EmailLogRepository } from 'src/model/repositories/log'; import { EmailLogRepository } from 'src/model/repositories/log';
@Injectable() @Injectable()
@@ -11,6 +12,8 @@ export class LogService {
log(type: LogType, data: any) { log(type: LogType, data: any) {
if (type == LogType.Mail) { if (type == LogType.Mail) {
return this.logEmail(data); return this.logEmail(data);
} else if (type == LogType.Auth) {
return this.logAuthEvent(data);
} }
} }
@@ -18,10 +21,15 @@ export class LogService {
const log = this.emailLogRepo.create(data); const log = this.emailLogRepo.create(data);
const logEntry = await this.emailLogRepo.save(log); const logEntry = await this.emailLogRepo.save(log);
} }
private async logAuthEvent(data: User) {
console.error("auth logging not implemented")
}
} }
export enum LogType { export enum LogType {
Mail Mail,
Auth
} }
export enum EmailEvent { export enum EmailEvent {

View File

@@ -2,6 +2,8 @@ import { MailerService } from "@nestjs-modules/mailer";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { EmailEvent, LogService, LogType } from "../log/log.service"; import { EmailEvent, LogService, LogType } from "../log/log.service";
import { KeySystem } from "src/model/entitites/system.entity";
import { User } from "src/model/entitites";
@Injectable() @Injectable()
export class MailService { export class MailService {
@@ -12,56 +14,60 @@ export class MailService {
) { ) {
} }
async sendAccessGrantedMail({to, firstName, systemName}: {to: string, firstName: string, systemName: string}) { async sendAccessGrantedMail({to, system}: {to: User, system: KeySystem}) {
this.mailserService.sendMail({ this.mailserService.sendMail({
template: './access', template: './access',
to: to, to: to.username,
from: this.configService.get<string>('MAILER_FROM'), from: this.configService.get<string>('MAILER_FROM'),
subject: 'Zugriff gewährt', subject: 'Zugriff gewährt',
context: { context: {
firstName, firstName: to.firstName,
systemName systemName: system.name
} }
}).then(v => { }).then(v => {
this.logService.log(LogType.Mail, { this.logService.log(LogType.Mail, {
to, to,
success: true, success: true,
message: v.response, message: v.response,
type: EmailEvent.GrantSystemAccess type: EmailEvent.GrantSystemAccess,
system
}) })
}).catch(e => { }).catch(e => {
this.logService.log(LogType.Mail, { this.logService.log(LogType.Mail, {
to, to,
success: false, success: false,
message: e.response, message: e.response,
type: EmailEvent.GrantSystemAccess type: EmailEvent.GrantSystemAccess,
system
}) })
}) })
} }
sendAccessRemovedMail({to, firstName, systemName}: {to: string, firstName: string, systemName: string}) { sendAccessRemovedMail({to, system}: {to: User, system: KeySystem}) {
this.mailserService.sendMail({ this.mailserService.sendMail({
template: './access-removed', template: './access-removed',
to: to, to: to.username,
from: this.configService.get<string>('MAILER_FROM'), from: this.configService.get<string>('MAILER_FROM'),
subject: 'Zugriff entzogen', subject: 'Zugriff entzogen',
context: { context: {
firstName, firstName: to.firstName,
systemName systemName: system.name
} }
}).then(v => { }).then(v => {
this.logService.log(LogType.Mail, { this.logService.log(LogType.Mail, {
to, to,
success: true, success: true,
message: v.response, message: v.response,
type: EmailEvent.RemoveSystemAccess type: EmailEvent.RemoveSystemAccess,
system
}) })
}).catch(e => { }).catch(e => {
this.logService.log(LogType.Mail, { this.logService.log(LogType.Mail, {
to, to,
success: false, success: false,
message: e.response, message: e.response,
type: EmailEvent.RemoveSystemAccess type: EmailEvent.RemoveSystemAccess,
system
}) })
}) })
} }

View File

@@ -4,10 +4,11 @@ import { SystemController } from './system.controller';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { DatabaseModule } from 'src/shared/database/database.module'; import { DatabaseModule } from 'src/shared/database/database.module';
import { MailModule } from '../mail/mail.module'; import { MailModule } from '../mail/mail.module';
import { ConfigService } from '@nestjs/config';
@Module({ @Module({
controllers: [SystemController], controllers: [SystemController],
providers: [SystemService], providers: [SystemService, ConfigService],
imports: [AuthModule, DatabaseModule, MailModule], imports: [AuthModule, DatabaseModule, MailModule],
}) })
export class SystemModule {} export class SystemModule {}

View File

@@ -5,6 +5,7 @@ import { ActivityRepository, KeySystemRepository, UserRepository } from 'src/mod
import { User } from 'src/model/entitites'; import { User } from 'src/model/entitites';
import { IUser } from 'src/model/interface'; import { IUser } from 'src/model/interface';
import { MailService } from '../mail/mail.service'; import { MailService } from '../mail/mail.service';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class SystemService { export class SystemService {
@@ -12,9 +13,14 @@ export class SystemService {
private systemRepo: KeySystemRepository, private systemRepo: KeySystemRepository,
private userRepo: UserRepository, private userRepo: UserRepository,
private systemActivityRepo: ActivityRepository, private systemActivityRepo: ActivityRepository,
private mailService: MailService private mailService: MailService,
private readonly configService: ConfigService
) {} ) {}
get isDevelopMode(): boolean {
return (this.configService.get('DEVELOP_MODE') || '').toLowerCase() == 'true';
}
async create(user: User, createSystemDto: CreateSystemDto) { async create(user: User, createSystemDto: CreateSystemDto) {
const sys = this.systemRepo.create(createSystemDto); const sys = this.systemRepo.create(createSystemDto);
sys.managers = [user]; sys.managers = [user];
@@ -34,11 +40,17 @@ export class SystemService {
} }
} }
findAll(user: User) { async findAll(user: User) {
return this.systemRepo.find({ let systems = await this.systemRepo.find({
where: { managers: { id: user.id } }, where: { managers: { id: user.id } },
order: { name: { direction: 'ASC' } }, order: { name: { direction: 'ASC' } },
}); });
if (this.isDevelopMode) {
systems = systems.filter(s => s.name.toLocaleLowerCase().includes('develop'));
}
return systems;
} }
findOne(id: string) { findOne(id: string) {
@@ -87,7 +99,7 @@ export class SystemService {
relations: ['settings'] relations: ['settings']
}); });
if (user.settings.sendSystemAccessMails) { if (user.settings.sendSystemAccessMails) {
this.mailService.sendAccessRemovedMail({to: manageObject.email, firstName: manageObject.email, systemName: sys.name}) this.mailService.sendAccessRemovedMail({to: user, system: sys})
} }
return sys.managers; return sys.managers;
} }
@@ -107,7 +119,7 @@ export class SystemService {
sys.managers.push(user); sys.managers.push(user);
await this.systemRepo.save(sys); await this.systemRepo.save(sys);
if (user.settings.sendSystemAccessMails) { if (user.settings.sendSystemAccessMails) {
this.mailService.sendAccessGrantedMail({to: user.username, firstName: user.firstName, systemName: sys.name}) this.mailService.sendAccessGrantedMail({to: user, system: sys})
} }
return sys.managers; return sys.managers;

View File

@@ -88,10 +88,39 @@
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"test": {
"builder": "@angular/build:unit-test"
},
"extract-i18n": { "extract-i18n": {
"builder": "@angular/build:extract-i18n" "builder": "@angular/build:extract-i18n"
} }
} }
} }
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
} }
} }

View File

@@ -1,11 +0,0 @@
import type { Config } from 'jest';
const jestConfig: Config = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
moduleNameMapper: {
'@ngxpert/hot-toast': '<rootDir>/mocks/modules/hot-toast',
},
};
export default jestConfig;

16142
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,38 +12,40 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ag-grid-community/locale": "^32.1.0", "@ag-grid-community/locale": "^35.1.0",
"@angular/animations": "^19.2.18", "@angular/animations": "^21.1.4",
"@angular/cdk": "^19.2.19", "@angular/cdk": "^21.1.4",
"@angular/common": "^19.2.18", "@angular/common": "^21.1.4",
"@angular/compiler": "^19.2.18", "@angular/compiler": "^21.1.4",
"@angular/core": "^19.2.18", "@angular/core": "^21.1.4",
"@angular/forms": "^19.2.18", "@angular/forms": "^21.1.4",
"@angular/material": "^19.2.19", "@angular/material": "^21.1.4",
"@angular/material-moment-adapter": "^19.2.19", "@angular/material-moment-adapter": "^21.1.4",
"@angular/platform-browser": "^19.2.18", "@angular/platform-browser": "^21.1.4",
"@angular/platform-browser-dynamic": "^19.2.18", "@angular/platform-browser-dynamic": "^21.1.4",
"@angular/router": "^19.2.18", "@angular/router": "^21.1.4",
"@ngneat/overview": "^6.0.0", "@ngneat/overview": "^7.0.0",
"@ngxpert/hot-toast": "^3.0.1", "@ngxpert/hot-toast": "^6.1.0",
"ag-grid-angular": "^32.1.0", "ag-grid-angular": "^35.1.0",
"ag-grid-community": "^32.1.0", "ag-grid-community": "^35.1.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.15.1" "zone.js": "~0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^19.2.20", "@analogjs/vitest-angular": "^2.2.3",
"@angular/cli": "^19.2.20", "@angular/build": "^21.1.4",
"@angular/compiler-cli": "^19.2.18", "@angular/cli": "^21.1.4",
"@angular/compiler-cli": "^21.1.4",
"@faker-js/faker": "^9.0.3", "@faker-js/faker": "^9.0.3",
"@types/jest": "^29.5.14", "@vitest/coverage-v8": "^4.0.18",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"jest": "^29.7.0", "jsdom": "^28.0.0",
"jest-preset-angular": "^14.4.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.16", "tailwindcss": "^3.4.16",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "~5.8.3" "typescript": "~5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
} }
} }

View File

@@ -1,22 +1,14 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component'; import { describe, it, expect, beforeEach } from 'vitest';
import { provideHttpClient } from '@angular/common/http';
describe('AppComponent', () => { describe('MathService', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent], beforeEach(() => {
providers: [ TestBed.configureTestingModule({});
provideHttpClient(),
provideHttpClient(),
]
}).compileComponents();
}); });
it('should create the app', () => { it('should add two numbers', () => {
const fixture = TestBed.createComponent(AppComponent); expect(5).toEqual(5)
const app = fixture.componentInstance;
expect(app).toBeTruthy();
}); });
}); });

View File

@@ -1,6 +1,16 @@
import { Component, LOCALE_ID } from '@angular/core'; import { Component, LOCALE_ID } from '@angular/core';
import { MAT_DATE_LOCALE } from '@angular/material/core'; import { MAT_DATE_LOCALE } from '@angular/material/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community';
ModuleRegistry.registerModules([ AllCommunityModule ]);
import { provideGlobalGridOptions } from 'ag-grid-community';
// Mark all grids as using legacy themes
provideGlobalGridOptions({
theme: "legacy",
});
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

View File

@@ -12,14 +12,14 @@
<mat-drawer-container class="example-container" autosize> <mat-drawer-container class="example-container" autosize>
<mat-drawer #drawer class="main_sidenav" mode="side" opened="true" style="border-right: 1px solid #dfdfdf"> <mat-drawer #drawer class="main_sidenav" mode="side" opened="true" style="border-right: 1px solid #dfdfdf">
<button mat-button routerLink="/" routerLinkActive="mat-elevation-z1" [routerLinkActiveOptions]="{exact: true}">Home</button> <button matButton routerLink="/" routerLinkActive="mat-elevation-z1" [routerLinkActiveOptions]="{exact: true}">Home</button>
<button mat-button routerLink="/keys" routerLinkActive="mat-elevation-z1">Schlüssel</button> <button matButton routerLink="/keys" routerLinkActive="mat-elevation-z1">Schlüssel</button>
<button mat-button routerLink="/cylinders" routerLinkActive="mat-elevation-z1">Zylinder</button> <button matButton routerLink="/cylinders" routerLinkActive="mat-elevation-z1">Zylinder</button>
<button mat-button routerLink="/systems" routerLinkActive="mat-elevation-z1">Schließanlagen</button> <button matButton routerLink="/systems" routerLinkActive="mat-elevation-z1">Schließanlagen</button>
@if (isAdmin) { @if (isAdmin) {
<button mat-button routerLink="/users" routerLinkActive="mat-elevation-z1">Alle User</button> <button matButton routerLink="/users" routerLinkActive="mat-elevation-z1">Alle User</button>
} }
<button mat-button (click)="openSidebar()">Einstellungen</button> <button matButton (click)="openSidebar()">Einstellungen</button>
</mat-drawer> </mat-drawer>
@@ -27,7 +27,7 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<!-- <div class="example-sidenav-content"> <!-- <div class="example-sidenav-content">
<button type="button" mat-button (click)="drawer.toggle()"> <button type="button" matButton (click)="drawer.toggle()">
Toggle sidenav Toggle sidenav
</button> </button>
</div> --> </div> -->

View File

@@ -3,6 +3,6 @@
Du bist nicht eingeloggt. Logge dich ein um zu Beginnen. Du bist nicht eingeloggt. Logge dich ein um zu Beginnen.
</p> </p>
<div> <div>
<button mat-raised-button color="primary" (click)="authService.routeToLogin()">Einloggen!</button> <button matButton="elevated" color="primary" (click)="authService.routeToLogin()">Einloggen!</button>
</div> </div>
</div> </div>

View File

@@ -25,8 +25,8 @@
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button mat-dialog-close color="warn">Abbrechen</button> <button matButton mat-dialog-close>Abbrechen</button>
<button mat-raised-button (click)="save()" [disabled]="createForm.disabled || createForm.invalid" color="accent"> <button matButton="elevated" (click)="save()" [disabled]="createForm.disabled || createForm.invalid" class="btn-primary">
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
Speichern Speichern
</button> </button>

View File

@@ -1,34 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateCylinderComponent } from './create-cylinder.component';
import { MatDialogRef } from '@angular/material/dialog';
import { HotToastService } from '@ngxpert/hot-toast';
import { ApiService } from '../../../../shared/api.service';
import { MockApiService } from '../../../../../../mocks/services/mock.api.service';
import { MockHotToastService } from '../../../../../../mocks/services/mock.hottoast.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CreateCylinderComponent', () => {
let component: CreateCylinderComponent;
let fixture: ComponentFixture<CreateCylinderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CreateCylinderComponent, NoopAnimationsModule],
providers: [
{ provide: ApiService, useClass: MockApiService },
{ provide: MatDialogRef, useValue: [] },
{ provide: HotToastService, useClass: MockHotToastService }
]
})
.compileComponents();
fixture = TestBed.createComponent(CreateCylinderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -3,7 +3,7 @@ import { FormGroup, FormControl, Validators, ReactiveFormsModule, FormsModule }
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { HotToastService } from '@ngxpert/hot-toast'; import { HotToastService } from '@ngxpert/hot-toast';
import { ApiService } from '../../../../shared/api.service'; import { ApiService } from '../../../../shared/api.service';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
@@ -12,7 +12,7 @@ import { MatIconModule } from '@angular/material/icon';
@Component({ @Component({
selector: 'app-create-cylinder', selector: 'app-create-cylinder',
imports: [CommonModule, MatFormFieldModule, MatInputModule, MatDialogModule, ReactiveFormsModule, FormsModule, MatSelectModule, MatButtonModule, MatIconModule], imports: [MatFormFieldModule, MatInputModule, MatDialogModule, ReactiveFormsModule, FormsModule, MatSelectModule, MatButtonModule, MatIconModule],
templateUrl: './create-cylinder.component.html', templateUrl: './create-cylinder.component.html',
styleUrl: './create-cylinder.component.scss' styleUrl: './create-cylinder.component.scss'
}) })

View File

@@ -1,7 +1,7 @@
<h2 mat-dialog-title>Zylinder <u>{{cylinder.name}}</u> löschen?</h2> <h2 mat-dialog-title>Zylinder <u>{{cylinder.name}}</u> löschen?</h2>
<mat-dialog-content> <mat-dialog-content>
<div class="warning-message"> <div class="warning-message">
<mat-icon color="warn">warning</mat-icon> <mat-icon>warning</mat-icon>
<p> <p>
<b>{{cylinder.name}}</b> wirklich entfernen? <b>{{cylinder.name}}</b> wirklich entfernen?
Alle Schlüssel die nur diesen Zylinder haben werden ebenfalls entfernt. Alle Schlüssel die nur diesen Zylinder haben werden ebenfalls entfernt.
@@ -13,8 +13,8 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="false">Abbrechen</button> <button matButton [mat-dialog-close]="false">Abbrechen</button>
<button mat-raised-button color="warn" [mat-dialog-close]="true"> <button matButton="elevated" [mat-dialog-close]="true">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
Entfernen Entfernen
</button> </button>

View File

@@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { Component, inject, LOCALE_ID } from '@angular/core'; import { Component, inject, LOCALE_ID } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
@@ -7,7 +7,7 @@ import { ICylinder } from '../../../../model/interface/cylinder.interface';
@Component({ @Component({
selector: 'app-delete-cylinder', selector: 'app-delete-cylinder',
imports: [MatDialogModule, MatButtonModule, CommonModule, MatIconModule], imports: [MatDialogModule, MatButtonModule, MatIconModule],
providers: [{ provide: LOCALE_ID, useValue: 'de-DE' },], providers: [{ provide: LOCALE_ID, useValue: 'de-DE' },],
templateUrl: './delete-cylinder.component.html', templateUrl: './delete-cylinder.component.html',
styleUrl: './delete-cylinder.component.scss' styleUrl: './delete-cylinder.component.scss'

View File

@@ -4,6 +4,6 @@
[gridOptions]="gridOptions!" [gridOptions]="gridOptions!"
/> />
<div class="floating-btn-container"> <div class="floating-btn-container">
<button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateCylinder()" color="accent" >Zylinder anlegen</button> <button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateCylinder()" >Zylinder anlegen</button>
<button mat-mini-fab disabled><mat-icon>inventory_2</mat-icon></button> <button mat-mini-fab disabled><mat-icon>inventory_2</mat-icon></button>
</div> </div>

View File

@@ -17,7 +17,7 @@
<p>Aktive Schlüssel</p> <p>Aktive Schlüssel</p>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-button routerLink="/keys">Verwalten</button> <button matButton routerLink="/keys">Verwalten</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
@@ -31,7 +31,7 @@
<p>Registrierte Zylinder</p> <p>Registrierte Zylinder</p>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-button routerLink="/cylinders">Verwalten</button> <button matButton routerLink="/cylinders">Verwalten</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
@@ -45,7 +45,7 @@
<p>Aktive Schließanlagen</p> <p>Aktive Schließanlagen</p>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-button routerLink="/systems">Verwalten</button> <button matButton routerLink="/systems">Verwalten</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
@@ -59,7 +59,7 @@
<p>Derzeit ausgegebene Schlüssel</p> <p>Derzeit ausgegebene Schlüssel</p>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-button routerLink="/keys">Verwalten</button> <button matButton routerLink="/keys">Verwalten</button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</div> </div>

View File

@@ -8,5 +8,5 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button [mat-dialog-close]="dataChanged">Schließen</button> <button matButton [mat-dialog-close]="dataChanged">Schließen</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -1,62 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HotToastService, provideHotToastConfig } from '@ngxpert/hot-toast';
import { from, map, of } from 'rxjs';
import { GridReadyEvent } from 'ag-grid-community';
import { MatDialog } from '@angular/material/dialog';
import { ArchiveComponent } from './archive.component';
import { ApiService } from '../../../../shared/api.service';
import { MockApiService } from '../../../../../../mocks/services/mock.api.service';
// Mocking the dependencies
jest.mock('@ngxpert/hot-toast', () => ({
HotToastService: jest.fn(),
}));
describe('ArchiveComponent', () => {
let component: ArchiveComponent;
let fixture: ComponentFixture<ArchiveComponent>;
const mockGridReadyEvent: GridReadyEvent = {
api: { setGridOption: jest.fn(), addEventListener: jest.fn() },
columnApi: { someColumnApiMethod: jest.fn() },
type: 'gridReady',
} as any;
const mockHotToastService = {
observe: jest.fn().mockImplementation(() => ({
loading: 'speichern...',
success: 'Änderungen gespeichert',
error: 'Änderungen konnten nicht gespeichert werden!',
subscribe: jest.fn().mockReturnValue(of([]))
}))
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ArchiveComponent, ],
providers: [
{ provide: HotToastService, useValue: mockHotToastService },
{ provide: ApiService, useClass: MockApiService },
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ArchiveComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.onGridReady(mockGridReadyEvent);
});
it('should create the archive', () => {
expect(component).toBeTruthy();
});
it('should load the data on start', () => {
expect(component['api'].getKeyArchive).toHaveBeenCalled();
});
});

View File

@@ -1,7 +1,7 @@
<h2 mat-dialog-title>Schlüssel <u>{{key.name}}</u> löschen?</h2> <h2 mat-dialog-title>Schlüssel <u>{{key.name}}</u> löschen?</h2>
<mat-dialog-content> <mat-dialog-content>
<div class="warning-message"> <div class="warning-message">
<mat-icon color="warn">warning</mat-icon> <mat-icon>warning</mat-icon>
<p> <p>
<b>{{key.name}}</b> wirklich entfernen? <b>{{key.name}}</b> wirklich entfernen?
</p> </p>
@@ -12,8 +12,8 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="false">Abbrechen</button> <button matButton [mat-dialog-close]="false">Abbrechen</button>
<button mat-raised-button color="warn" [mat-dialog-close]="true"> <button matButton="elevated" [mat-dialog-close]="true" class="btn-warning">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
Entfernen Entfernen
</button> </button>

View File

@@ -43,8 +43,11 @@
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button mat-dialog-close color="warn" >Schließen</button> <button matButton mat-dialog-close class="btn-warning">Schließen</button>
<button mat-button (click)="save()" [disabled]="handoverForm.invalid">Speichern</button> <button matButton="elevated" (click)="save()" class="btn-primary" [disabled]="handoverForm.invalid || handoverForm.pristine">
<mat-icon>save</mat-icon>
Speichern
</button>
</mat-dialog-actions> </mat-dialog-actions>
</mat-tab> </mat-tab>
@@ -59,7 +62,7 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button mat-dialog-close color="warn" >Schließen</button> <button matButton mat-dialog-close >Schließen</button>
</mat-dialog-actions> </mat-dialog-actions>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>

View File

@@ -8,7 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MAT_DATE_LOCALE, provideNativeDateAdapter } from '@angular/material/core'; import { MAT_DATE_LOCALE, provideNativeDateAdapter } from '@angular/material/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { CommonModule, DatePipe } from '@angular/common'; import { AsyncPipe, DatePipe } from '@angular/common';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { map, Observable, startWith, timestamp } from 'rxjs'; import { map, Observable, startWith, timestamp } from 'rxjs';
import { import {
@@ -24,10 +24,11 @@ import {MatTabsModule} from '@angular/material/tabs';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'; import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale';
import { AgGridAngular } from 'ag-grid-angular'; import { AgGridAngular } from 'ag-grid-angular';
import { MatIconModule } from '@angular/material/icon';
@Component({ @Component({
selector: 'app-handover-dialog', selector: 'app-handover-dialog',
imports: [FormsModule, MatTabsModule, AgGridAngular, ReactiveFormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatDialogModule, CommonModule, MatAutocompleteModule, MatProgressSpinnerModule, MatRadioModule], imports: [FormsModule, MatTabsModule, AgGridAngular, ReactiveFormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatDialogModule, MatAutocompleteModule, MatProgressSpinnerModule, MatRadioModule, AsyncPipe, MatIconModule],
providers: [ providers: [
provideNativeDateAdapter(), provideNativeDateAdapter(),
{ provide: LOCALE_ID, useValue: 'de-DE' }, { provide: LOCALE_ID, useValue: 'de-DE' },

View File

@@ -6,7 +6,7 @@
<mat-dialog-content> <mat-dialog-content>
<div class="warning-message"> <div class="warning-message">
@if(key.keyLost != null) { @if(key.keyLost != null) {
<mat-icon color="accent">report</mat-icon> <mat-icon >report</mat-icon>
<p> <p>
<b>{{key.name}}</b> wirklich als gefunden markieren? <b>{{key.name}}</b> wirklich als gefunden markieren?
</p> </p>
@@ -14,7 +14,7 @@
<small>Die Information, dass er am {{ key.keyLost| date:'shortDate' }} verloren wurde, wird gelöscht!</small> <small>Die Information, dass er am {{ key.keyLost| date:'shortDate' }} verloren wurde, wird gelöscht!</small>
</p> </p>
} @else { } @else {
<mat-icon color="warn">warning</mat-icon> <mat-icon>warning</mat-icon>
<p> <p>
<b>{{key.name}}</b> wirklich als verloren markieren? <b>{{key.name}}</b> wirklich als verloren markieren?
</p> </p>
@@ -25,15 +25,15 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="null">Abbrechen</button> <button matButton [mat-dialog-close]="null">Abbrechen</button>
@if(key.keyLost != null) { @if(key.keyLost != null) {
<button mat-raised-button color="accent" (click)="closeFound()"> <button matButton="elevated" (click)="closeFound()" class="btn-primary">
<mat-icon>report</mat-icon> <mat-icon>report</mat-icon>
Als gefunden melden Als gefunden melden
</button> </button>
} @else { } @else {
<button mat-raised-button color="warn" (click)="closeWithData()"> <button matButton="elevated" (click)="closeWithData()" class="btn-warning">
<mat-icon>report_problem</mat-icon> <mat-icon>report_problem</mat-icon>
Als verloren melden Als verloren melden
</button> </button>

View File

@@ -1,28 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LostKeyComponent } from './lost-key.component';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
describe('LostKeyComponent', () => {
let component: LostKeyComponent;
let fixture: ComponentFixture<LostKeyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LostKeyComponent],
providers: [
{ provide: MatDialogRef, useValue: [] },
{ provide: MAT_DIALOG_DATA, useValue: [] }
]
})
.compileComponents();
fixture = TestBed.createComponent(LostKeyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -7,5 +7,5 @@
/> />
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button [mat-dialog-close]="dataChanged">Schließen</button> <button matButton [mat-dialog-close]="dataChanged">Schließen</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -1,30 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LostKeysComponent } from './lost-keys.component';
import { ApiService } from '../../../../shared/api.service';
import { MockApiService } from '../../../../../../mocks/services/mock.api.service';
import { HotToastService } from '@ngxpert/hot-toast';
describe('LostKeysComponent', () => {
let component: LostKeysComponent;
let fixture: ComponentFixture<LostKeysComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LostKeysComponent],
providers: [
{ provide: ApiService, useClass: MockApiService },
HotToastService
]
})
.compileComponents();
fixture = TestBed.createComponent(LostKeysComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,4 +1,4 @@
import { CommonModule, DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { HotToastService } from '@ngxpert/hot-toast'; import { HotToastService } from '@ngxpert/hot-toast';
@@ -13,7 +13,7 @@ import { MatButtonModule } from '@angular/material/button';
@Component({ @Component({
selector: 'app-lost-keys', selector: 'app-lost-keys',
imports: [MatDialogModule, AgGridAngular, CommonModule, MatButtonModule], imports: [MatDialogModule, AgGridAngular, MatButtonModule],
providers: [DatePipe], providers: [DatePipe],
templateUrl: './lost-keys.component.html', templateUrl: './lost-keys.component.html',
styleUrl: './lost-keys.component.scss' styleUrl: './lost-keys.component.scss'

View File

@@ -41,8 +41,8 @@
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button mat-dialog-close color="warn">Abbrechen</button> <button matButton mat-dialog-close>Abbrechen</button>
<button (click)="save()" [disabled]="createForm.disabled || createForm.invalid" mat-raised-button color="accent"> <button (click)="save()" [disabled]="createForm.disabled || createForm.invalid" matButton="elevated" class="btn-primary">
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
Speichern Speichern
</button> </button>

View File

@@ -10,6 +10,6 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button color="warn" [mat-dialog-close]="null">Abbrechen</button> <button matButton [mat-dialog-close]="null">Abbrechen</button>
<button mat-button color="accent" [mat-dialog-close]="selectedCylinders">Übernehmen</button> <button matButton [mat-dialog-close]="selectedCylinders">Übernehmen</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -1,37 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SelectKeyCylinderComponent } from './select-key-cylinder.component';
import { HotToastService } from '@ngxpert/hot-toast';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MockHotToastService } from '../../../../../../mocks/services/mock.hottoast.service';
describe('SelectKeyCylinderComponent', () => {
let component: SelectKeyCylinderComponent;
let fixture: ComponentFixture<SelectKeyCylinderComponent>;
const mockHotToastService = {
info: jest.fn(),
error: jest.fn(),
success: jest.fn(),
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SelectKeyCylinderComponent],
providers: [
{ provide: HotToastService, useClass: MockHotToastService },
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] }
]
})
.compileComponents();
fixture = TestBed.createComponent(SelectKeyCylinderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -5,7 +5,7 @@
/> />
<div class="floating-btn-container"> <div class="floating-btn-container">
<button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateKey()" color="accent" >Schlüssel anlegen</button> <button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateKey()" >Schlüssel anlegen</button>
<button mat-mini-fab (click)="openArchive()" matTooltip="Archiv"><mat-icon>inventory_2</mat-icon></button> <button mat-mini-fab (click)="openArchive()" matTooltip="Archiv"><mat-icon>inventory_2</mat-icon></button>
<button mat-mini-fab (click)="openLostKeys()" class="lost icon-btn-xs" style="background-repeat: no-repeat; background-position: center;" matTooltip="Verlorene Schlüssel"></button> <button mat-mini-fab (click)="openLostKeys()" class="lost icon-btn-xs" style="background-repeat: no-repeat; background-position: center;" matTooltip="Verlorene Schlüssel"></button>
</div> </div>

View File

@@ -1,109 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeysComponent } from './keys.component';
import { HotToastService } from '@ngxpert/hot-toast';
import { of } from 'rxjs';
import { ApiService } from '../../shared/api.service';
import { GridReadyEvent } from 'ag-grid-community';
import { MatDialog } from '@angular/material/dialog';
import { MockApiService } from '../../../../mocks/services/mock.api.service';
describe('KeysComponent', () => {
let component: KeysComponent;
let fixture: ComponentFixture<KeysComponent>;
const mockGridReadyEvent: GridReadyEvent = {
api: { setGridOption: jest.fn(), addEventListener: jest.fn(), getGridOption: jest.fn() },
columnApi: { someColumnApiMethod: jest.fn() },
type: 'gridReady',
} as any;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [KeysComponent, ],
providers: [
{ provide: ApiService, useClass: MockApiService },
{ provide: MatDialog, useClass: MockMatDialog },
HotToastService
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KeysComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.onGridReady(mockGridReadyEvent);
});
it('should create the keyscomponent', () => {
expect(component).toBeTruthy();
});
it('should call getCylinders on ngOnInit', () => {
component.ngOnInit();
expect(component['api'].getCylinders).toHaveBeenCalled();
});
it('should call getKeys and set rowData when loadKeys is called', () => {
component.loadKeys();
expect(component['api'].getKeys).toHaveBeenCalled();
expect(component.gridApi.setGridOption).toHaveBeenCalledWith('rowData', [{ name: 'Key 1' }]);
});
it('should call deleteKey when deleteKey is triggered', () => {
const keyId = '123';
component.deleteKey(keyId);
expect(component['api'].deleteKey).toHaveBeenCalledWith(keyId);
});
it('should call updateKey on cellEditEnd when a value is changed', () => {
const mockEvent = {
data: { id: '1', name: 'Old Name' },
oldValue: 'Old Name',
newValue: 'New Name',
valueChanged: true,
};
component.cellEditEnd(mockEvent as any);
expect(component['api'].updateKey).toHaveBeenCalledWith(mockEvent.data);
});
it('should not call updateKey on cellEditEnd if value is not changed', () => {
const mockEvent = {
data: { id: '1', name: 'Old Name' },
oldValue: 'Old Name',
newValue: 'Old Name',
valueChanged: false,
};
component.cellEditEnd(mockEvent as any);
expect(component['api'].updateKey).not.toHaveBeenCalled();
});
it('should reload Keys after creation', () => {
component['dialog'].open = jest.fn().mockReturnValue({
afterClosed: jest.fn().mockReturnValue(of(true)),
})
component.openCreateKey();
expect(component['dialog'].open).toHaveBeenCalled();
expect(component['api'].getKeys).toHaveBeenCalledTimes(2)
})
it('should not reload Keys after cancellation', () => {
component['dialog'].open = jest.fn().mockReturnValue({
afterClosed: jest.fn().mockReturnValue(of(null)),
})
component.openCreateKey();
expect(component['dialog'].open).toHaveBeenCalled();
expect(component['api'].getKeys).toHaveBeenCalledTimes(1)
})
});
class MockMatDialog {
open = jest.fn().mockReturnValue({
afterClosed: jest.fn().mockReturnValue(of(true)),
})
};

View File

@@ -47,9 +47,7 @@ export class KeysComponent {
valueFormatter: (data: any) => { return data; }, valueFormatter: (data: any) => { return data; },
cellRenderer: (data: any) => {return data.value?.map((m: ICylinder) => m.name).join(', ')}, cellRenderer: (data: any) => {return data.value?.map((m: ICylinder) => m.name).join(', ')},
tooltipValueGetter: (data: any) => data.value?.map((m: ICylinder) => m.name).join(','), tooltipValueGetter: (data: any) => data.value?.map((m: ICylinder) => m.name).join(','),
onCellDoubleClicked(event) { onCellDoubleClicked(event) {},
},
cellEditorPopup: true, cellEditorPopup: true,
filterValueGetter: (params: any) => {return params.data.cylinder?.map((m: ICylinder) => m.name).join(', ')}, filterValueGetter: (params: any) => {return params.data.cylinder?.map((m: ICylinder) => m.name).join(', ')},
}, },

View File

@@ -3,12 +3,16 @@
} }
<div class="sidebar" [class.open]="isOpen"> <div class="sidebar" [class.open]="isOpen">
<div class="sidebar-header"> <div class="sidebar-header">
<div class="title_text">Einstellungen</div>
<button class="close-btn" (click)="closeSidebar()"></button> <button class="close-btn" (click)="closeSidebar()"></button>
</div> </div>
<div class="content" style="flex: 1 1 auto" > <div class="content" style="flex: 1 1 auto" >
<h4>Einstellungen</h4> <div class="px-4">
<div class="text-2xl">Allgemeine Einstellungen</div>
<div>Name, Login...:</div>
</div>
<form [formGroup]="userData" class="flex flex-col p-4"> <form [formGroup]="userData" class="flex flex-col p-4">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -23,12 +27,14 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Email</mat-label> <mat-label>Email</mat-label>
<input type="text" matInput formControlName="userName"> <input type="text" matInput formControlName="userName">
<mat-hint>Wird zum Login benötigt</mat-hint>
</mat-form-field> </mat-form-field>
<button mat-button [disabled]="userData.invalid" (click)="saveUser()">Speichern</button> <div class="spacer-y16"></div>
<button matButton="elevated" [disabled]="userData.invalid" (click)="saveUser()">Speichern</button>
</form> </form>
<div class="spacer-y32"></div>
<div class=" px-4"> <div class="px-4">
<div class="text-2xl">Emailbenachrichtigungen</div> <div class="text-2xl">Emailbenachrichtigungen</div>
<div>Sende Emails bei: </div> <div>Sende Emails bei: </div>
</div> </div>

View File

@@ -35,7 +35,10 @@
.sidebar-header { .sidebar-header {
padding: 1rem; padding: 1rem;
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
.title_text {
font-size: 2rem;
}
} }
.close-btn { .close-btn {

View File

@@ -1,34 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
import { ApiService } from '../../shared/api.service';
import { MockApiService } from '../../../../mocks/services/mock.api.service';
import { AuthService } from '../../core/auth/auth.service';
import { MockAuthService } from '../../../../mocks/services/mock.auth.service';
import { HotToastService } from '@ngxpert/hot-toast';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SettingsComponent, NoopAnimationsModule],
providers: [
{ provide: ApiService, useClass: MockApiService },
{ provide: AuthService, useClass: MockAuthService },
HotToastService
]
})
.compileComponents();
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -44,6 +44,9 @@ export class SettingsComponent {
} }
closeSidebar() { closeSidebar() {
if (this.isDirty()) {
this.toast.warning('Ungespeicherte Änderungen wurden verworfen.');
}
this.isOpen = false; this.isOpen = false;
} }
@@ -83,11 +86,13 @@ export class SettingsComponent {
next: () => { next: () => {
this.toast.success('Gespeichert') this.toast.success('Gespeichert')
this.isLoading = false; this.isLoading = false;
this.userSettings.markAsPristine();
} }
}); });
} }
saveUser() { saveUser() {
this.isDirty();
const user = this.authService.user; const user = this.authService.user;
user.firstName = this.$userData.firstName.value!; user.firstName = this.$userData.firstName.value!;
user.lastName = this.$userData.lastName.value!; user.lastName = this.$userData.lastName.value!;
@@ -95,8 +100,14 @@ export class SettingsComponent {
this.api.saveUser(user).subscribe({ this.api.saveUser(user).subscribe({
next: () => { next: () => {
this.toast.success('Gespeichert') this.toast.success('Gespeichert');
this.userSettings.markAsPristine()
} }
}) })
} }
isDirty(): boolean {
return this.userData.dirty || this.userSettings.dirty;
}
} }

View File

@@ -2,7 +2,7 @@
<h2 mat-dialog-title>Manager entfernen</h2> <h2 mat-dialog-title>Manager entfernen</h2>
<mat-dialog-content> <mat-dialog-content>
<div class="warning-message"> <div class="warning-message">
<mat-icon color="warn">warning</mat-icon> <mat-icon>warning</mat-icon>
<p> <p>
<b>{{ manager.firstName }} {{ manager.lastName }}</b> wirklich entfernen? <b>{{ manager.firstName }} {{ manager.lastName }}</b> wirklich entfernen?
Der Benutzer hat dann keinen Zugriff mehr auf diese Schließanlage mit allen ihren Daten. Der Benutzer hat dann keinen Zugriff mehr auf diese Schließanlage mit allen ihren Daten.
@@ -14,8 +14,8 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="false">Abbrechen</button> <button matButton [mat-dialog-close]="false">Abbrechen</button>
<button mat-raised-button color="warn" [mat-dialog-close]="true"> <button matButton="elevated" [mat-dialog-close]="true">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
Entfernen Entfernen
</button> </button>

View File

@@ -1,34 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemoveManagerPopupComponent } from './remove-manager-popup.component';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
describe('RemoveManagerPopupComponent', () => {
let component: RemoveManagerPopupComponent;
let fixture: ComponentFixture<RemoveManagerPopupComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RemoveManagerPopupComponent],
providers: [
{
provide: MatDialogRef,
useValue: []
},
{
provide: MAT_DIALOG_DATA,
useValue: ''
},
]
})
.compileComponents();
fixture = TestBed.createComponent(RemoveManagerPopupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -18,7 +18,7 @@
<mat-hint>Emailadresse des neuen Users eingeben</mat-hint> <mat-hint>Emailadresse des neuen Users eingeben</mat-hint>
<mat-icon matPrefix class="text-gray-400 mr-2">email</mat-icon> <mat-icon matPrefix class="text-gray-400 mr-2">email</mat-icon>
</mat-form-field> </mat-form-field>
<button mat-raised-button <button matButton="elevated"
color="primary" color="primary"
(click)="addManagerByEmail()" (click)="addManagerByEmail()"
[disabled]="email == '' || email == null"> [disabled]="email == '' || email == null">
@@ -29,5 +29,5 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button [mat-dialog-close]="true">Schließen</button> <button matButton [mat-dialog-close]="true">Schließen</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -6,26 +6,3 @@
--ag-selected-row-background-color: rgb(229 231 235); --ag-selected-row-background-color: rgb(229 231 235);
} }
.p-4 {
padding: 1rem;
}
.bg-gray-50 {
background-color: rgb(249 250 251);
}
.rounded-md {
border-radius: 0.375rem;
}
.shadow-sm {
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.text-gray-400 {
color: rgb(156 163 175);
}
.mr-2 {
margin-right: 0.5rem;
}

View File

@@ -1,68 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SystemManagerComponent } from './system-manager.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { AgGridAngular } from 'ag-grid-angular';
import { ApiService } from '../../../../shared/api.service';
import { HotToastService } from '@ngxpert/hot-toast';
import { MockApiService } from '../../../../../../mocks/services/mock.api.service';
import { GridReadyEvent } from 'ag-grid-community';
import { AuthService } from '../../../../core/auth/auth.service';
import { MockAuthService } from '../../../../../../mocks/services/mock.auth.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SystemManagerComponent', () => {
let component: SystemManagerComponent;
let fixture: ComponentFixture<SystemManagerComponent>;
let api: ApiService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SystemManagerComponent, AgGridAngular, MatDialogModule, NoopAnimationsModule],
providers: [
HotToastService,
{ provide: ApiService, useClass: MockApiService },
{
provide: MatDialogRef,
useValue: []
},
{
provide: MAT_DIALOG_DATA,
useValue: []
},
{ provide: AuthService, useClass: MockAuthService }
]
})
.compileComponents();
fixture = TestBed.createComponent(SystemManagerComponent);
component = fixture.componentInstance;
api = component['api']
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize gridApi and gridColumnApi on gridReady and fill data', () => {
// Mock des GridReadyEvent
let mockData = [{ id: 1, name: 'Test' }];
const mockGridReadyEvent: GridReadyEvent = {
api: { setGridOption: jest.fn() },
columnApi: { someColumnApiMethod: jest.fn() },
type: 'gridReady',
} as any;
// Methode aufrufen
component.onGridReady(mockGridReadyEvent);
// Assertions
expect(component.gridApi).toBe(mockGridReadyEvent.api);
expect(api.getSystemManagers).toHaveBeenCalled();
expect(component.gridApi.setGridOption).toHaveBeenCalled();
});
});

View File

@@ -8,7 +8,7 @@ import { ApiService } from '../../../../shared/api.service';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AuthService } from '../../../../core/auth/auth.service'; import { AuthService } from '../../../../core/auth/auth.service';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
@@ -18,7 +18,7 @@ import { MatIconModule } from '@angular/material/icon';
@Component({ @Component({
selector: 'app-system-manager', selector: 'app-system-manager',
imports: [AgGridAngular, MatDialogModule, MatButtonModule, MatInputModule, MatFormFieldModule, CommonModule, FormsModule, MatIconModule], imports: [AgGridAngular, MatDialogModule, MatButtonModule, MatInputModule, MatFormFieldModule, FormsModule, MatIconModule],
templateUrl: './system-manager.component.html', templateUrl: './system-manager.component.html',
styleUrl: './system-manager.component.scss' styleUrl: './system-manager.component.scss'
}) })

View File

@@ -14,8 +14,8 @@
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button mat-dialog-close color="warn">Abbrechen</button> <button matButton mat-dialog-close>Abbrechen</button>
<button mat-raised-button (click)="save()" [disabled]="createForm.disabled" color="accent"> <button matButton="elevated" (click)="save()" [disabled]="createForm.disabled" >
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
Speichern Speichern
</button> </button>

View File

@@ -1,66 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CreateSystemComponent } from './create.component';
import { ApiService } from '../../../shared/api.service';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { of, throwError } from 'rxjs';
import { HotToastService } from '@ngxpert/hot-toast';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MockApiService } from '../../../../../mocks/services/mock.api.service';
describe('CreateComponent', () => {
let component: CreateSystemComponent;
let fixture: ComponentFixture<CreateSystemComponent>;
let apiService: ApiService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CreateSystemComponent, NoopAnimationsModule, FormsModule, ReactiveFormsModule],
providers: [
HotToastService,
{ provide: ApiService, useClass: MockApiService },
{
provide: MatDialogRef,
useValue: []
},
{
provide: MAT_DIALOG_DATA,
useValue: []
}
]
})
.compileComponents();
fixture = TestBed.createComponent(CreateSystemComponent);
component = fixture.componentInstance;
apiService = component['api'];
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call apiService.createSystem when createSystem is called', () => {
expect(apiService.createSystem).not.toHaveBeenCalled();
component.createForm.setValue({ name: 'Test System' });
component.save();
expect(apiService.createSystem).toHaveBeenCalledWith({ name: 'Test System' });
});
it('should handle success response correctly', () => {
jest.spyOn(apiService, 'createSystem').mockReturnValue(of({}));
const toastSpy = jest.spyOn(component['toast'], 'observe');
component.createForm.setValue({ name: 'Test System' });
component.save();
expect(toastSpy).toHaveBeenCalled();
});
it('should handle error response correctly', () => {
jest.spyOn(apiService, 'createSystem').mockReturnValue(throwError(() => new Error('Test Error')));
const toastSpy = jest.spyOn(component['toast'], 'observe');
component.save();
expect(toastSpy).toHaveBeenCalled();
});
});

View File

@@ -5,5 +5,5 @@
/> />
<div class="floating-btn-container"> <div class="floating-btn-container">
<button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateSystem()" color="accent" >Schließanlage anlegen</button> <button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateSystem()" >Schließanlage anlegen</button>
</div> </div>

View File

@@ -1,54 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { SystemComponent } from './system.component';
import { ApiService } from '../../shared/api.service';
import { GridReadyEvent } from 'ag-grid-community';
import { of } from 'rxjs';
describe('SystemcomponentComponent', () => {
let component: SystemComponent;
let mockApiService: MockApiService;
beforeEach(async () => {
mockApiService = new MockApiService();
await TestBed.configureTestingModule({
imports: [SystemComponent],
providers: [
{ provide: ApiService, useValue: mockApiService }
]
}).compileComponents();
const fixture = TestBed.createComponent(SystemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the SystemComponent', () => {
expect(component).toBeTruthy();
});
it('should initialize gridApi and gridColumnApi on gridReady and fill data', () => {
// Mock des GridReadyEvent
let mockData = [{ id: 1, name: 'Test' }];
mockApiService.getSystems.mockReturnValue(of(mockData));
const mockGridReadyEvent: GridReadyEvent = {
api: { setGridOption: jest.fn() },
columnApi: { someColumnApiMethod: jest.fn() },
type: 'gridReady',
} as any;
// Methode aufrufen
component.onGridReady(mockGridReadyEvent);
// Assertions
expect(component.gridApi).toBe(mockGridReadyEvent.api);
expect(mockApiService.getSystems).toHaveBeenCalled();
expect(component.gridApi.setGridOption).toHaveBeenCalled();
});
});
class MockApiService {
getSystems = jest.fn();
}

View File

@@ -79,10 +79,7 @@ export class AgDeleteKeyComponent implements ICellRendererAngularComp {
}) })
).subscribe({ ).subscribe({
next: () => { next: () => {
let data = this.params.api.getGridOption("rowData"); this.setData();
data = data?.filter(d => d.id != this.key.id);
this.params.api.setGridOption("rowData", data);
this.params.api.setGridOption("loading", false);
}, },
error: () => { error: () => {
this.params.api.setGridOption("loading", false); this.params.api.setGridOption("loading", false);
@@ -122,8 +119,16 @@ export class AgDeleteKeyComponent implements ICellRendererAngularComp {
this.key.keyLost = n; this.key.keyLost = n;
this.params.api.refreshCells(); this.params.api.refreshCells();
this.api.updateKey(this.key).subscribe(); this.api.updateKey(this.key).subscribe();
this.setData();
} }
} }
}) })
} }
private setData() {
let data = this.params.api.getGridOption("rowData");
data = data?.filter(d => d.id != this.key.id);
this.params.api.setGridOption("rowData", data);
this.params.api.setGridOption("loading", false);
}
} }

View File

@@ -1,47 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AgSystemManagerComponent } from './ag-system-manager.component';
import { MatDialogModule } from '@angular/material/dialog';
import { AgGridAngular } from 'ag-grid-angular';
import { ApiService } from '../../../api.service';
import { of } from 'rxjs';
import { HotToastService } from '@ngxpert/hot-toast';
describe('AgSystemManagerComponent', () => {
let component: AgSystemManagerComponent;
let fixture: ComponentFixture<AgSystemManagerComponent>;
let mockApiService: MockApiService;
const mockHotToastService = {
observe: jest.fn().mockImplementation(() => ({
loading: 'speichern...',
success: 'Änderungen gespeichert',
error: 'Änderungen konnten nicht gespeichert werden!',
subscribe: jest.fn().mockReturnValue(of([]))
}))
};
beforeEach(async () => {
mockApiService = new MockApiService();
await TestBed.configureTestingModule({
imports: [AgSystemManagerComponent, AgGridAngular, MatDialogModule],
providers: [
{ provide: ApiService, useValue: mockApiService },
{ provide: HotToastService, useValue: mockHotToastService },
]
})
.compileComponents();
fixture = TestBed.createComponent(AgSystemManagerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
class MockApiService {
getSystems = jest.fn();
}

View File

@@ -158,3 +158,59 @@ div.ag-row {
{ {
border-right: none; border-right: none;
} }
.p-4 {
padding: 1rem;
}
.bg-gray-50 {
background-color: rgb(249 250 251);
}
.rounded-md {
border-radius: 0.375rem;
}
.shadow-sm {
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.text-gray-400 {
color: rgb(156 163 175);
}
.mr-2 {
margin-right: 0.5rem;
}
.spacer-y8 {
height: 8px;
}
.spacer-y16 {
height: 16px;
}
.spacer-y32 {
height: 32px;
}
.mat-mdc-button, .mat-mdc-raised-button:not(.mat-mdc-button-disabled) {
.mdc-button__label, .mat-icon {
color: #000;
}
}
.btn-warning:not(.mat-mdc-button-disabled) {
.mdc-button__label, .mat-icon {
color: rgb(197, 0, 0);
}
}
.btn-primary:not(.mat-mdc-button-disabled).mat-mdc-raised-button {
.mdc-button__label, .mat-icon {
color: rgb(32, 100, 0);
}
}

12
client/src/test-setup.ts Normal file
View File

@@ -0,0 +1,12 @@
import 'zone.js/testing';
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
// Wichtig: Initialisiert Angulars Test-Environment (wie früher in test.ts)
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);

View File

@@ -17,11 +17,7 @@
"importHelpers": true, "importHelpers": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
"useDefineForClassFields": false, "useDefineForClassFields": false
"lib": [
"ES2022",
"dom"
]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,

View File

@@ -1,13 +1,5 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "types": ["vitest/globals", "node"]
"module": "CommonJS", }
"types": ["jest"]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
} }

15
client/vitest.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['src/**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
reportsDirectory: './coverage'
}
}
});