diff --git a/backend/src/base/location/location-db.service.ts b/backend/src/base/location/location-db.service.ts index 2291a8b..597da35 100644 --- a/backend/src/base/location/location-db.service.ts +++ b/backend/src/base/location/location-db.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { LocationEntity } from './location.entity'; -import { TreeRepository } from 'typeorm'; +import { SelectQueryBuilder, TreeRepository } from 'typeorm'; import { DeepPartial } from 'typeorm/common/DeepPartial'; @Injectable() @@ -11,8 +11,21 @@ export class LocationDbService { private readonly repo: TreeRepository, ) {} - public async getCount() { - return this.repo.count(); + private searchQueryBuilder( + query: SelectQueryBuilder, + searchTerm: string, + ): SelectQueryBuilder { + return query.where('l.name ilike :searchTerm', { + searchTerm: `%${searchTerm}%`, + }); + } + + public async getCount(searchTerm?: string) { + let query = this.repo.createQueryBuilder('l'); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + return query.getCount(); } public async findAll( @@ -20,6 +33,7 @@ export class LocationDbService { limit?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { let query = this.repo .createQueryBuilder('l') @@ -27,6 +41,10 @@ export class LocationDbService { .limit(limit ?? 100) .offset(offset ?? 0); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + if (sortCol) { if (sortCol.startsWith('parent.')) { query = query.orderBy( diff --git a/backend/src/base/location/location.controller.ts b/backend/src/base/location/location.controller.ts index 36641b7..1737941 100644 --- a/backend/src/base/location/location.controller.ts +++ b/backend/src/base/location/location.controller.ts @@ -12,6 +12,7 @@ import { LocationGetQueryDto } from './dto/location-get-query.dto'; import { LocationCreateDto } from './dto/location-create.dto'; import { LocationUpdateDto } from './dto/location-update.dto'; import { PaginationDto } from '../../shared/dto/pagination.dto'; +import { SearchDto } from '../../shared/dto/search.dto'; @Controller('location') export class LocationController { @@ -23,8 +24,8 @@ export class LocationController { responseType: CountDto, roles: [Role.LocationView], }) - public getCount(): Promise { - return this.service.getCount(); + public getCount(@Query() search: SearchDto): Promise { + return this.service.getCount(search.searchTerm); } @Endpoint(EndpointType.GET, { @@ -36,12 +37,14 @@ export class LocationController { public async getAll( @Query() pagination: PaginationDto, @Query() querys: LocationGetQueryDto, + @Query() search: SearchDto, ): Promise { return this.service.findAll( pagination.offset, pagination.limit, querys.sortCol, querys.sortDir, + search.searchTerm, ); } diff --git a/backend/src/base/location/location.service.ts b/backend/src/base/location/location.service.ts index 70e577b..4e6544d 100644 --- a/backend/src/base/location/location.service.ts +++ b/backend/src/base/location/location.service.ts @@ -20,12 +20,14 @@ export class LocationService { limit?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { const locations = await this.dbService.findAll( offset, limit, sortCol, sortDir, + searchTerm, ); return plainToInstance(LocationDto, locations); } @@ -44,8 +46,8 @@ export class LocationService { } } - public async getCount() { - const count = await this.dbService.getCount(); + public async getCount(searchTerm?: string) { + const count = await this.dbService.getCount(searchTerm); return plainToInstance(CountDto, { count }); } diff --git a/backend/src/inventory/device-group/device-group-db.service.spec.ts b/backend/src/inventory/device-group/device-group-db.service.spec.ts index 5e03e99..fdf9859 100644 --- a/backend/src/inventory/device-group/device-group-db.service.spec.ts +++ b/backend/src/inventory/device-group/device-group-db.service.spec.ts @@ -85,7 +85,10 @@ describe('DeviceGroupDbService', () => { expect(repoMock.createQueryBuilder).toHaveBeenCalled(); expect(repoMock.createQueryBuilder().limit).toHaveBeenCalledWith(10); expect(repoMock.createQueryBuilder().offset).toHaveBeenCalledWith(10); - expect(repoMock.createQueryBuilder().orderBy).toHaveBeenCalledWith('dg.name', 'ASC'); + expect(repoMock.createQueryBuilder().orderBy).toHaveBeenCalledWith( + 'dg.name', + 'ASC', + ); expect(repoMock.createQueryBuilder().getMany).toHaveBeenCalled(); }); }); diff --git a/backend/src/inventory/device-group/device-group-db.service.ts b/backend/src/inventory/device-group/device-group-db.service.ts index f94fb30..afcc8ab 100644 --- a/backend/src/inventory/device-group/device-group-db.service.ts +++ b/backend/src/inventory/device-group/device-group-db.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; import { DeepPartial } from 'typeorm/common/DeepPartial'; import { DeviceGroupEntity } from './device-group.entity'; @@ -11,8 +11,21 @@ export class DeviceGroupDbService { private readonly repo: Repository, ) {} - public async getCount() { - return this.repo.count(); + private searchQueryBuilder( + query: SelectQueryBuilder, + searchTerm: string, + ): SelectQueryBuilder { + return query.where('dg.name ilike :searchTerm', { + searchTerm: `%${searchTerm}%`, + }); + } + + public async getCount(searchTerm?: string) { + let query = this.repo.createQueryBuilder('dg'); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + return query.getCount(); } public async findAll( @@ -20,12 +33,17 @@ export class DeviceGroupDbService { limit?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { let query = this.repo .createQueryBuilder('dg') .limit(limit ?? 100) .offset(offset ?? 0); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + if (sortCol) { query = query.orderBy(`dg.${sortCol}`, sortDir ?? 'ASC'); } else { diff --git a/backend/src/inventory/device-group/device-group.controller.ts b/backend/src/inventory/device-group/device-group.controller.ts index 6cb7eff..fea158b 100644 --- a/backend/src/inventory/device-group/device-group.controller.ts +++ b/backend/src/inventory/device-group/device-group.controller.ts @@ -12,6 +12,7 @@ import { DeviceGroupUpdateDto } from './dto/device-group-update.dto'; import { DeviceGroupCreateDto } from './dto/device-group-create.dto'; import { DeviceGroupGetQueryDto } from './dto/device-group-get-query.dto'; import { PaginationDto } from '../../shared/dto/pagination.dto'; +import { SearchDto } from '../../shared/dto/search.dto'; @Controller('device-group') export class DeviceGroupController { @@ -23,8 +24,8 @@ export class DeviceGroupController { responseType: CountDto, roles: [Role.DeviceTypeView], }) - public getCount(): Promise { - return this.service.getCount(); + public getCount(@Query() search: SearchDto): Promise { + return this.service.getCount(search.searchTerm); } @Endpoint(EndpointType.GET, { @@ -36,12 +37,14 @@ export class DeviceGroupController { public async getAll( @Query() pagination: PaginationDto, @Query() querys: DeviceGroupGetQueryDto, + @Query() search: SearchDto, ): Promise { return this.service.findAll( pagination.offset, pagination.limit, querys.sortCol, querys.sortDir, + search.searchTerm, ); } diff --git a/backend/src/inventory/device-group/device-group.service.ts b/backend/src/inventory/device-group/device-group.service.ts index 58179ca..6c36807 100644 --- a/backend/src/inventory/device-group/device-group.service.ts +++ b/backend/src/inventory/device-group/device-group.service.ts @@ -19,12 +19,14 @@ export class DeviceGroupService { limit?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { const entities = await this.dbService.findAll( offset, limit, sortCol, sortDir, + searchTerm, ); return plainToInstance(DeviceGroupDto, entities); } @@ -43,8 +45,8 @@ export class DeviceGroupService { } } - public async getCount() { - const count = await this.dbService.getCount(); + public async getCount(searchTerm?: string) { + const count = await this.dbService.getCount(searchTerm); return plainToInstance(CountDto, { count }); } diff --git a/backend/src/inventory/device-type/device-type-db.service.spec.ts b/backend/src/inventory/device-type/device-type-db.service.spec.ts index 22ae566..360f13b 100644 --- a/backend/src/inventory/device-type/device-type-db.service.spec.ts +++ b/backend/src/inventory/device-type/device-type-db.service.spec.ts @@ -102,7 +102,10 @@ describe('DeviceTypeDbService', () => { expect(repoMock.createQueryBuilder).toHaveBeenCalled(); expect(repoMock.createQueryBuilder().limit).toHaveBeenCalledWith(10); expect(repoMock.createQueryBuilder().offset).toHaveBeenCalledWith(10); - expect(repoMock.createQueryBuilder().orderBy).toHaveBeenCalledWith('dt.name', 'ASC'); + expect(repoMock.createQueryBuilder().orderBy).toHaveBeenCalledWith( + 'dt.name', + 'ASC', + ); expect(repoMock.createQueryBuilder().getMany).toHaveBeenCalled(); }); }); diff --git a/backend/src/inventory/device-type/device-type-db.service.ts b/backend/src/inventory/device-type/device-type-db.service.ts index 042c781..8edefe0 100644 --- a/backend/src/inventory/device-type/device-type-db.service.ts +++ b/backend/src/inventory/device-type/device-type-db.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; import { DeviceTypeEntity } from './device-type.entity'; import { DeepPartial } from 'typeorm/common/DeepPartial'; @@ -11,8 +11,21 @@ export class DeviceTypeDbService { private readonly repo: Repository, ) {} - public async getCount() { - return this.repo.count(); + private searchQueryBuilder( + query: SelectQueryBuilder, + searchTerm: string, + ): SelectQueryBuilder { + return query.where('dt.name ilike :searchTerm', { + searchTerm: `%${searchTerm}%`, + }); + } + + public async getCount(searchTerm?: string) { + let query = this.repo.createQueryBuilder('dt'); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + return query.getCount(); } public async findAll( @@ -20,12 +33,17 @@ export class DeviceTypeDbService { limit?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { let query = this.repo .createQueryBuilder('dt') .limit(limit ?? 100) .offset(offset ?? 0); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + if (sortCol) { query = query.orderBy(`dt.${sortCol}`, sortDir ?? 'ASC'); } else { diff --git a/backend/src/inventory/device-type/device-type.controller.ts b/backend/src/inventory/device-type/device-type.controller.ts index 2b21927..a0ac1af 100644 --- a/backend/src/inventory/device-type/device-type.controller.ts +++ b/backend/src/inventory/device-type/device-type.controller.ts @@ -12,6 +12,7 @@ import { DeviceTypeUpdateDto } from './dto/device-type-update.dto'; import { DeviceTypeGetQueryDto } from './dto/device-type-get-query.dto'; import { CountDto } from '../../shared/dto/count.dto'; import { PaginationDto } from '../../shared/dto/pagination.dto'; +import { SearchDto } from '../../shared/dto/search.dto'; @Controller('device-type') export class DeviceTypeController { @@ -23,8 +24,8 @@ export class DeviceTypeController { responseType: CountDto, roles: [Role.DeviceTypeView], }) - public getCount(): Promise { - return this.service.getCount(); + public getCount(@Query() search: SearchDto): Promise { + return this.service.getCount(search.searchTerm); } @Endpoint(EndpointType.GET, { @@ -36,12 +37,14 @@ export class DeviceTypeController { public async getAll( @Query() pagination: PaginationDto, @Query() querys: DeviceTypeGetQueryDto, + @Query() search: SearchDto, ): Promise { return this.service.findAll( pagination.offset, pagination.limit, querys.sortCol, querys.sortDir, + search.searchTerm, ); } diff --git a/backend/src/inventory/device-type/device-type.service.ts b/backend/src/inventory/device-type/device-type.service.ts index 6795e75..8f06bee 100644 --- a/backend/src/inventory/device-type/device-type.service.ts +++ b/backend/src/inventory/device-type/device-type.service.ts @@ -19,12 +19,14 @@ export class DeviceTypeService { limit?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { const entities = await this.dbService.findAll( offset, limit, sortCol, sortDir, + searchTerm, ); return plainToInstance(DeviceTypeDto, entities); } @@ -43,8 +45,8 @@ export class DeviceTypeService { } } - public async getCount() { - const count = await this.dbService.getCount(); + public async getCount(searchTerm?: string) { + const count = await this.dbService.getCount(searchTerm); return plainToInstance(CountDto, { count }); } diff --git a/backend/src/inventory/device/device-db.service.ts b/backend/src/inventory/device/device-db.service.ts index 7c234b2..c82a4c6 100644 --- a/backend/src/inventory/device/device-db.service.ts +++ b/backend/src/inventory/device/device-db.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; import { DeepPartial } from 'typeorm/common/DeepPartial'; import { DeviceEntity } from './device.entity'; @@ -11,8 +11,21 @@ export class DeviceDbService { private readonly repo: Repository, ) {} - public async getCount() { - return this.repo.count(); + private searchQueryBuilder( + query: SelectQueryBuilder, + searchTerm: string, + ): SelectQueryBuilder { + return query.where('d.name ilike :searchTerm', { + searchTerm: `%${searchTerm}%`, + }); + } + + public async getCount(searchTerm?: string) { + let query = this.repo.createQueryBuilder('d'); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + return query.getCount(); } public async findAll( @@ -23,6 +36,7 @@ export class DeviceDbService { locationId?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { let query = this.repo .createQueryBuilder('d') @@ -33,6 +47,10 @@ export class DeviceDbService { .leftJoinAndSelect('d.location', 'l') .leftJoinAndSelect('l.parent', 'lp'); + if (searchTerm) { + query = this.searchQueryBuilder(query, searchTerm); + } + if (sortCol) { if (sortCol.startsWith('type.')) { query = query.orderBy( diff --git a/backend/src/inventory/device/device.controller.ts b/backend/src/inventory/device/device.controller.ts index d454719..2b1e6d3 100644 --- a/backend/src/inventory/device/device.controller.ts +++ b/backend/src/inventory/device/device.controller.ts @@ -12,6 +12,7 @@ import { DeviceGetQueryDto } from './dto/device-get-query.dto'; import { DeviceUpdateDto } from './dto/device-update.dto'; import { DeviceCreateDto } from './dto/device-create.dto'; import { PaginationDto } from '../../shared/dto/pagination.dto'; +import { SearchDto } from '../../shared/dto/search.dto'; @Controller('device') export class DeviceController { @@ -23,8 +24,8 @@ export class DeviceController { responseType: CountDto, roles: [Role.DeviceView], }) - public getCount(): Promise { - return this.service.getCount(); + public getCount(@Query() search: SearchDto): Promise { + return this.service.getCount(search.searchTerm); } @Endpoint(EndpointType.GET, { @@ -36,6 +37,7 @@ export class DeviceController { public async getAll( @Query() pagination: PaginationDto, @Query() querys: DeviceGetQueryDto, + @Query() search: SearchDto, ): Promise { return this.service.findAll( pagination.offset, @@ -45,6 +47,7 @@ export class DeviceController { querys.locationId, querys.sortCol, querys.sortDir, + search.searchTerm, ); } diff --git a/backend/src/inventory/device/device.service.ts b/backend/src/inventory/device/device.service.ts index c25d758..80e328e 100644 --- a/backend/src/inventory/device/device.service.ts +++ b/backend/src/inventory/device/device.service.ts @@ -22,6 +22,7 @@ export class DeviceService { locationId?: number, sortCol?: string, sortDir?: 'ASC' | 'DESC', + searchTerm?: string, ) { const entities = await this.dbService.findAll( offset, @@ -31,6 +32,7 @@ export class DeviceService { locationId, sortCol, sortDir, + searchTerm, ); return plainToInstance(DeviceDto, entities); } @@ -49,8 +51,8 @@ export class DeviceService { } } - public async getCount() { - const count = await this.dbService.getCount(); + public async getCount(searchTerm?: string) { + const count = await this.dbService.getCount(searchTerm); return plainToInstance(CountDto, { count }); } diff --git a/backend/src/shared/dto/search.dto.ts b/backend/src/shared/dto/search.dto.ts new file mode 100644 index 0000000..e4b3836 --- /dev/null +++ b/backend/src/shared/dto/search.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class SearchDto { + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + searchTerm?: string; +} diff --git a/frontend/package.json b/frontend/package.json index 8428ecd..2ed6266 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "generate:backend": "openapi-generator-cli generate -i ../openapi/backend.yml -g typescript-angular -o client/backend", + "generate:backend": "openapi-generator-cli generate -i ../openapi/backend.yml -g typescript-angular -o client/backend --additional-properties=useSingleRequestParameter=true", "lint": "ng lint" }, "private": true, @@ -50,4 +50,4 @@ "typescript": "~5.7.2", "typescript-eslint": "8.27.0" } -} \ No newline at end of file +} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 4fa21f2..856b897 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -21,6 +21,7 @@ import {authConfig} from './core/auth/auth-config'; import {authModuleConfig} from './core/auth/auth-module-config'; import {de as deDE} from 'date-fns/locale'; import {registerLocaleData} from '@angular/common'; +import {provideAppConfigs} from './app.configs'; registerLocaleData(de); @@ -47,5 +48,6 @@ export const appConfig: ApplicationConfig = { importProvidersFrom(FormsModule), provideAnimationsAsync(), {provide: BASE_PATH, useValue: ''}, + provideAppConfigs(), ] }; diff --git a/frontend/src/app/app.configs.ts b/frontend/src/app/app.configs.ts new file mode 100644 index 0000000..5314b67 --- /dev/null +++ b/frontend/src/app/app.configs.ts @@ -0,0 +1,17 @@ +import {EnvironmentProviders, InjectionToken, makeEnvironmentProviders} from '@angular/core'; + +export const SEARCH_DEBOUNCE_TIME = new InjectionToken('SEARCH_DEBOUNCE_TIME'); +export const SELECT_ITEMS_COUNT = new InjectionToken('SELECT_ITEMS_COUNT'); + +export function provideAppConfigs(): EnvironmentProviders { + return makeEnvironmentProviders([ + { + provide: SEARCH_DEBOUNCE_TIME, + useValue: 300, + }, + { + provide: SELECT_ITEMS_COUNT, + useValue: 20, + }, + ]); +} diff --git a/frontend/src/app/pages/administration/groups/group-create/group-create.service.ts b/frontend/src/app/pages/administration/groups/group-create/group-create.service.ts index 9726fdb..e3d7231 100644 --- a/frontend/src/app/pages/administration/groups/group-create/group-create.service.ts +++ b/frontend/src/app/pages/administration/groups/group-create/group-create.service.ts @@ -18,7 +18,7 @@ export class GroupCreateService { create(rawValue: GroupCreateDto): void { this.createLoading.set(true); - this.groupService.groupControllerCreateGroup(rawValue) + this.groupService.groupControllerCreateGroup({groupCreateDto: rawValue}) .subscribe({ next: (group) => { this.createLoading.set(false); diff --git a/frontend/src/app/pages/administration/groups/group-detail/group-detail.service.ts b/frontend/src/app/pages/administration/groups/group-detail/group-detail.service.ts index 6e6a773..a0c3856 100644 --- a/frontend/src/app/pages/administration/groups/group-detail/group-detail.service.ts +++ b/frontend/src/app/pages/administration/groups/group-detail/group-detail.service.ts @@ -40,10 +40,10 @@ export class GroupDetailService { this.usersTransferLoading.set(true); this.rolesTransferLoading.set(true); - this.groupService.groupControllerGetGroup(id) + this.groupService.groupControllerGetGroup({id}) .pipe( tap((group) => this.group.set(group)), - mergeMap(() => this.groupService.groupControllerGetMembers(id)), + mergeMap(() => this.groupService.groupControllerGetMembers({id})), tap((data) => this.users.set(data.users.map((user) => ({ user, member: data.members.some(x => x.id === user.id) @@ -82,7 +82,7 @@ export class GroupDetailService { const group = this.group(); if (group) { this.updateLoading.set(true); - this.groupService.groupControllerUpdateGroup(group.id, rawValue) + this.groupService.groupControllerUpdateGroup({id: group.id, groupUpdateDto: rawValue}) .subscribe({ next: (group) => { this.updateLoading.set(false); @@ -101,7 +101,7 @@ export class GroupDetailService { const group = this.group(); if (group) { this.deleteLoading.set(true); - this.groupService.groupControllerDeleteGroup(group.id) + this.groupService.groupControllerDeleteGroup({id: group.id}) .subscribe({ next: () => { this.deleteLoading.set(false); @@ -120,13 +120,13 @@ export class GroupDetailService { this.usersTransferLoading.set(true); from([users]).pipe( mergeMap((users) => - forkJoin(users.map((user) => add ? - this.groupService.groupControllerAddMemberToGroup(this.group()!.id, user) : - this.groupService.groupControllerRemoveMemberFromGroup(this.group()!.id, user))) + forkJoin(users.map((userId) => add ? + this.groupService.groupControllerAddMemberToGroup({id: this.group()!.id, userId}) : + this.groupService.groupControllerRemoveMemberFromGroup({id: this.group()!.id, userId}))) ), - mergeMap(() => this.groupService.groupControllerGetGroup(this.group()!.id)), + mergeMap(() => this.groupService.groupControllerGetGroup({id: this.group()!.id})), tap((group) => this.group.set(group)), - mergeMap(() => this.groupService.groupControllerGetMembers(this.group()!.id)), + mergeMap(() => this.groupService.groupControllerGetMembers({id: this.group()!.id})), tap((data) => this.users.set(data.users.map((user) => ({ user, member: data.members.some(x => x.id === user.id) @@ -148,10 +148,10 @@ export class GroupDetailService { from([roles]).pipe( mergeMap((roles) => forkJoin(roles.map((role) => add ? - this.groupService.groupControllerAddRoleToGroup(this.group()!.id, role) : - this.groupService.groupControllerRemoveRoleFromGroup(this.group()!.id, role))) + this.groupService.groupControllerAddRoleToGroup({id: this.group()!.id, role}) : + this.groupService.groupControllerRemoveRoleFromGroup({id: this.group()!.id, role}))) ), - mergeMap(() => this.groupService.groupControllerGetGroup(this.group()!.id)), + mergeMap(() => this.groupService.groupControllerGetGroup({id: this.group()!.id})), tap((group) => this.group.set(group)), mergeMap(() => this.groupService.groupControllerGetRoles()), tap((data) => this.roles.set(data.map((role) => ({ diff --git a/frontend/src/app/pages/administration/groups/groups.service.ts b/frontend/src/app/pages/administration/groups/groups.service.ts index 6aabf46..20c11c0 100644 --- a/frontend/src/app/pages/administration/groups/groups.service.ts +++ b/frontend/src/app/pages/administration/groups/groups.service.ts @@ -20,7 +20,10 @@ export class GroupsService { this.groupsLoading.set(true); this.groupService.groupControllerGetCount() .subscribe((count) => this.total.set(count.count)); - this.groupService.groupControllerGetGroups(this.itemsPerPage(), this.page() * this.itemsPerPage()) + this.groupService.groupControllerGetGroups({ + limit: this.itemsPerPage(), + offset: this.page() * this.itemsPerPage(), + }) .subscribe({ next: (groups) => { this.groups.set(groups); diff --git a/frontend/src/app/pages/administration/users/user-create/user-create.service.ts b/frontend/src/app/pages/administration/users/user-create/user-create.service.ts index 042861e..4393031 100644 --- a/frontend/src/app/pages/administration/users/user-create/user-create.service.ts +++ b/frontend/src/app/pages/administration/users/user-create/user-create.service.ts @@ -18,7 +18,7 @@ export class UserCreateService { create(rawValue: UserCreateDto) { this.createLoading.set(true); - this.userService.userControllerCreateUser(rawValue) + this.userService.userControllerCreateUser({userCreateDto: rawValue}) .subscribe({ next: (user) => { this.createLoading.set(false); diff --git a/frontend/src/app/pages/administration/users/user-detail/user-detail.service.ts b/frontend/src/app/pages/administration/users/user-detail/user-detail.service.ts index 46fb7b4..9fb9158 100644 --- a/frontend/src/app/pages/administration/users/user-detail/user-detail.service.ts +++ b/frontend/src/app/pages/administration/users/user-detail/user-detail.service.ts @@ -34,10 +34,10 @@ export class UserDetailService { load(id: string) { this.loading.set(true); - this.userService.userControllerGetUser(id) + this.userService.userControllerGetUser({id}) .pipe( tap((user) => this.user.set(user)), - mergeMap(() => this.userService.userControllerGetGroups(id)), + mergeMap(() => this.userService.userControllerGetGroups({id})), tap((data) => this.groups.set(data.groups.map((group) => ({ group, member: data.members.some(x => x.id === group.id) @@ -63,7 +63,7 @@ export class UserDetailService { const user = this.user(); if (user) { this.updateLoading.set(true); - this.userService.userControllerUpdateUser(user.id, rawValue) + this.userService.userControllerUpdateUser({id: user.id, userUpdateDto: rawValue}) .subscribe({ next: (user) => { this.updateLoading.set(false); @@ -82,13 +82,13 @@ export class UserDetailService { this.groupsTransferLoading.set(true); from([groups]).pipe( mergeMap((users) => - forkJoin(users.map((group) => add ? - this.userService.userControllerAddUserToGroup(this.user()!.id, group) : - this.userService.userControllerRemoveUserFromGroup(this.user()!.id, group))) + forkJoin(users.map((groupId) => add ? + this.userService.userControllerAddUserToGroup({id: this.user()!.id, groupId}) : + this.userService.userControllerRemoveUserFromGroup({id: this.user()!.id, groupId}))) ), - mergeMap(() => this.userService.userControllerGetUser(this.user()!.id)), + mergeMap(() => this.userService.userControllerGetUser({id: this.user()!.id})), tap((user) => this.user.set(user)), - mergeMap(() => this.userService.userControllerGetGroups(this.user()!.id)), + mergeMap(() => this.userService.userControllerGetGroups({id: this.user()!.id})), tap((data) => this.groups.set(data.groups.map((group) => ({ group, member: data.members.some(x => x.id === group.id) @@ -109,7 +109,7 @@ export class UserDetailService { const user = this.user(); if (user) { this.deleteLoading.set(true); - this.userService.userControllerDeleteUser(user.id) + this.userService.userControllerDeleteUser({id: user.id}) .subscribe({ next: () => { this.deleteLoading.set(false); diff --git a/frontend/src/app/pages/administration/users/users.service.ts b/frontend/src/app/pages/administration/users/users.service.ts index 818be30..dd97ea4 100644 --- a/frontend/src/app/pages/administration/users/users.service.ts +++ b/frontend/src/app/pages/administration/users/users.service.ts @@ -20,7 +20,10 @@ export class UsersService { this.usersLoading.set(true); this.userService.userControllerGetCount() .subscribe((count) => this.total.set(count.count)); - this.userService.userControllerGetUsers(this.itemsPerPage(), this.page() * this.itemsPerPage()) + this.userService.userControllerGetUsers({ + limit: this.itemsPerPage(), + offset: this.page() * this.itemsPerPage(), + }) .subscribe({ next: (users) => { this.users.set(users); diff --git a/frontend/src/app/pages/base/locations/location-create/location-create.component.html b/frontend/src/app/pages/base/locations/location-create/location-create.component.html index 2658401..3f1ba83 100644 --- a/frontend/src/app/pages/base/locations/location-create/location-create.component.html +++ b/frontend/src/app/pages/base/locations/location-create/location-create.component.html @@ -49,21 +49,22 @@

Ort/Fahrzeug erstellen

Übergeordneter Ort/Fahrzeug + - @for (item of parents(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (parentsIsLoading()) { + } @else { + @for (item of parents(); track item) { + + } } - + diff --git a/frontend/src/app/pages/base/locations/location-create/location-create.component.ts b/frontend/src/app/pages/base/locations/location-create/location-create.component.ts index bc070fe..9faae41 100644 --- a/frontend/src/app/pages/base/locations/location-create/location-create.component.ts +++ b/frontend/src/app/pages/base/locations/location-create/location-create.component.ts @@ -1,4 +1,4 @@ -import {Component, OnDestroy, OnInit, Signal} from '@angular/core'; +import {Component, OnDestroy, Signal} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {NzButtonModule} from 'ng-zorro-antd/button'; import {NzFormModule} from 'ng-zorro-antd/form'; @@ -26,7 +26,7 @@ interface LocationCreateForm { templateUrl: './location-create.component.html', styleUrl: './location-create.component.less' }) -export class LocationCreateComponent implements OnDestroy, OnInit { +export class LocationCreateComponent implements OnDestroy { types = LocationTypes.all; parentsIsLoading: Signal; @@ -52,35 +52,30 @@ export class LocationCreateComponent implements OnDestroy, OnInit { private destroy$ = new Subject(); constructor( - private readonly locationCreateService: LocationCreateService, + private readonly service: LocationCreateService, private readonly inAppMessagingService: InAppMessageService ) { - this.createLoading = this.locationCreateService.createLoading; - this.parentsIsLoading = this.locationCreateService.parentsIsLoading; - this.parents = this.locationCreateService.parents; + this.createLoading = this.service.createLoading; + this.parentsIsLoading = this.service.parentsIsLoading; + this.parents = this.service.parents; - this.locationCreateService.createLoadingError + this.service.createLoadingError .pipe(takeUntil(this.destroy$)) .subscribe((x) => this.inAppMessagingService.showError(x)); - this.locationCreateService.createLoadingSuccess + this.service.createLoadingSuccess .pipe(takeUntil(this.destroy$)) .subscribe(() => this.inAppMessagingService.showSuccess('Änderungen gespeichert')); } - - ngOnInit(): void { - this.locationCreateService.loadParents(true); - } - submit() { - this.locationCreateService.create(this.form.getRawValue()); - } - - loadMore() { - this.locationCreateService.loadParents(); + this.service.create(this.form.getRawValue()); } ngOnDestroy(): void { this.destroy$.next(); } + + onSearchParent(search: string) { + this.service.onSearchParent(search); + } } diff --git a/frontend/src/app/pages/base/locations/location-create/location-create.service.ts b/frontend/src/app/pages/base/locations/location-create/location-create.service.ts index 51ff2e8..65e342d 100644 --- a/frontend/src/app/pages/base/locations/location-create/location-create.service.ts +++ b/frontend/src/app/pages/base/locations/location-create/location-create.service.ts @@ -1,10 +1,11 @@ -import {Injectable, signal} from '@angular/core'; -import {Subject} from 'rxjs'; +import {Inject, Injectable, signal} from '@angular/core'; +import {BehaviorSubject, debounceTime, filter, Subject} from 'rxjs'; import {Router} from '@angular/router'; import {HttpErrorResponse} from '@angular/common/http'; import {LocationService} from '@backend/api/location.service'; import {LocationCreateDto} from '@backend/model/locationCreateDto'; import {LocationDto} from '@backend/model/locationDto'; +import {SEARCH_DEBOUNCE_TIME, SELECT_ITEMS_COUNT} from '../../../../app.configs'; @Injectable({ providedIn: 'root' @@ -16,15 +17,27 @@ export class LocationCreateService { parentsIsLoading = signal(false); parents = signal([]); - private parentsPage = 0; - private parentsItemsPerPage = 10; + parentsSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + parentsSearch = ''; - constructor(private readonly locationService: LocationService, private readonly router: Router) { + constructor( + private readonly locationService: LocationService, + private readonly router: Router, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + @Inject(SELECT_ITEMS_COUNT) private readonly selectCount: number, + ) { + this.parentsSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.parentsSearch = x.value; + this.loadParents(); + }); } create(rawValue: LocationCreateDto) { this.createLoading.set(true); - this.locationService.locationControllerCreate(rawValue) + this.locationService.locationControllerCreate({locationCreateDto: rawValue}) .subscribe({ next: (entity) => { this.createLoading.set(false); @@ -38,29 +51,26 @@ export class LocationCreateService { }); } - loadParents(init = false) { - if (init) { - this.parentsPage = 0; - this.parents.set([]); - } + loadParents() { this.parentsIsLoading.set(true); this.locationService - .locationControllerGetAll(this.parentsItemsPerPage, this.parentsPage * this.parentsItemsPerPage) + .locationControllerGetAll({ + searchTerm: this.parentsSearch, + limit: this.selectCount, + }) .subscribe({ next: (parents) => { this.parentsIsLoading.set(false); - const newParents = [ - ...this.parents(), - ...parents, - ]; - this.parents.set(newParents); - this.parentsPage += 1; + this.parents.set(parents); }, error: () => { this.parentsIsLoading.set(false); this.parents.set([]); - this.parentsPage = 0; } }); } + + onSearchParent(value: string): void { + this.parentsSearch$.next({propagate: true, value}); + } } diff --git a/frontend/src/app/pages/base/locations/location-detail/location-detail.component.html b/frontend/src/app/pages/base/locations/location-detail/location-detail.component.html index 1cb0c60..ef62839 100644 --- a/frontend/src/app/pages/base/locations/location-detail/location-detail.component.html +++ b/frontend/src/app/pages/base/locations/location-detail/location-detail.component.html @@ -61,21 +61,20 @@

Der Ort/Fahrzeug wurde nicht gefunden!

Übergeordneter Ort/Fahrzeug - @for (item of parents(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (parentsIsLoading()) { + } @else { + @for (item of parents(); track item) { + + } } - + diff --git a/frontend/src/app/pages/base/locations/location-detail/location-detail.component.ts b/frontend/src/app/pages/base/locations/location-detail/location-detail.component.ts index 1419c45..911a821 100644 --- a/frontend/src/app/pages/base/locations/location-detail/location-detail.component.ts +++ b/frontend/src/app/pages/base/locations/location-detail/location-detail.component.ts @@ -60,39 +60,39 @@ export class LocationDetailComponent implements OnInit, OnDestroy { constructor( private readonly activatedRoute: ActivatedRoute, - private readonly locationDetailService: LocationDetailService, + private readonly service: LocationDetailService, private readonly inAppMessagingService: InAppMessageService, ) { - this.notFound = this.locationDetailService.notFound; - this.loading = this.locationDetailService.loading; - this.loadingError = this.locationDetailService.loadingError; - this.deleteLoading = this.locationDetailService.deleteLoading; - this.updateLoading = this.locationDetailService.updateLoading; - this.parentsIsLoading = this.locationDetailService.parentsIsLoading; - this.parents = this.locationDetailService.parents; + this.notFound = this.service.notFound; + this.loading = this.service.loading; + this.loadingError = this.service.loadingError; + this.deleteLoading = this.service.deleteLoading; + this.updateLoading = this.service.updateLoading; + this.parentsIsLoading = this.service.parentsIsLoading; + this.parents = this.service.parents; effect(() => { - const location = this.locationDetailService.location(); + const location = this.service.location(); if (location) this.form.patchValue(location); }); effect(() => { - const updateLoading = this.locationDetailService.loadingError(); + const updateLoading = this.service.loadingError(); if (updateLoading) { this.inAppMessagingService.showError('Fehler beim laden des Orts/Fahrzeugs.'); } }); - this.locationDetailService.deleteLoadingError + this.service.deleteLoadingError .pipe(takeUntil(this.destroy$)) .subscribe((x) => this.inAppMessagingService.showError(x)); - this.locationDetailService.deleteLoadingSuccess + this.service.deleteLoadingSuccess .pipe(takeUntil(this.destroy$)) .subscribe(() => this.inAppMessagingService.showSuccess('Ort/Fahrzeug gelöscht')); - this.locationDetailService.updateLoadingError + this.service.updateLoadingError .pipe(takeUntil(this.destroy$)) .subscribe(() => this.inAppMessagingService.showError('Fehler beim speichern.')); - this.locationDetailService.updateLoadingSuccess + this.service.updateLoadingSuccess .pipe(takeUntil(this.destroy$)) .subscribe(() => this.inAppMessagingService.showSuccess('Änderungen gespeichert')); } @@ -104,19 +104,19 @@ export class LocationDetailComponent implements OnInit, OnDestroy { ngOnInit(): void { this.activatedRoute.params.subscribe(params => { - this.locationDetailService.load(params['id']); + this.service.load(params['id']); }); } submit() { - this.locationDetailService.update(this.form.getRawValue()); + this.service.update(this.form.getRawValue()); } delete() { - this.locationDetailService.delete(); + this.service.delete(); } - loadMore() { - this.locationDetailService.loadParents(); + onSearchParent(search: string) { + this.service.onSearchParent(search); } } diff --git a/frontend/src/app/pages/base/locations/location-detail/location-detail.service.ts b/frontend/src/app/pages/base/locations/location-detail/location-detail.service.ts index d11f31e..5411409 100644 --- a/frontend/src/app/pages/base/locations/location-detail/location-detail.service.ts +++ b/frontend/src/app/pages/base/locations/location-detail/location-detail.service.ts @@ -1,10 +1,11 @@ -import {Injectable, signal} from '@angular/core'; -import {Subject} from 'rxjs'; +import {Inject, Injectable, signal} from '@angular/core'; +import {BehaviorSubject, debounceTime, filter, Subject} from 'rxjs'; import {Router} from '@angular/router'; import {HttpErrorResponse} from '@angular/common/http'; import {LocationDto} from '@backend/model/locationDto'; import {LocationService} from '@backend/api/location.service'; import {LocationUpdateDto} from '@backend/model/locationUpdateDto'; +import {SEARCH_DEBOUNCE_TIME, SELECT_ITEMS_COUNT} from '../../../../app.configs'; @Injectable({ providedIn: 'root' @@ -24,21 +25,28 @@ export class LocationDetailService { updateLoadingSuccess = new Subject(); deleteLoadingSuccess = new Subject(); - private parentsPage = 0; - private parentsItemsPerPage = 10; + parentsSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + parentsSearch = ''; constructor( private readonly locationService: LocationService, private readonly router: Router, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + @Inject(SELECT_ITEMS_COUNT) private readonly selectCount: number, ) { + this.parentsSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.parentsSearch = x.value; + this.loadParents(); + }); } load(id: number) { this.id = id; - this.parentsPage = 0; - this.parents.set([]); this.loading.set(true); - this.locationService.locationControllerGetOne(id) + this.locationService.locationControllerGetOne({id}) .subscribe({ next: (newEntity) => { this.location.set(newEntity); @@ -47,7 +55,6 @@ export class LocationDetailService { if (newEntity.parent) { this.parents.set([newEntity.parent]); } - this.loadParents(); }, error: (err: HttpErrorResponse) => { if (err.status === 404) { @@ -57,7 +64,6 @@ export class LocationDetailService { this.loadingError.set(true); this.loading.set(false); this.parents.set([]); - this.parentsPage = 0; } }) } @@ -66,7 +72,7 @@ export class LocationDetailService { const entity = this.location(); if (entity) { this.updateLoading.set(true); - this.locationService.locationControllerUpdate(entity.id, rawValue) + this.locationService.locationControllerUpdate({id: entity.id, locationUpdateDto: rawValue}) .subscribe({ next: (newEntity) => { this.updateLoading.set(false); @@ -85,7 +91,7 @@ export class LocationDetailService { const entity = this.location(); if (entity) { this.deleteLoading.set(true); - this.locationService.locationControllerDelete(entity.id) + this.locationService.locationControllerDelete({id: entity.id}) .subscribe({ next: () => { this.deleteLoading.set(false); @@ -103,22 +109,23 @@ export class LocationDetailService { loadParents() { this.parentsIsLoading.set(true); this.locationService - .locationControllerGetAll(this.parentsItemsPerPage, this.parentsPage * this.parentsItemsPerPage) + .locationControllerGetAll({ + searchTerm: this.parentsSearch, + limit: this.selectCount, + }) .subscribe({ next: (parents) => { this.parentsIsLoading.set(false); - const newParents = [ - ...this.parents(), - ...parents.filter(x => x.id != this.location()?.parentId && x.id != this.id), - ]; - this.parents.set(newParents); - this.parentsPage += 1; + this.parents.set(parents.filter(x => x.id !== this.location()?.id)); }, error: () => { this.parentsIsLoading.set(false); this.parents.set([]); - this.parentsPage = 0; } }); } + + onSearchParent(value: string): void { + this.parentsSearch$.next({propagate: true, value}); + } } diff --git a/frontend/src/app/pages/base/locations/locations.component.html b/frontend/src/app/pages/base/locations/locations.component.html index c9c2a03..6bea5a5 100644 --- a/frontend/src/app/pages/base/locations/locations.component.html +++ b/frontend/src/app/pages/base/locations/locations.component.html @@ -1,3 +1,15 @@

Orte/Fahrzeuge

- +
+
+ +
+
+ + + + + + +
+
diff --git a/frontend/src/app/pages/base/locations/locations.component.ts b/frontend/src/app/pages/base/locations/locations.component.ts index 65b0f01..fee3830 100644 --- a/frontend/src/app/pages/base/locations/locations.component.ts +++ b/frontend/src/app/pages/base/locations/locations.component.ts @@ -1,8 +1,12 @@ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {HasRoleDirective} from '../../../core/auth/has-role.directive'; import {NzButtonModule} from 'ng-zorro-antd/button'; import {RouterLink} from '@angular/router'; import {LocationListComponent} from './location-list/location-list.component'; +import {NzGridModule} from 'ng-zorro-antd/grid'; +import {NzInputModule} from 'ng-zorro-antd/input'; +import {NzIconModule} from 'ng-zorro-antd/icon'; +import {LocationsService} from './locations.service'; @Component({ selector: 'ofs-locations', @@ -11,10 +15,24 @@ import {LocationListComponent} from './location-list/location-list.component'; RouterLink, LocationListComponent, NzButtonModule, + NzGridModule, + NzInputModule, + NzIconModule, ], templateUrl: './locations.component.html', styleUrl: './locations.component.less' }) -export class LocationsComponent { +export class LocationsComponent implements OnInit { + constructor(private readonly service: LocationsService) { + } + + search($event: Event) { + const target = $event.target as HTMLInputElement; + this.service.search(target.value); + } + + ngOnInit(): void { + this.service.init(); + } } diff --git a/frontend/src/app/pages/base/locations/locations.service.ts b/frontend/src/app/pages/base/locations/locations.service.ts index a289a85..2daf251 100644 --- a/frontend/src/app/pages/base/locations/locations.service.ts +++ b/frontend/src/app/pages/base/locations/locations.service.ts @@ -1,6 +1,8 @@ -import {Injectable, signal} from '@angular/core'; +import {Inject, Injectable, signal} from '@angular/core'; import {LocationDto} from '@backend/model/locationDto'; import {LocationService} from '@backend/api/location.service'; +import {BehaviorSubject, debounceTime, distinctUntilChanged, filter} from 'rxjs'; +import {SEARCH_DEBOUNCE_TIME} from '../../../app.configs'; @Injectable({ providedIn: 'root' @@ -14,15 +16,37 @@ export class LocationsService { locationsLoadError = signal(false); sortCol?: string; sortDir?: string; + searchTerm$ = new BehaviorSubject<{ propagate: boolean, value: string }>({propagate: true, value: ''}); + private searchTerm?: string; - constructor(private readonly locationService: LocationService) { + + constructor( + private readonly locationService: LocationService, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + ) { + this.searchTerm$ + .pipe( + filter(x => x.propagate), + debounceTime(time), + distinctUntilChanged(), + ) + .subscribe(term => { + this.searchTerm = term.value; + this.load(); + }); } load() { this.locationsLoading.set(true); - this.locationService.locationControllerGetCount() + this.locationService.locationControllerGetCount({searchTerm: this.searchTerm}) .subscribe((count) => this.total.set(count.count)); - this.locationService.locationControllerGetAll(this.itemsPerPage, (this.page - 1) * this.itemsPerPage, this.sortCol, this.sortDir) + this.locationService.locationControllerGetAll({ + limit: this.itemsPerPage, + offset: (this.page - 1) * this.itemsPerPage, + sortCol: this.sortCol, + sortDir: this.sortDir, + searchTerm: this.searchTerm + }) .subscribe({ next: (users) => { this.locations.set(users); @@ -44,4 +68,14 @@ export class LocationsService { this.sortDir = this.sortCol ? sortDir : undefined; this.load(); } + + search(term: string) { + this.searchTerm$.next({propagate: true, value: term}); + this.page = 1; + } + + init() { + this.searchTerm = ''; + this.searchTerm$.next({propagate: false, value: ''}); + } } diff --git a/frontend/src/app/pages/inventory/device-groups/device-group-create/device-group-create.service.ts b/frontend/src/app/pages/inventory/device-groups/device-group-create/device-group-create.service.ts index 41cdbc9..5f3df7d 100644 --- a/frontend/src/app/pages/inventory/device-groups/device-group-create/device-group-create.service.ts +++ b/frontend/src/app/pages/inventory/device-groups/device-group-create/device-group-create.service.ts @@ -9,7 +9,7 @@ import {HttpErrorResponse} from '@angular/common/http'; providedIn: 'root' }) export class DeviceGroupCreateService { -createLoading = signal(false); + createLoading = signal(false); createLoadingError = new Subject(); createLoadingSuccess = new Subject(); @@ -21,7 +21,7 @@ createLoading = signal(false); create(rawValue: DeviceGroupCreateDto) { this.createLoading.set(true); - this.apiService.deviceGroupControllerCreate(rawValue) + this.apiService.deviceGroupControllerCreate({deviceGroupCreateDto: rawValue}) .subscribe({ next: (entity) => { this.createLoading.set(false); diff --git a/frontend/src/app/pages/inventory/device-groups/device-group-detail/device-group-detail.service.ts b/frontend/src/app/pages/inventory/device-groups/device-group-detail/device-group-detail.service.ts index 0e2bd67..912b548 100644 --- a/frontend/src/app/pages/inventory/device-groups/device-group-detail/device-group-detail.service.ts +++ b/frontend/src/app/pages/inventory/device-groups/device-group-detail/device-group-detail.service.ts @@ -31,7 +31,7 @@ export class DeviceGroupDetailService { load(id: number) { this.id = id; this.loading.set(true); - this.locationService.deviceGroupControllerGetOne(id) + this.locationService.deviceGroupControllerGetOne({id}) .subscribe({ next: (newEntity) => { this.entity.set(newEntity); @@ -53,7 +53,7 @@ export class DeviceGroupDetailService { const entity = this.entity(); if (entity) { this.updateLoading.set(true); - this.locationService.deviceGroupControllerUpdate(entity.id, rawValue) + this.locationService.deviceGroupControllerUpdate({id: entity.id, deviceGroupUpdateDto: rawValue}) .subscribe({ next: (newEntity) => { this.updateLoading.set(false); @@ -72,7 +72,7 @@ export class DeviceGroupDetailService { const entity = this.entity(); if (entity) { this.deleteLoading.set(true); - this.locationService.deviceGroupControllerDelete(entity.id) + this.locationService.deviceGroupControllerDelete({id: entity.id}) .subscribe({ next: () => { this.deleteLoading.set(false); diff --git a/frontend/src/app/pages/inventory/device-groups/device-groups.component.html b/frontend/src/app/pages/inventory/device-groups/device-groups.component.html index b76a791..7dae087 100644 --- a/frontend/src/app/pages/inventory/device-groups/device-groups.component.html +++ b/frontend/src/app/pages/inventory/device-groups/device-groups.component.html @@ -1,3 +1,17 @@

Geräte-Gruppen

- +
+
+ +
+
+ + + + + + +
+
diff --git a/frontend/src/app/pages/inventory/device-groups/device-groups.component.ts b/frontend/src/app/pages/inventory/device-groups/device-groups.component.ts index 22fd44c..0335f9d 100644 --- a/frontend/src/app/pages/inventory/device-groups/device-groups.component.ts +++ b/frontend/src/app/pages/inventory/device-groups/device-groups.component.ts @@ -1,23 +1,40 @@ -import { Component } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {HasRoleDirective} from '../../../core/auth/has-role.directive'; -import {NzButtonComponent} from 'ng-zorro-antd/button'; +import {NzButtonModule} from 'ng-zorro-antd/button'; import {NzWaveDirective} from 'ng-zorro-antd/core/wave'; import {RouterLink} from '@angular/router'; import {DeviceGroupListComponent} from './device-group-list/device-group-list.component'; +import {NzGridModule} from 'ng-zorro-antd/grid'; +import {NzInputModule} from 'ng-zorro-antd/input'; +import {NzIconModule} from 'ng-zorro-antd/icon'; +import {DeviceGroupsService} from './device-groups.service'; @Component({ selector: 'ofs-device-groups', imports: [ HasRoleDirective, - NzButtonComponent, NzWaveDirective, RouterLink, - DeviceGroupListComponent + DeviceGroupListComponent, + NzButtonModule, + NzGridModule, + NzInputModule, + NzIconModule, ], standalone: true, templateUrl: './device-groups.component.html', styleUrl: './device-groups.component.less' }) -export class DeviceGroupsComponent { +export class DeviceGroupsComponent implements OnInit { + constructor(private service: DeviceGroupsService) { + } + search($event: Event) { + const target = $event.target as HTMLInputElement; + this.service.search(target.value); + } + + ngOnInit(): void { + this.service.init(); + } } diff --git a/frontend/src/app/pages/inventory/device-groups/device-groups.service.ts b/frontend/src/app/pages/inventory/device-groups/device-groups.service.ts index 71b5b52..ad13c3b 100644 --- a/frontend/src/app/pages/inventory/device-groups/device-groups.service.ts +++ b/frontend/src/app/pages/inventory/device-groups/device-groups.service.ts @@ -1,6 +1,8 @@ -import {Injectable, signal} from '@angular/core'; +import {Inject, Injectable, signal} from '@angular/core'; import {DeviceGroupDto} from '@backend/model/deviceGroupDto'; import {DeviceGroupService} from '@backend/api/deviceGroup.service'; +import {BehaviorSubject, debounceTime, distinctUntilChanged, filter} from 'rxjs'; +import {SEARCH_DEBOUNCE_TIME} from '../../../app.configs'; @Injectable({ providedIn: 'root' @@ -14,15 +16,35 @@ export class DeviceGroupsService { entitiesLoadError = signal(false); sortCol?: string; sortDir?: string; + searchTerm$ = new BehaviorSubject<{ propagate: boolean, value: string }>({propagate: true, value: ''}); + private searchTerm?: string; - constructor(private readonly apiService: DeviceGroupService) { + constructor(private readonly apiService: DeviceGroupService, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + ) { + this.searchTerm$ + .pipe( + filter(x => x.propagate), + debounceTime(time), + distinctUntilChanged(), + ) + .subscribe(term => { + this.searchTerm = term.value; + this.load(); + }); } load() { this.entitiesLoading.set(true); - this.apiService.deviceGroupControllerGetCount() + this.apiService.deviceGroupControllerGetCount({searchTerm: this.searchTerm}) .subscribe((count) => this.total.set(count.count)); - this.apiService.deviceGroupControllerGetAll(this.itemsPerPage, (this.page - 1) * this.itemsPerPage, this.sortCol, this.sortDir) + this.apiService.deviceGroupControllerGetAll({ + limit: this.itemsPerPage, + offset: (this.page - 1) * this.itemsPerPage, + sortCol: this.sortCol, + sortDir: this.sortDir, + searchTerm: this.searchTerm + }) .subscribe({ next: (users) => { this.entities.set(users); @@ -44,4 +66,14 @@ export class DeviceGroupsService { this.sortDir = this.sortCol ? sortDir : undefined; this.load(); } + + search(term: string) { + this.searchTerm$.next({propagate: true, value: term}); + this.page = 1; + } + + init() { + this.searchTerm = ''; + this.searchTerm$.next({propagate: false, value: ''}); + } } diff --git a/frontend/src/app/pages/inventory/device-types/device-type-create/device-type-create.service.ts b/frontend/src/app/pages/inventory/device-types/device-type-create/device-type-create.service.ts index 6bef996..eb2043a 100644 --- a/frontend/src/app/pages/inventory/device-types/device-type-create/device-type-create.service.ts +++ b/frontend/src/app/pages/inventory/device-types/device-type-create/device-type-create.service.ts @@ -21,7 +21,7 @@ export class DeviceTypeCreateService { create(rawValue: DeviceTypeCreateDto) { this.createLoading.set(true); - this.apiService.deviceTypeControllerCreate(rawValue) + this.apiService.deviceTypeControllerCreate({deviceTypeCreateDto: rawValue}) .subscribe({ next: (entity) => { this.createLoading.set(false); diff --git a/frontend/src/app/pages/inventory/device-types/device-type-detail/device-type-detail.service.ts b/frontend/src/app/pages/inventory/device-types/device-type-detail/device-type-detail.service.ts index 40b4805..55ceb0d 100644 --- a/frontend/src/app/pages/inventory/device-types/device-type-detail/device-type-detail.service.ts +++ b/frontend/src/app/pages/inventory/device-types/device-type-detail/device-type-detail.service.ts @@ -31,7 +31,7 @@ export class DeviceTypeDetailService { load(id: number) { this.id = id; this.loading.set(true); - this.locationService.deviceTypeControllerGetOne(id) + this.locationService.deviceTypeControllerGetOne({id}) .subscribe({ next: (newEntity) => { this.entity.set(newEntity); @@ -53,7 +53,7 @@ export class DeviceTypeDetailService { const entity = this.entity(); if (entity) { this.updateLoading.set(true); - this.locationService.deviceTypeControllerUpdate(entity.id, rawValue) + this.locationService.deviceTypeControllerUpdate({id: entity.id, deviceTypeUpdateDto: rawValue}) .subscribe({ next: (newEntity) => { this.updateLoading.set(false); @@ -72,7 +72,7 @@ export class DeviceTypeDetailService { const entity = this.entity(); if (entity) { this.deleteLoading.set(true); - this.locationService.deviceTypeControllerDelete(entity.id) + this.locationService.deviceTypeControllerDelete({id: entity.id}) .subscribe({ next: () => { this.deleteLoading.set(false); diff --git a/frontend/src/app/pages/inventory/device-types/device-types.component.html b/frontend/src/app/pages/inventory/device-types/device-types.component.html index 657ded3..e926a01 100644 --- a/frontend/src/app/pages/inventory/device-types/device-types.component.html +++ b/frontend/src/app/pages/inventory/device-types/device-types.component.html @@ -1,3 +1,15 @@

Geräte-Typen

- +
+
+ +
+
+ + + + + + +
+
diff --git a/frontend/src/app/pages/inventory/device-types/device-types.component.ts b/frontend/src/app/pages/inventory/device-types/device-types.component.ts index 5fea3d6..9f5be48 100644 --- a/frontend/src/app/pages/inventory/device-types/device-types.component.ts +++ b/frontend/src/app/pages/inventory/device-types/device-types.component.ts @@ -1,23 +1,40 @@ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {HasRoleDirective} from '../../../core/auth/has-role.directive'; -import {NzButtonComponent} from 'ng-zorro-antd/button'; +import {NzButtonModule} from 'ng-zorro-antd/button'; import {NzWaveDirective} from 'ng-zorro-antd/core/wave'; import {RouterLink} from '@angular/router'; import {DeviceTypeListComponent} from './device-type-list/device-type-list.component'; +import {NzGridModule} from 'ng-zorro-antd/grid'; +import {NzInputModule} from 'ng-zorro-antd/input'; +import {NzIconModule} from 'ng-zorro-antd/icon'; +import {DeviceTypesService} from './device-types.service'; @Component({ selector: 'ofs-device-types', imports: [ HasRoleDirective, - NzButtonComponent, NzWaveDirective, RouterLink, - DeviceTypeListComponent + DeviceTypeListComponent, + NzButtonModule, + NzGridModule, + NzInputModule, + NzIconModule, ], standalone: true, templateUrl: './device-types.component.html', styleUrl: './device-types.component.less' }) -export class DeviceTypesComponent { +export class DeviceTypesComponent implements OnInit { + constructor(private service: DeviceTypesService) { + } + search($event: Event) { + const target = $event.target as HTMLInputElement; + this.service.search(target.value); + } + + ngOnInit(): void { + this.service.init(); + } } diff --git a/frontend/src/app/pages/inventory/device-types/device-types.service.ts b/frontend/src/app/pages/inventory/device-types/device-types.service.ts index 4bf60a1..82706f0 100644 --- a/frontend/src/app/pages/inventory/device-types/device-types.service.ts +++ b/frontend/src/app/pages/inventory/device-types/device-types.service.ts @@ -1,6 +1,8 @@ -import {Injectable, signal} from '@angular/core'; +import {Inject, Injectable, signal} from '@angular/core'; import {DeviceTypeDto} from '@backend/model/deviceTypeDto'; import {DeviceTypeService} from '@backend/api/deviceType.service'; +import {BehaviorSubject, debounceTime, distinctUntilChanged, filter} from 'rxjs'; +import {SEARCH_DEBOUNCE_TIME} from '../../../app.configs'; @Injectable({ providedIn: 'root' @@ -14,15 +16,35 @@ export class DeviceTypesService { entitiesLoadError = signal(false); sortCol?: string; sortDir?: string; + searchTerm$ = new BehaviorSubject<{ propagate: boolean, value: string }>({propagate: true, value: ''}); + private searchTerm?: string; - constructor(private readonly apiService: DeviceTypeService) { + constructor(private readonly apiService: DeviceTypeService, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + ) { + this.searchTerm$ + .pipe( + filter(x => x.propagate), + debounceTime(time), + distinctUntilChanged(), + ) + .subscribe(term => { + this.searchTerm = term.value; + this.load(); + }); } load() { this.entitiesLoading.set(true); - this.apiService.deviceTypeControllerGetCount() + this.apiService.deviceTypeControllerGetCount({searchTerm: this.searchTerm}) .subscribe((count) => this.total.set(count.count)); - this.apiService.deviceTypeControllerGetAll(this.itemsPerPage, (this.page - 1) * this.itemsPerPage, this.sortCol, this.sortDir) + this.apiService.deviceTypeControllerGetAll({ + limit: this.itemsPerPage, + offset: (this.page - 1) * this.itemsPerPage, + sortCol: this.sortCol, + sortDir: this.sortDir, + searchTerm: this.searchTerm + }) .subscribe({ next: (users) => { this.entities.set(users); @@ -44,4 +66,14 @@ export class DeviceTypesService { this.sortDir = this.sortCol ? sortDir : undefined; this.load(); } + + search(term: string) { + this.searchTerm$.next({propagate: true, value: term}); + this.page = 1; + } + + init() { + this.searchTerm = ''; + this.searchTerm$.next({propagate: false, value: ''}); + } } diff --git a/frontend/src/app/pages/inventory/devices/device-create/device-create.component.html b/frontend/src/app/pages/inventory/devices/device-create/device-create.component.html index b7ee5c4..ecdb9fd 100644 --- a/frontend/src/app/pages/inventory/devices/device-create/device-create.component.html +++ b/frontend/src/app/pages/inventory/devices/device-create/device-create.component.html @@ -41,20 +41,20 @@

Geräte erstellen

Standort/Fahrzeug - @for (item of locations(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (locationsIsLoading()) { + } @else { + @for (item of locations(); track item) { + + } } - + @@ -62,20 +62,20 @@

Geräte erstellen

Geräte-Typ - @for (item of deviceTypes(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (deviceTypesIsLoading()) { + } @else { + @for (item of deviceTypes(); track item) { + + } } - + @@ -83,20 +83,20 @@

Geräte erstellen

Geräte-Gruppe - @for (item of deviceGroups(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (deviceGroupsIsLoading()) { + } @else { + @for (item of deviceGroups(); track item) { + + } } - + diff --git a/frontend/src/app/pages/inventory/devices/device-create/device-create.component.ts b/frontend/src/app/pages/inventory/devices/device-create/device-create.component.ts index e979b74..dde98f1 100644 --- a/frontend/src/app/pages/inventory/devices/device-create/device-create.component.ts +++ b/frontend/src/app/pages/inventory/devices/device-create/device-create.component.ts @@ -1,4 +1,4 @@ -import {Component, OnDestroy, OnInit, Signal} from '@angular/core'; +import {Component, OnDestroy, Signal} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {NzButtonModule} from 'ng-zorro-antd/button'; import {NzFormModule} from 'ng-zorro-antd/form'; @@ -35,7 +35,6 @@ interface DeviceCreateForm { locationId: FormControl; } -// TODO Geräte-Typ durch eintippen von filtern @Component({ selector: 'ofs-device-create', imports: [ReactiveFormsModule, NzButtonModule, NzFormModule, NzInputModule, NzCheckboxModule, NzInputNumberModule, NzDatePickerModule, NzOptionComponent, NzSelectComponent, NzSpinComponent], @@ -43,7 +42,7 @@ interface DeviceCreateForm { templateUrl: './device-create.component.html', styleUrl: './device-create.component.less' }) -export class DeviceCreateComponent implements OnInit, OnDestroy { +export class DeviceCreateComponent implements OnDestroy { states = DeviceTypes.all; form = new FormGroup({ @@ -133,25 +132,20 @@ export class DeviceCreateComponent implements OnInit, OnDestroy { this.service.create(this.form.getRawValue() as any); // TODO } - loadMoreLocations() { - this.service.loadMoreLocations(); - } - - loadMoreTypes() { - this.service.loadMoreTypes(); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } - loadMoreGroups() { - this.service.loadMoreGroups(); + onSearchLocation(search: string) { + this.service.onSearchLocation(search); } - ngOnInit(): void { - this.service.loadMoreTypes(true); - this.service.loadMoreGroups(true); - this.service.loadMoreLocations(true); + onSearchType(search: string) { + this.service.onSearchType(search); } - ngOnDestroy(): void { - this.destroy$.next(); + onSearchGroup(search: string) { + this.service.onSearchGroup(search); } } diff --git a/frontend/src/app/pages/inventory/devices/device-create/device-create.service.ts b/frontend/src/app/pages/inventory/devices/device-create/device-create.service.ts index 82ce408..fc5a029 100644 --- a/frontend/src/app/pages/inventory/devices/device-create/device-create.service.ts +++ b/frontend/src/app/pages/inventory/devices/device-create/device-create.service.ts @@ -1,5 +1,5 @@ -import {Injectable, signal} from '@angular/core'; -import {Subject} from 'rxjs'; +import {Inject, Injectable, signal} from '@angular/core'; +import {BehaviorSubject, debounceTime, filter, Subject} from 'rxjs'; import {Router} from '@angular/router'; import {DeviceService} from '@backend/api/device.service'; import {HttpErrorResponse} from '@angular/common/http'; @@ -10,6 +10,7 @@ import {DeviceGroupDto} from '@backend/model/deviceGroupDto'; import {DeviceGroupService} from '@backend/api/deviceGroup.service'; import {LocationDto} from '@backend/model/locationDto'; import {LocationService} from '@backend/api/location.service'; +import {SEARCH_DEBOUNCE_TIME, SELECT_ITEMS_COUNT} from '../../../../app.configs'; @Injectable({ providedIn: 'root' @@ -19,20 +20,20 @@ export class DeviceCreateService { createLoadingError = new Subject(); createLoadingSuccess = new Subject(); + deviceTypesSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + deviceTypesSearch = ''; deviceTypes = signal([]); deviceTypesIsLoading = signal(false); - private deviceTypesPage = 0; - private deviceTypesItemsPerPage = 10; + deviceGroupsSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + deviceGroupsSearch = ''; deviceGroups = signal([]); deviceGroupsIsLoading = signal(false); - private deviceGroupsPage = 0; - private deviceGroupsItemsPerPage = 10; + locationSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + locationSearch = ''; locations = signal([]); locationsIsLoading = signal(false); - private locationsPage = 0; - private locationsItemsPerPage = 10; constructor( private readonly apiService: DeviceService, @@ -40,12 +41,35 @@ export class DeviceCreateService { private readonly apiDeviceGroupsService: DeviceGroupService, private readonly apiLocationsService: LocationService, private readonly router: Router, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + @Inject(SELECT_ITEMS_COUNT) private readonly selectCount: number, ) { + this.locationSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.locationSearch = x.value; + this.loadMoreLocations(); + }); + this.deviceTypesSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.deviceTypesSearch = x.value; + this.loadMoreTypes(); + }); + this.deviceGroupsSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.deviceGroupsSearch = x.value; + this.loadMoreGroups(); + }); } create(rawValue: DeviceCreateDto) { this.createLoading.set(true); - this.apiService.deviceControllerCreate(rawValue) + this.apiService.deviceControllerCreate({deviceCreateDto: rawValue}) .subscribe({ next: (entity) => { this.createLoading.set(false); @@ -59,81 +83,73 @@ export class DeviceCreateService { }); } - loadMoreTypes(init = false) { - if (init) { - this.deviceTypesPage = 0; - this.deviceTypes.set([]); - } + loadMoreTypes() { + this.deviceTypesIsLoading.set(true); this.apiDeviceTypesService - .deviceTypeControllerGetAll(this.deviceTypesItemsPerPage, this.deviceTypesPage * this.deviceTypesItemsPerPage) + .deviceTypeControllerGetAll({ + limit: this.selectCount, + searchTerm: this.deviceTypesSearch + }) .subscribe({ next: (deviceTypes) => { this.deviceTypesIsLoading.set(false); - const newDeviceTypes = [ - ...this.deviceTypes(), - ...deviceTypes, - ]; - this.deviceTypes.set(newDeviceTypes); - this.deviceTypesPage += 1; + this.deviceTypes.set(deviceTypes); }, error: () => { this.deviceTypesIsLoading.set(false); this.deviceTypes.set([]); - this.deviceTypesPage = 0; } }); } - loadMoreGroups(init = false) { - if (init) { - this.deviceGroupsPage = 0; - this.deviceGroups.set([]); - } + loadMoreGroups() { this.deviceGroupsIsLoading.set(true); this.apiDeviceGroupsService - .deviceGroupControllerGetAll(this.deviceGroupsItemsPerPage, this.deviceGroupsPage * this.deviceGroupsItemsPerPage) + .deviceGroupControllerGetAll({ + limit: this.selectCount, + searchTerm: this.deviceGroupsSearch, + }) .subscribe({ next: (deviceGroups) => { this.deviceGroupsIsLoading.set(false); - const newDeviceGroups = [ - ...this.deviceGroups(), - ...deviceGroups, - ]; - this.deviceGroups.set(newDeviceGroups); - this.deviceGroupsPage += 1; + this.deviceGroups.set(deviceGroups); }, error: () => { this.deviceGroupsIsLoading.set(false); this.deviceGroups.set([]); - this.deviceGroupsPage = 0; } }); } - loadMoreLocations(init = false) { - if (init) { - this.locationsPage = 0; - this.locations.set([]); - } + loadMoreLocations() { this.locationsIsLoading.set(true); this.apiLocationsService - .locationControllerGetAll(this.locationsItemsPerPage, this.locationsPage * this.locationsItemsPerPage) + .locationControllerGetAll({ + limit: this.selectCount, + searchTerm: this.locationSearch + }) .subscribe({ next: (locations) => { this.locationsIsLoading.set(false); - const newlocations = [ - ...this.locations(), - ...locations, - ]; - this.locations.set(newlocations); - this.locationsPage += 1; + this.locations.set(locations); }, error: () => { this.locationsIsLoading.set(false); this.locations.set([]); - this.locationsPage = 0; } }); } + + onSearchLocation(value: string): void { + this.locationSearch$.next({propagate: true, value}); + } + + onSearchType(value: string): void { + this.deviceTypesSearch$.next({propagate: true, value}); + } + + onSearchGroup(value: string): void { + this.deviceGroupsSearch$.next({propagate: true, value}); + } } diff --git a/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.html b/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.html index 468e73b..9df7f9d 100644 --- a/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.html +++ b/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.html @@ -47,21 +47,20 @@

Der Gerät wurde nicht gefunden!

Standort/Fahrzeug - @for (item of locations(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (locationsIsLoading()) { + } @else { + @for (item of locations(); track item) { + + } } - + @@ -69,20 +68,20 @@

Der Gerät wurde nicht gefunden!

Geräte-Typ - @for (item of deviceTypes(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (deviceTypesIsLoading()) { + } @else { + @for (item of deviceTypes(); track item) { + + } } - + @@ -90,20 +89,20 @@

Der Gerät wurde nicht gefunden!

Geräte-Gruppe - @for (item of deviceGroups(); track item) { - - } - - + nzShowSearch + nzServerSearch> @if (deviceGroupsIsLoading()) { + } @else { + @for (item of deviceGroups(); track item) { + + } } - + diff --git a/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.ts b/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.ts index 65ffd6a..d2b41f0 100644 --- a/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.ts +++ b/frontend/src/app/pages/inventory/devices/device-detail/device-detail.component.ts @@ -195,15 +195,15 @@ export class DeviceDetailComponent implements OnInit, OnDestroy { this.service.delete(); } - loadMoreTypes() { - this.service.loadMoreTypes(); + onSearchLocation(search: string) { + this.service.onSearchLocation(search); } - loadMoreGroups() { - this.service.loadMoreGroups(); + onSearchType(search: string) { + this.service.onSearchType(search); } - loadMoreLocations() { - this.service.loadMoreLocations(); + onSearchGroup(search: string) { + this.service.onSearchGroup(search); } } diff --git a/frontend/src/app/pages/inventory/devices/device-detail/device-detail.service.ts b/frontend/src/app/pages/inventory/devices/device-detail/device-detail.service.ts index 8265dbd..bdb152c 100644 --- a/frontend/src/app/pages/inventory/devices/device-detail/device-detail.service.ts +++ b/frontend/src/app/pages/inventory/devices/device-detail/device-detail.service.ts @@ -1,9 +1,9 @@ -import {Injectable, signal} from '@angular/core'; +import {Inject, Injectable, signal} from '@angular/core'; import {DeviceService} from '@backend/api/device.service'; import {DeviceTypeService} from '@backend/api/deviceType.service'; import {Router} from '@angular/router'; import {HttpErrorResponse} from '@angular/common/http'; -import {Subject} from 'rxjs'; +import {BehaviorSubject, debounceTime, filter, Subject} from 'rxjs'; import {DeviceDto} from '@backend/model/deviceDto'; import {DeviceTypeDto} from '@backend/model/deviceTypeDto'; import {DeviceUpdateDto} from '@backend/model/deviceUpdateDto'; @@ -11,6 +11,7 @@ import {DeviceGroupDto} from '@backend/model/deviceGroupDto'; import {DeviceGroupService} from '@backend/api/deviceGroup.service'; import {LocationDto} from '@backend/model/locationDto'; import {LocationService} from '@backend/api/location.service'; +import {SEARCH_DEBOUNCE_TIME, SELECT_ITEMS_COUNT} from '../../../../app.configs'; @Injectable({ providedIn: 'root' @@ -28,20 +29,20 @@ export class DeviceDetailService { updateLoadingSuccess = new Subject(); deleteLoadingSuccess = new Subject(); + deviceTypesSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + deviceTypesSearch = ''; deviceTypes = signal([]); deviceTypesIsLoading = signal(false); - private deviceTypesPage = 0; - private deviceTypesItemsPerPage = 10; + deviceGroupsSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + deviceGroupsSearch = ''; deviceGroups = signal([]); deviceGroupsIsLoading = signal(false); - private deviceGroupsPage = 0; - private deviceGroupsItemsPerPage = 10; + locationSearch$ = new BehaviorSubject<{ propagate: boolean; value: string }>({propagate: false, value: ''}); + locationSearch = ''; locations = signal([]); locationsIsLoading = signal(false); - private locationsPage = 0; - private locationsItemsPerPage = 10; constructor( private readonly apiService: DeviceService, @@ -49,19 +50,40 @@ export class DeviceDetailService { private readonly apiDeviceGroupsService: DeviceGroupService, private readonly apiLocationsService: LocationService, private readonly router: Router, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + @Inject(SELECT_ITEMS_COUNT) private readonly selectCount: number, ) { + this.locationSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.locationSearch = x.value; + this.loadMoreLocations(); + }); + this.deviceTypesSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.deviceTypesSearch = x.value; + this.loadMoreTypes(); + }); + this.deviceGroupsSearch$.pipe( + filter(x => x.propagate), + debounceTime(time), + ).subscribe((x) => { + this.deviceGroupsSearch = x.value; + this.loadMoreGroups(); + }); } load(id: number) { this.id = id; - this.deviceTypesPage = 0; - this.deviceGroupsPage = 0; - this.locationsPage = 0; + this.deviceTypes.set([]); this.deviceGroups.set([]); this.locations.set([]); this.loading.set(true); - this.apiService.deviceControllerGetOne(id) + this.apiService.deviceControllerGetOne({id}) .subscribe({ next: (newEntity) => { this.entity.set(newEntity); @@ -76,9 +98,6 @@ export class DeviceDetailService { if (newEntity.location) { this.locations.set([newEntity.location]); } - this.loadMoreTypes(); - this.loadMoreGroups(); - this.loadMoreLocations(); }, error: (err: HttpErrorResponse) => { if (err.status === 404) { @@ -95,7 +114,7 @@ export class DeviceDetailService { const entity = this.entity(); if (entity) { this.updateLoading.set(true); - this.apiService.deviceControllerUpdate(entity.id, rawValue) + this.apiService.deviceControllerUpdate({id: entity.id, deviceUpdateDto: rawValue}) .subscribe({ next: (newEntity) => { this.updateLoading.set(false); @@ -114,7 +133,7 @@ export class DeviceDetailService { const entity = this.entity(); if (entity) { this.deleteLoading.set(true); - this.apiService.deviceControllerDelete(entity.id) + this.apiService.deviceControllerDelete({id: entity.id}) .subscribe({ next: () => { this.deleteLoading.set(false); @@ -132,21 +151,18 @@ export class DeviceDetailService { loadMoreTypes() { this.deviceTypesIsLoading.set(true); this.apiDeviceTypesService - .deviceTypeControllerGetAll(this.deviceTypesItemsPerPage, this.deviceTypesPage * this.deviceTypesItemsPerPage) + .deviceTypeControllerGetAll({ + limit: this.selectCount, + searchTerm: this.deviceTypesSearch, + }) .subscribe({ next: (deviceTypes) => { this.deviceTypesIsLoading.set(false); - const newDeviceTypes = [ - ...this.deviceTypes(), - ...deviceTypes.filter(x => x.id != this.entity()?.typeId), - ]; - this.deviceTypes.set(newDeviceTypes); - this.deviceTypesPage += 1; + this.deviceTypes.set(deviceTypes); }, error: () => { this.deviceTypesIsLoading.set(false); this.deviceTypes.set([]); - this.deviceTypesPage = 0; } }); } @@ -154,21 +170,18 @@ export class DeviceDetailService { loadMoreGroups() { this.deviceGroupsIsLoading.set(true); this.apiDeviceGroupsService - .deviceGroupControllerGetAll(this.deviceGroupsItemsPerPage, this.deviceGroupsPage * this.deviceGroupsItemsPerPage) + .deviceGroupControllerGetAll({ + limit: this.selectCount, + searchTerm: this.deviceGroupsSearch, + }) .subscribe({ next: (deviceGroups) => { this.deviceGroupsIsLoading.set(false); - const newDeviceGroups = [ - ...this.deviceGroups(), - ...deviceGroups.filter(x => x.id != this.entity()?.groupId), - ]; - this.deviceGroups.set(newDeviceGroups); - this.deviceGroupsPage += 1; + this.deviceGroups.set(deviceGroups); }, error: () => { this.deviceGroupsIsLoading.set(false); this.deviceGroups.set([]); - this.deviceGroupsPage = 0; } }); } @@ -176,22 +189,31 @@ export class DeviceDetailService { loadMoreLocations() { this.locationsIsLoading.set(true); this.apiLocationsService - .locationControllerGetAll(this.locationsItemsPerPage, this.locationsPage * this.locationsItemsPerPage) + .locationControllerGetAll({ + limit: this.selectCount, + searchTerm: this.locationSearch, + }) .subscribe({ next: (locations) => { this.locationsIsLoading.set(false); - const newlocations = [ - ...this.locations(), - ...locations.filter(x => x.id != this.entity()?.locationId), - ]; - this.locations.set(newlocations); - this.locationsPage += 1; + this.locations.set(locations); }, error: () => { this.locationsIsLoading.set(false); this.locations.set([]); - this.locationsPage = 0; } }); } + + onSearchLocation(value: string): void { + this.locationSearch$.next({propagate: true, value}); + } + + onSearchType(value: string): void { + this.deviceTypesSearch$.next({propagate: true, value}); + } + + onSearchGroup(value: string): void { + this.deviceGroupsSearch$.next({propagate: true, value}); + } } diff --git a/frontend/src/app/pages/inventory/devices/devices.component.html b/frontend/src/app/pages/inventory/devices/devices.component.html index 83f2476..30093a6 100644 --- a/frontend/src/app/pages/inventory/devices/devices.component.html +++ b/frontend/src/app/pages/inventory/devices/devices.component.html @@ -1,3 +1,15 @@

Geräte

- +
+
+ +
+
+ + + + + + +
+
diff --git a/frontend/src/app/pages/inventory/devices/devices.component.ts b/frontend/src/app/pages/inventory/devices/devices.component.ts index 95053ab..4f79b5c 100644 --- a/frontend/src/app/pages/inventory/devices/devices.component.ts +++ b/frontend/src/app/pages/inventory/devices/devices.component.ts @@ -1,9 +1,15 @@ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {HasRoleDirective} from '../../../core/auth/has-role.directive'; import {NzButtonComponent} from 'ng-zorro-antd/button'; import {NzWaveDirective} from 'ng-zorro-antd/core/wave'; import {RouterLink} from '@angular/router'; import {DeviceListComponent} from './device-list/device-list.component'; +import {NzGridModule} from 'ng-zorro-antd/grid'; +import {NzIconModule} from 'ng-zorro-antd/icon'; +import { + NzInputModule +} from 'ng-zorro-antd/input'; +import {DevicesService} from './devices.service'; @Component({ selector: 'ofs-devices', @@ -12,12 +18,25 @@ import {DeviceListComponent} from './device-list/device-list.component'; NzButtonComponent, NzWaveDirective, RouterLink, - DeviceListComponent + DeviceListComponent, + NzGridModule, + NzInputModule, + NzIconModule, ], standalone: true, templateUrl: './devices.component.html', styleUrl: './devices.component.less' }) -export class DevicesComponent { +export class DevicesComponent implements OnInit { + constructor(private readonly service: DevicesService) { + } + search($event: Event) { + const target = $event.target as HTMLInputElement; + this.service.search(target.value); + } + + ngOnInit(): void { + this.service.init(); + } } diff --git a/frontend/src/app/pages/inventory/devices/devices.service.ts b/frontend/src/app/pages/inventory/devices/devices.service.ts index b07e3ac..9345700 100644 --- a/frontend/src/app/pages/inventory/devices/devices.service.ts +++ b/frontend/src/app/pages/inventory/devices/devices.service.ts @@ -1,6 +1,8 @@ -import {Injectable, signal} from '@angular/core'; +import {Inject, Injectable, signal} from '@angular/core'; import {DeviceDto} from '@backend/model/deviceDto'; import {DeviceService} from '@backend/api/device.service'; +import {BehaviorSubject, debounceTime, distinctUntilChanged, filter} from 'rxjs'; +import {SEARCH_DEBOUNCE_TIME} from '../../../app.configs'; @Injectable({ providedIn: 'root' @@ -14,16 +16,36 @@ export class DevicesService { entitiesLoadError = signal(false); sortCol?: string; sortDir?: string; + searchTerm$ = new BehaviorSubject<{ propagate: boolean, value: string }>({propagate: true, value: ''}); + private searchTerm?: string; - constructor(private readonly apiService: DeviceService) { + constructor( + private readonly apiService: DeviceService, + @Inject(SEARCH_DEBOUNCE_TIME) time: number, + ) { + this.searchTerm$ + .pipe( + filter(x => x.propagate), + debounceTime(time), + distinctUntilChanged(), + ) + .subscribe(term => { + this.searchTerm = term.value; + this.load(); + }); } load() { this.entitiesLoading.set(true); - this.apiService.deviceControllerGetCount() + this.apiService.deviceControllerGetCount({searchTerm: this.searchTerm}) .subscribe((count) => this.total.set(count.count)); - this.apiService.deviceControllerGetAll(this.itemsPerPage, (this.page - 1) * this.itemsPerPage, undefined, - undefined, undefined, this.sortCol, this.sortDir) + this.apiService.deviceControllerGetAll({ + limit: this.itemsPerPage, + offset: (this.page - 1) * this.itemsPerPage, + searchTerm: this.searchTerm, + sortCol: this.sortCol, + sortDir: this.sortDir, + }) .subscribe({ next: (entities) => { this.entities.set(entities); @@ -45,4 +67,14 @@ export class DevicesService { this.sortDir = this.sortCol ? sortDir : undefined; this.load(); } + + search(term: string) { + this.searchTerm$.next({propagate: true, value: term}); + this.page = 1; + } + + init() { + this.searchTerm = ''; + this.searchTerm$.next({propagate: false, value: ''}); + } } diff --git a/openapi/backend.yml b/openapi/backend.yml index 995f3f3..138e4ed 100644 --- a/openapi/backend.yml +++ b/openapi/backend.yml @@ -512,7 +512,12 @@ paths: get: description: Gibt die Anzahl aller Device-Typen zurück operationId: DeviceTypeController_getCount - parameters: [] + parameters: + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -552,6 +557,11 @@ paths: in: query schema: type: string + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -676,7 +686,12 @@ paths: get: description: Gibt die Anzahl aller Geräte-Gruppen zurück operationId: DeviceGroupController_getCount - parameters: [] + parameters: + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -716,6 +731,11 @@ paths: in: query schema: type: string + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -840,7 +860,12 @@ paths: get: description: Gibt die Anzahl aller Geräte zurück operationId: DeviceController_getCount - parameters: [] + parameters: + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -898,6 +923,11 @@ paths: in: query schema: type: string + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -1022,7 +1052,12 @@ paths: get: description: Gibt die Anzahl aller Standorte zurück operationId: LocationController_getCount - parameters: [] + parameters: + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: '' @@ -1062,6 +1097,11 @@ paths: in: query schema: type: string + - name: searchTerm + required: false + in: query + schema: + type: string responses: '200': description: ''