diff --git a/api/src/app.module.ts b/api/src/app.module.ts index a9622d5..35d7a40 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -8,6 +8,7 @@ import { AuthGuard } from './core/guards/auth.guard'; import { UserModule } from './modules/user/user.module'; import { RoleModule } from './modules/role/role.module'; import { KeyModule } from './modules/key/key.module'; +import { CustomerModule } from './modules/customer/customer.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { KeyModule } from './modules/key/key.module'; UserModule, RoleModule, KeyModule, + CustomerModule, ], controllers: [AppController], providers: [AppService, AuthGuard], diff --git a/api/src/model/dto/handover-key.dto.ts b/api/src/model/dto/handover-key.dto.ts new file mode 100644 index 0000000..fea422d --- /dev/null +++ b/api/src/model/dto/handover-key.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty } from 'class-validator'; +import { Customer, Key } from '../entitites'; + +export class HandoverKeyDto { + @IsNotEmpty() + key: Key; + + @IsNotEmpty() + customer: Customer; + + @IsNotEmpty() + direction: 'out' | 'return'; +} diff --git a/api/src/model/dto/index.ts b/api/src/model/dto/index.ts index 3dabed8..8de6036 100644 --- a/api/src/model/dto/index.ts +++ b/api/src/model/dto/index.ts @@ -1,3 +1,4 @@ export * from './login.dto'; export * from './auth-code.dto'; export * from './create-key-system.dto'; +export * from './handover-key.dto'; diff --git a/api/src/model/entitites/customer.entity.ts b/api/src/model/entitites/customer.entity.ts new file mode 100644 index 0000000..0e29193 --- /dev/null +++ b/api/src/model/entitites/customer.entity.ts @@ -0,0 +1,33 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Key } from './key.entity'; +import { KeySystem } from './system.entity'; +import { ICustomer } from '../interface'; + +@Entity() +export class Customer implements ICustomer { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, unique: false }) + name: string; + + @OneToMany(() => Key, (key) => key.customer) + keys: Key[]; + + @ManyToOne(() => KeySystem) + system: KeySystem; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updatet_at' }) + updatedAt: Date; +} diff --git a/api/src/model/entitites/index.ts b/api/src/model/entitites/index.ts index 5143349..6751e80 100644 --- a/api/src/model/entitites/index.ts +++ b/api/src/model/entitites/index.ts @@ -4,3 +4,5 @@ export * from './role.entity'; export * from './cylinder.entity'; export * from './key.entity'; export * from './key_activity.entity'; +export * from './customer.entity'; +export * from './key-handout.entity'; diff --git a/api/src/model/entitites/key-handout.entity.ts b/api/src/model/entitites/key-handout.entity.ts new file mode 100644 index 0000000..7d3dbd4 --- /dev/null +++ b/api/src/model/entitites/key-handout.entity.ts @@ -0,0 +1,38 @@ +import { + BeforeInsert, + Column, + CreateDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Key } from './key.entity'; +import { Customer } from './customer.entity'; + +@Entity() +export class KeyHandout { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Key) + key: Key; + + @Column() + direction: 'out' | 'return'; + + @ManyToOne(() => Customer) + customer: Customer; + + @Column() + timestamp: Date; + + @CreateDateColumn() + created: Date; + + @BeforeInsert() + insertTimestamp() { + if (this.timestamp == null) { + this.timestamp = new Date(); + } + } +} diff --git a/api/src/model/entitites/key.entity.ts b/api/src/model/entitites/key.entity.ts index 0624815..5e26cf2 100644 --- a/api/src/model/entitites/key.entity.ts +++ b/api/src/model/entitites/key.entity.ts @@ -8,6 +8,7 @@ import { } from 'typeorm'; import { Cylinder } from './cylinder.entity'; import { IKey } from '../interface/key.interface'; +import { Customer } from './customer.entity'; @Entity() export class Key implements IKey { @@ -26,6 +27,9 @@ export class Key implements IKey { @ManyToOne(() => Cylinder, (cylinder) => cylinder.keys) cylinder: Cylinder; + @ManyToOne(() => Customer, (customer) => customer.keys) + customer: Customer; + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/api/src/model/entitites/key_activity.entity.ts b/api/src/model/entitites/key_activity.entity.ts index a179006..0393616 100644 --- a/api/src/model/entitites/key_activity.entity.ts +++ b/api/src/model/entitites/key_activity.entity.ts @@ -8,6 +8,7 @@ import { import { User } from './user.entity'; import { IKey } from '../interface/key.interface'; import { Cylinder } from './cylinder.entity'; +import { Customer } from './customer.entity'; @Entity() export class KeyActivity implements IKey { @@ -29,6 +30,9 @@ export class KeyActivity implements IKey { @ManyToOne(() => Cylinder) cylinder: Cylinder; + @ManyToOne(() => Customer) + customer: Customer; + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/api/src/model/interface/customer.interface.ts b/api/src/model/interface/customer.interface.ts new file mode 100644 index 0000000..e3fcbcb --- /dev/null +++ b/api/src/model/interface/customer.interface.ts @@ -0,0 +1,11 @@ +import { Key } from 'readline'; +import { KeySystem } from '../entitites/system.entity'; + +export interface ICustomer { + id: string; + name: string; + keys: Key[]; + system: KeySystem; + createdAt: Date; + updatedAt: Date; +} diff --git a/api/src/model/interface/index.ts b/api/src/model/interface/index.ts index c6a8758..5532747 100644 --- a/api/src/model/interface/index.ts +++ b/api/src/model/interface/index.ts @@ -2,3 +2,4 @@ export * from './user.interface'; export * from './external-access-token.payload.interface'; export * from './payload.interface'; export * from './key-system.interface'; +export * from './customer.interface'; diff --git a/api/src/model/interface/key.interface.ts b/api/src/model/interface/key.interface.ts index 4e5d830..2cf819a 100644 --- a/api/src/model/interface/key.interface.ts +++ b/api/src/model/interface/key.interface.ts @@ -1,4 +1,4 @@ -import { Cylinder } from '../entitites'; +import { Customer, Cylinder } from '../entitites'; export interface IKey { id: string; @@ -6,5 +6,6 @@ export interface IKey { nr: number; handedOut: boolean; cylinder: Cylinder; + customer: Customer; createdAt: Date; } diff --git a/api/src/model/repositories/customer.repository.ts b/api/src/model/repositories/customer.repository.ts new file mode 100644 index 0000000..4747776 --- /dev/null +++ b/api/src/model/repositories/customer.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { Repository, DataSource } from 'typeorm'; +import { Customer } from '../entitites'; + +@Injectable() +export class CustomerRepository extends Repository { + constructor(dataSource: DataSource) { + super(Customer, dataSource.createEntityManager()); + } +} diff --git a/api/src/model/repositories/index.ts b/api/src/model/repositories/index.ts index 49cf029..3d023ad 100644 --- a/api/src/model/repositories/index.ts +++ b/api/src/model/repositories/index.ts @@ -5,3 +5,4 @@ export * from './system.repository'; export * from './cylinder.repository'; export * from './key.repository'; export * from './key_activity.repository'; +export * from './customer.repository'; diff --git a/api/src/model/repositories/key-handout.repository.ts b/api/src/model/repositories/key-handout.repository.ts new file mode 100644 index 0000000..a11a27f --- /dev/null +++ b/api/src/model/repositories/key-handout.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { Repository, DataSource } from 'typeorm'; +import { KeyHandout } from '../entitites'; + +@Injectable() +export class KeyHandoutRepository extends Repository { + constructor(dataSource: DataSource) { + super(KeyHandout, dataSource.createEntityManager()); + } +} diff --git a/api/src/modules/customer/customer.controller.ts b/api/src/modules/customer/customer.controller.ts new file mode 100644 index 0000000..ae1abf7 --- /dev/null +++ b/api/src/modules/customer/customer.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common'; +import { AuthGuard } from 'src/core/guards/auth.guard'; +import { AuthenticatedRequest } from 'src/model/interface/authenticated-request.interface'; +import { CustomerService } from './customer.service'; + +@Controller('customer') +@UseGuards(AuthGuard) +export class CustomerController { + constructor(private service: CustomerService) {} + + @Get() + getCustomers(@Req() req: AuthenticatedRequest) { + return this.service.getCustomers(req.user); + } + + @Post() + createCustomer(@Req() req: AuthenticatedRequest, @Body() body: any) { + return this.service.createCustomer(req.user, body); + } +} diff --git a/api/src/modules/customer/customer.module.ts b/api/src/modules/customer/customer.module.ts new file mode 100644 index 0000000..a107a3f --- /dev/null +++ b/api/src/modules/customer/customer.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CustomerController } from './customer.controller'; +import { CustomerService } from './customer.service'; +import { DatabaseModule } from 'src/shared/database/database.module'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + controllers: [CustomerController], + providers: [CustomerService], + imports: [DatabaseModule, AuthModule], +}) +export class CustomerModule {} diff --git a/api/src/modules/customer/customer.service.ts b/api/src/modules/customer/customer.service.ts new file mode 100644 index 0000000..618e2eb --- /dev/null +++ b/api/src/modules/customer/customer.service.ts @@ -0,0 +1,24 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { Customer } from 'src/model/entitites'; +import { IUser } from 'src/model/interface'; +import { CustomerRepository } from 'src/model/repositories'; + +@Injectable() +export class CustomerService { + constructor(private customerRepository: CustomerRepository) {} + + createCustomer(user: IUser, data: any) { + if (!user || !data.name || data.name.length == 0 || !data.system) { + throw new HttpException('invalid', HttpStatus.UNPROCESSABLE_ENTITY); + } + + return this.customerRepository.save(this.customerRepository.create(data)); + } + + getCustomers(user: IUser): Promise { + return this.customerRepository.find({ + where: { system: { managers: { id: user.id } } }, + order: { name: { direction: 'ASC' } }, + }); + } +} diff --git a/api/src/modules/key/key.controller.ts b/api/src/modules/key/key.controller.ts index efa6dc9..1962aaa 100644 --- a/api/src/modules/key/key.controller.ts +++ b/api/src/modules/key/key.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, + Param, Post, Put, Req, @@ -12,6 +13,7 @@ import { AuthenticatedRequest } from 'src/model/interface/authenticated-request. import { AuthGuard } from 'src/core/guards/auth.guard'; import { Key } from 'src/model/entitites'; import { CreateKeySystemDto } from 'src/model/dto/create-key-system.dto'; +import { HandoverKeyDto } from 'src/model/dto'; @UseGuards(AuthGuard) @Controller('key') @@ -34,7 +36,24 @@ export class KeyController { } @Post('system') - createKeySystem(@Req() req: AuthenticatedRequest, @Body() body: CreateKeySystemDto) { + createKeySystem( + @Req() req: AuthenticatedRequest, + @Body() body: CreateKeySystemDto, + ) { return this.service.createKeySystem(req.user, body); } + + @Post(':id/handover') + handoutKey( + @Req() req: AuthenticatedRequest, + @Body() body: any, + @Param('id') id: string, + ) { + return this.service.handoverKey(req.user, body, id); + } + + @Get(':id/handover') + getKeyHandouts(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.service.getKeyHandovers(req.user, id); + } } diff --git a/api/src/modules/key/key.service.ts b/api/src/modules/key/key.service.ts index c29cf84..f9607cf 100644 --- a/api/src/modules/key/key.service.ts +++ b/api/src/modules/key/key.service.ts @@ -1,12 +1,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateKeySystemDto } from 'src/model/dto'; import { Cylinder, Key, User } from 'src/model/entitites'; +import { IUser } from 'src/model/interface'; import { CylinderRepository, KeyActivityRepository, KeyRepository, KeySystemRepository, } from 'src/model/repositories'; +import { KeyHandoutRepository } from 'src/model/repositories/key-handout.repository'; @Injectable() export class KeyService { @@ -15,12 +17,13 @@ export class KeyService { private readonly cylinderRepository: CylinderRepository, private readonly systemRepo: KeySystemRepository, private activityRepo: KeyActivityRepository, + private handoverRepo: KeyHandoutRepository, ) {} async getUsersKeys(user: User): Promise { return this.keyrepository.find({ where: { cylinder: { system: { managers: { id: user.id } } } }, - relations: ['cylinder', 'cylinder.system'], + relations: ['cylinder', 'cylinder.system', 'customer'], }); } @@ -74,4 +77,35 @@ export class KeyService { throw new HttpException(e.code, HttpStatus.UNPROCESSABLE_ENTITY); } } + + async handoverKey(user: IUser, data: any, keyID: string) { + const key: Key = await this.keyrepository.findOneOrFail({ + where: { id: keyID, cylinder: { system: { managers: { id: user.id } } } }, + }); + + key.handedOut = data.direction == 'out'; + this.keyrepository.save(key); + + return this.handoverRepo.save( + this.handoverRepo.create({ + customer: data.customer, + direction: data.direction, + timestamp: data.timestamp, + key: key, + }), + ); + } + + getKeyHandovers(user: User, keyID: string) { + return this.handoverRepo.find({ + where: { + key: { cylinder: { system: { managers: { id: user.id } } }, id: keyID }, + }, + order: { + timestamp: { direction: 'DESC' }, + created: { direction: 'DESC' }, + }, + relations: ['customer'], + }); + } } diff --git a/api/src/modules/role/role.controller.spec.ts b/api/src/modules/role/role.controller.spec.ts deleted file mode 100644 index 4b8b85f..0000000 --- a/api/src/modules/role/role.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { RoleController } from './role.controller'; - -describe('RoleController', () => { - let controller: RoleController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [RoleController], - }).compile(); - - controller = module.get(RoleController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/api/src/modules/role/role.service.spec.ts b/api/src/modules/role/role.service.spec.ts deleted file mode 100644 index e1e0c00..0000000 --- a/api/src/modules/role/role.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { RoleService } from './role.service'; - -describe('RoleService', () => { - let service: RoleService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [RoleService], - }).compile(); - - service = module.get(RoleService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/api/src/shared/database/database.module.ts b/api/src/shared/database/database.module.ts index cfc723e..d33150b 100644 --- a/api/src/shared/database/database.module.ts +++ b/api/src/shared/database/database.module.ts @@ -1,8 +1,18 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Cylinder, Key, KeyActivity, Role, SSOUser, User } from 'src/model/entitites'; +import { + Customer, + Cylinder, + Key, + KeyActivity, + KeyHandout, + Role, + SSOUser, + User, +} from 'src/model/entitites'; import { KeySystem } from 'src/model/entitites/system.entity'; import { + CustomerRepository, CylinderRepository, KeyActivityRepository, KeyRepository, @@ -11,8 +21,19 @@ import { SsoUserRepository, UserRepository, } from 'src/model/repositories'; +import { KeyHandoutRepository } from 'src/model/repositories/key-handout.repository'; -const ENTITIES = [User, SSOUser, Role, KeySystem, Key, Cylinder, KeyActivity]; +const ENTITIES = [ + User, + SSOUser, + Role, + KeySystem, + Key, + Cylinder, + KeyActivity, + Customer, + KeyHandout, +]; const REPOSITORIES = [ UserRepository, SsoUserRepository, @@ -21,6 +42,8 @@ const REPOSITORIES = [ KeyRepository, CylinderRepository, KeyActivityRepository, + CustomerRepository, + KeyHandoutRepository, ]; @Module({ diff --git a/client/package-lock.json b/client/package-lock.json index c520eaa..df644f1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "@angular/core": "^18.0.0", "@angular/forms": "^18.0.0", "@angular/material": "^18.2.4", + "@angular/material-moment-adapter": "^18.2.9", "@angular/platform-browser": "^18.0.0", "@angular/platform-browser-dynamic": "^18.0.0", "@angular/router": "^18.0.0", @@ -355,9 +356,10 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.4.tgz", - "integrity": "sha512-o+TuxZDqStfkviEkCR05pVyP6R2RIruEs/45Cms76hlsIheMoxRaxir/yrHdh4tZESJJhcO/EVE+aymNIRWAfg==", + "version": "18.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.9.tgz", + "integrity": "sha512-hV2dXpvy2TLwCsRtI/ZXkb2EoaJiellRr+kbcnKwO15LFoz3mTAOhKtsvu7yOyURkaPiI605qiIZrPP4zLL1qw==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -498,15 +500,16 @@ } }, "node_modules/@angular/material": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.4.tgz", - "integrity": "sha512-F09145mI/EAHY9ngdnQTo3pFRmUoU/50i6cmddtL4cse0WidatoodQr0gZCksxhmpJgRy5mTcjh/LU2hShOgcA==", + "version": "18.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.9.tgz", + "integrity": "sha512-M2oCgPPIMMd6BLgEJCD+FvdC7gRDeCjj9yktNn3ctHmkKUWRvpJ3xRBH/WjVXb+9fPCCW1iNwZI7+bN1fHE7cA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.4", + "@angular/cdk": "18.2.9", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -514,6 +517,20 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material-moment-adapter": { + "version": "18.2.9", + "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-18.2.9.tgz", + "integrity": "sha512-GjvqMoVcPPP1xpqMPSKEL1eSSfG2omULTdYnN3xFUroKmo8ZPS9+rgcbIi3At3ErnWctayEB0BUycoZwYtwg2A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^18.0.0 || ^19.0.0", + "@angular/material": "18.2.9", + "moment": "^2.18.1" + } + }, "node_modules/@angular/platform-browser": { "version": "18.2.4", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.4.tgz", @@ -9207,6 +9224,16 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index 166d1c2..0a48e7e 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@angular/core": "^18.0.0", "@angular/forms": "^18.0.0", "@angular/material": "^18.2.4", + "@angular/material-moment-adapter": "^18.2.9", "@angular/platform-browser": "^18.0.0", "@angular/platform-browser-dynamic": "^18.0.0", "@angular/router": "^18.0.0", diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 5c30dae..0833d95 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,16 +1,15 @@ -import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Component, inject, LOCALE_ID } from '@angular/core'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; import { RouterOutlet } from '@angular/router'; - - - @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet,], providers: [ { provide: LOCALE_ID, useValue: 'de-DE' }, + { provide: MAT_DATE_LOCALE, useValue: 'de-DE' } ], templateUrl: './app.component.html', styleUrl: './app.component.scss' @@ -18,8 +17,6 @@ import { RouterOutlet } from '@angular/router'; export class AppComponent { title = 'client'; - - private http: HttpClient = inject(HttpClient); constructor() { @@ -28,9 +25,4 @@ export class AppComponent { ngOnInit(): void { } - - - - - } diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index ba30d7e..83078c2 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHotToastConfig } from '@ngxpert/hot-toast'; @@ -17,5 +17,7 @@ export const appConfig: ApplicationConfig = { autoClose: true, dismissible: false, duration: 5000 - }), provideAnimationsAsync()] + }), + provideAnimationsAsync() +] }; diff --git a/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.html b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.html new file mode 100644 index 0000000..f6103bd --- /dev/null +++ b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.html @@ -0,0 +1,77 @@ +@if (isLoading) { +
+ +
+} + +

Übergaben {{ data.name }}

+ + +
Historie:
+ + + + + + + + @if (handovers.length == 0) { + + + + + + } + @for (item of handovers; track $index) { + + + + + + } + + +
KundeDatum + Übergabe +
---
{{ item.customer.name }}{{ item.timestamp | date}}{{ item.direction == 'out' ? 'Ausgabe' : 'Rückgabe'}}
+ +
+
Neue Übergabe anlegen:
+ + + Kunde + + + @for (option of filteredCustomers | async; track option) { + {{option.name}} + } + + Wähle den Empfänger oder tippe einen neuen Namen ein + + +
+ Der Schlüssel wurde + + Ausgegeben + Zurückgegeben + +
+ + + Datum der Übergabe + + TT/MM/JJJJ + + + +
+ + +
+ + + + \ No newline at end of file diff --git a/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.scss b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.scss new file mode 100644 index 0000000..1e69252 --- /dev/null +++ b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.scss @@ -0,0 +1,24 @@ +:host { + width: min(calc(100vw - 24px), 700px); + max-height: calc(100vh - 24px); + display: flex; + flex-direction: column; +} + +form { + align-items: stretch; + justify-content: stretch; +} + +:host { + position: relative; +} + +.handouts{ + //margin-top: 32px; + width: 100%; +} + +th, td { + text-align: start; +} \ No newline at end of file diff --git a/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.spec.ts b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.spec.ts new file mode 100644 index 0000000..a5a324b --- /dev/null +++ b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HandoverDialogComponent } from './handover-dialog.component'; + +describe('HandoverDialogComponent', () => { + let component: HandoverDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HandoverDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HandoverDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.ts b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.ts new file mode 100644 index 0000000..2a99d34 --- /dev/null +++ b/client/src/app/modules/keys/components/handover-dialog/handover-dialog.component.ts @@ -0,0 +1,193 @@ +import { Component, inject, LOCALE_ID } from '@angular/core'; +import { ApiService } from '../../../../shared/api.service'; +import { IKey } from '../../../../model/interface/key.interface'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MAT_DATE_LOCALE, provideNativeDateAdapter } from '@angular/material/core'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { map, Observable, startWith, timestamp } from 'rxjs'; +import { + MatBottomSheet, + MatBottomSheetModule, + MatBottomSheetRef, +} from '@angular/material/bottom-sheet'; +import {MatListModule} from '@angular/material/list'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatRadioModule} from '@angular/material/radio'; +import { HotToastService } from '@ngxpert/hot-toast'; + +@Component({ + selector: 'app-handover-dialog', + standalone: true, + imports: [FormsModule, ReactiveFormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatDialogModule, CommonModule, MatAutocompleteModule, MatProgressSpinnerModule, MatRadioModule], + providers: [ + provideNativeDateAdapter(), + { provide: LOCALE_ID, useValue: 'de-DE' }, + { provide: MAT_DATE_LOCALE, useValue: 'de-DE' }, + ], + templateUrl: './handover-dialog.component.html', + styleUrl: './handover-dialog.component.scss' +}) +export class HandoverDialogComponent { + + private api: ApiService = inject(ApiService); + readonly dialogRef = inject(MatDialogRef); + readonly data = inject(MAT_DIALOG_DATA); + private _bottomSheet = inject(MatBottomSheet); + private toast: HotToastService = inject(HotToastService); + + isLoading: boolean = false; + + customers: { name: string, id: string }[] = []; + filteredCustomers: Observable = new Observable(); + + handovers: any[] = []; + + + handoverForm = new FormGroup({ + customer: new FormControl(null, Validators.required), + key: new FormControl(this.data), + direction: new FormControl('out', Validators.required), + timestamp: new FormControl(new Date(), Validators.required) + }); + + ngOnInit() { + this.loadData(); + } + + loadData() { + this.isLoading = true; + const promises: Observable[] = [ + this.getHandovers(), + this.loadCustomers() + ]; + + Promise.all(promises).then(() => { + this.isLoading = false; + }) + + } + + getHandovers() { + const promise = this.api.getHandovers(this.data.id) + + promise.subscribe({ + next: n => { + this.handovers = n; + if (n && n.length > 0) { + this.handoverForm.controls.customer.patchValue(n[0].customer.name); + this.handoverForm.controls.direction.patchValue(n[0].direction == 'out' ? 'return' : 'out') + } + } + }); + + return promise; + } + + loadCustomers() { + const promise = this.api.getCustomers() + + promise.subscribe({ + next: customers => { + this.customers = customers; + this.filteredCustomers = this.handoverForm.controls.customer.valueChanges.pipe( + startWith(''), + map(value => this._filter(value || '')), + ); + } + }); + + return promise; + + } + + private _filter(value: string): any[] { + const filterValue = value.toLowerCase(); + + return this.customers.filter(option => option.name.toLowerCase().includes(filterValue) || option.id.toLowerCase().includes(filterValue)); + } + + save() { + const val = this.handoverForm.value; + const dto = { + key: this.data, + customer: this.customers.find(c => c.name == val.customer || c.id == val.customer), + timestamp: val.timestamp, + direction: val.direction + } + + if (dto.customer == null) { + this._bottomSheet.open(BottomSheetCreateCustomer).afterDismissed().subscribe({ + next: async n => { + if (!n) { return; } + await this.createCustomer(val.customer); + this.saveIt(dto); + } + }) + } else { + this.saveIt(dto); + } + } + + saveIt(data: any) { + this.api.handoverKey(data) + .pipe( + this.toast.observe({ + loading: 'Speichern...', + error: 'Konnte nicht gespeichert werden. Bitte versuche es später erneut', + success: 'Gespeichert' + }) + ) + .subscribe({ + next: n => { + this.dialogRef.close(data.direction == 'out') + } + }) + } + + createCustomer(name: string) { + this.isLoading = true; + this.api.createCustomer({ name, system: this.data.cylinder.system}).subscribe({ + next: n => { + this.isLoading = false; + } + }) + } + + +} + + + +@Component({ + template: ` + + + Anlegen + Neuen Empfänger anlegen + + + + Abbrechen + Zurück zur Auswahl + + + + `, + standalone: true, + imports: [MatInputModule, MatListModule], +}) +export class BottomSheetCreateCustomer { + private _bottomSheetRef = + inject>(MatBottomSheetRef); + + openLink(event: MouseEvent, data?: boolean): void { + this._bottomSheetRef.dismiss(data); + event.preventDefault(); + } +} \ No newline at end of file diff --git a/client/src/app/modules/keys/keys.component.ts b/client/src/app/modules/keys/keys.component.ts index 7672826..bf37d2c 100644 --- a/client/src/app/modules/keys/keys.component.ts +++ b/client/src/app/modules/keys/keys.component.ts @@ -9,6 +9,7 @@ import { HotToastService } from '@ngxpert/hot-toast'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { CreateKeyComponent } from './create/create.component'; +import { AgOpenHandoutComponent } from '../../shared/ag-grid/components/ag-open-handout/ag-open-handout.component'; @Component({ selector: 'app-keys', @@ -32,7 +33,12 @@ export class KeysComponent { localeText: AG_GRID_LOCALE_DE, rowData: [], columnDefs: [ - { field: 'handedOut' , headerName: 'Ausgegeben', width: 100,editable: true, filter: true, headerTooltip: 'Ausgegeben' }, + { + cellRenderer: AgOpenHandoutComponent, + width: 100, + headerName: 'Übergabe' + }, + { field: 'handedOut' , headerName: 'Ausgegeben', width: 100, editable: false, filter: false, headerTooltip: 'Ausgegeben' }, { field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true }, { field: 'nr' , headerName: 'Schlüsselnummer', flex: 1, editable: true, filter: true }, { field: 'cylinder' , headerName: 'Zylinder', flex: 1, editable: true, filter: true, cellRenderer: (data: any) => {return data.value?.name}, cellEditor: 'agSelectCellEditor', @@ -60,9 +66,10 @@ export class KeysComponent { , type: 'date' , cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' , tooltipValueGetter: (data: any) => this.datePipe.transform(new Date(data.value), 'medium') - }, + } ], loading: true, + rowHeight: 48, } ngOnInit(): void { @@ -71,7 +78,6 @@ export class KeysComponent { this.cylinders = n; } }) - this.api.postKeySystem({ name: 'Development' }).subscribe() } loadKeys() { @@ -79,7 +85,6 @@ export class KeysComponent { this.api.getKeys().subscribe(res => { this.gridApi.setGridOption("rowData", res); this.gridApi.setGridOption("loading", false); - res.map((r: any) => console.log(r.updatedAt)) }) } @@ -91,8 +96,6 @@ export class KeysComponent { cellEditEnd(event: CellEditingStoppedEvent) { const key: IKey = event.data; - console.log(event) - if (!event.valueChanged || event.newValue == event.oldValue) { return; } this.gridApi.setGridOption("loading", true); diff --git a/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.html b/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.html new file mode 100644 index 0000000..5df3305 --- /dev/null +++ b/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.html @@ -0,0 +1 @@ +
diff --git a/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.scss b/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.scss new file mode 100644 index 0000000..0708e2b --- /dev/null +++ b/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.scss @@ -0,0 +1,11 @@ +:host { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 100%; +} + +.handover { + background-image: url('../../../../../assets/img/handover.svg'); +} \ No newline at end of file diff --git a/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.ts b/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.ts new file mode 100644 index 0000000..2a34237 --- /dev/null +++ b/client/src/app/shared/ag-grid/components/ag-open-handout/ag-open-handout.component.ts @@ -0,0 +1,44 @@ +import { Component, HostBinding, inject } from '@angular/core'; +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; +import { IKey } from '../../../../model/interface/key.interface'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { HandoverDialogComponent } from '../../../../modules/keys/components/handover-dialog/handover-dialog.component'; + +@Component({ + selector: 'app-ag-open-handout', + standalone: true, + imports: [MatDialogModule], + templateUrl: './ag-open-handout.component.html', + styleUrl: './ag-open-handout.component.scss' +}) +export class AgOpenHandoutComponent implements ICellRendererAngularComp { + private dialog: MatDialog = inject(MatDialog); + key!: IKey; + params!: ICellRendererParams; + + agInit(params: ICellRendererParams): void { + this.params = params; + this.key = params.data; + } + refresh(params: ICellRendererParams): boolean { + return false; + } + + + openDialog() { + this.dialog.open(HandoverDialogComponent, { + data: this.key, + autoFocus: false, + maxWidth: '100vw', + maxHeight: '100vh' + }).afterClosed().subscribe({ + next: n => { + if (n != null) { + this.key.handedOut = n; + this.params.api.refreshCells(); + } + } + }) + } +} diff --git a/client/src/app/shared/api.service.ts b/client/src/app/shared/api.service.ts index 714c9c0..a506d72 100644 --- a/client/src/app/shared/api.service.ts +++ b/client/src/app/shared/api.service.ts @@ -43,4 +43,21 @@ export class ApiService { postKeySystem(keySystem: any) { return this.http.post('api/key/system', keySystem); } + + handoverKey(data: any) { + return this.http.post(`api/key/${data.key.id}/handover`, data); + } + + getHandovers(keyID: string): Observable { + return this.http.get(`api/key/${keyID}/handover`); + } + + createCustomer(data: { name: string, system: any}) { + return this.http.post('api/customer', data); + } + + getCustomers(): Observable { + return this.http.get('api/customer') + } + } diff --git a/client/src/assets/img/handover.svg b/client/src/assets/img/handover.svg new file mode 100644 index 0000000..04efff2 --- /dev/null +++ b/client/src/assets/img/handover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/styles.scss b/client/src/styles.scss index bb6a1ee..bbd9ee6 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -8,6 +8,50 @@ html, body { background-color: #e2e2e2; } +.flex { + display: flex; + align-items: center; + justify-content: center; +} + +.flex-column { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.icon-btn-sm { + box-shadow: 0 0 0 4px transparent,0 1px 2px #0c111d11; + border: 1px solid #d0d5dd; + background-color: white; + cursor: pointer; + padding: 4px; + box-sizing: border-box; + border-radius: 6px; + background-size: 28px; + background-position: center; + background-repeat: no-repeat; + width: 38px; + height: 38px; + +} + +.loading-spinner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.4); + z-index: 1000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + + /* Core Data Grid CSS */ @import "ag-grid-community/styles/ag-grid.css"; /* Quartz Theme Specific CSS */