diff --git a/src/app-storage/app-storage.controller.ts b/src/app-storage/app-storage.controller.ts index 9aa9cf8..ae1557a 100644 --- a/src/app-storage/app-storage.controller.ts +++ b/src/app-storage/app-storage.controller.ts @@ -4,8 +4,11 @@ import { Controller, Delete, Get, + Param, + ParseIntPipe, Patch, Post, + Query, UseGuards, UseInterceptors, } from '@nestjs/common'; @@ -13,6 +16,7 @@ import { ApiBody, ApiOkResponse, ApiOperation, + ApiParam, ApiSecurity, ApiTags, } from '@nestjs/swagger'; @@ -30,6 +34,15 @@ import { RoomGuard } from 'src/shared/guards/room.guard'; import { StorageSetGuard } from 'src/shared/guards/storage-set.guard'; import { StorageGuard } from 'src/shared/guards/storage.guard'; import { AppStorageService } from './app-storage.service'; +import { + StorageAddExistingItemRequestDto, + StorageAddItemRequestDto, +} from './dto/storage-add-item-request.dto'; +import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; +import { + StorageItemSearchResponseDto, + StorageStoredItemResponseDto, +} from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, StorageUpdateRequestDto, @@ -51,28 +64,9 @@ import { StorageSetResponseDto } from './dto/storage-set-response.dto'; export class AppStorageController { constructor(private readonly service: AppStorageService) {} - @Get(':storageId') - @ApiOperation({ summary: 'Get storage by ID' }) - @ApiOkResponse({ type: StorageResponseDto }) - async getStorage( - @CurrentStorage() storage: Storage, - ): Promise { - return this.service.formatStorageNoItems(storage); - } - - @Patch(':storageId') - @ApiBody({ type: StorageUpdateRequestDto }) - @ApiOperation({ summary: 'Update storage by ID' }) - @ApiOkResponse({ type: StorageResponseDto }) - async updateStorage( - @CurrentStorage() storage: Storage, - @Body() body: StorageUpdateRequestDto, - ): Promise { - return this.service.updateStorage(storage, body); - } - @UseGuards(StorageSetGuard) @Get('set/:storageSetId') + @ApiParam({ name: 'storageSetId', description: 'Storage set ID' }) @ApiOperation({ summary: 'Get storage set' }) @ApiOkResponse({ type: StorageSetResponseDto }) async getStorageSet(@CurrentStorageSet() set: StorageSet) { @@ -81,6 +75,7 @@ export class AppStorageController { @UseGuards(StorageSetGuard) @Patch('set/:storageSetId') + @ApiParam({ name: 'storageSetId', description: 'Storage set ID' }) @ApiBody({ type: StorageSetUpdateRequestDto }) @ApiOperation({ summary: 'Update storage set by ID' }) @ApiOkResponse({ type: StorageSetResponseDto }) @@ -93,6 +88,8 @@ export class AppStorageController { @UseGuards(StorageSetGuard) @Post('set/:storageSetId/:storageId') + @ApiParam({ name: 'storageSetId', description: 'Storage set ID' }) + @ApiParam({ name: 'storageId', description: 'Storage ID' }) @ApiOperation({ summary: 'Move storage to storage set' }) @ApiOkResponse({ type: StorageSetResponseDto }) async moveStorage( @@ -104,6 +101,8 @@ export class AppStorageController { @UseGuards(StorageSetGuard) @Delete('set/:storageSetId/:storageId') + @ApiParam({ name: 'storageSetId', description: 'Storage set ID' }) + @ApiParam({ name: 'storageId', description: 'Storage ID' }) @ApiOperation({ summary: 'Remove storage from storage set' }) @ApiOkResponse({ type: StorageSetResponseDto }) async removeStorageFromSet( @@ -114,6 +113,7 @@ export class AppStorageController { } @Get('room/:roomId') + @ApiParam({ name: 'roomId', description: 'Room ID' }) @ApiOperation({ summary: 'Get storages in room' }) @ApiOkResponse({ type: StorageResponseDto, isArray: true }) async getStorages(@CurrentRoom() room: Room) { @@ -121,6 +121,7 @@ export class AppStorageController { } @Get('set/room/:roomId') + @ApiParam({ name: 'roomId', description: 'Room ID' }) @ApiOperation({ summary: 'Get storage sets in room' }) @ApiOkResponse({ type: StorageSetResponseDto, isArray: true }) async getStorageSets(@CurrentRoom() room: Room) { @@ -128,6 +129,7 @@ export class AppStorageController { } @Post('set/room/:roomId') + @ApiParam({ name: 'roomId', description: 'Room ID' }) @ApiBody({ type: StorageCreateRequestDto }) @ApiOperation({ summary: 'Create storage sets in room' }) @ApiOkResponse({ type: StorageSetResponseDto, isArray: true }) @@ -140,6 +142,7 @@ export class AppStorageController { } @Post('room/:roomId') + @ApiParam({ name: 'roomId', description: 'Room ID' }) @ApiBody({ type: StorageCreateRequestDto }) @ApiOperation({ summary: 'Add a new storage to room' }) @ApiOkResponse({ type: StorageResponseDto }) @@ -150,4 +153,64 @@ export class AppStorageController { ): Promise { return this.service.createStorage(user, room, body); } + + @Get('item') + @ApiOperation({ summary: 'Search for an item' }) + @ApiOkResponse({ type: StorageItemSearchResponseDto, isArray: true }) + async searchForItem( + @LoggedInUser() user: User, + @Query() search: StorageItemRequestQueryDto, + ) { + return this.service.searchForItems(user, search); + } + + @Post('item/:storageId') + @ApiParam({ name: 'storageId', description: 'Storage ID' }) + @ApiBody({ type: StorageAddItemRequestDto }) + @ApiOperation({ summary: 'Add a new item to storage' }) + @ApiOkResponse({ type: StorageStoredItemResponseDto }) + async addItemToStorage( + @LoggedInUser() user: User, + @Body() body: StorageAddItemRequestDto, + @CurrentStorage() storage: Storage, + ) { + return; + } + + @Post('item/:storageId/:itemId') + @ApiParam({ name: 'storageId', description: 'Storage ID' }) + @ApiParam({ name: 'itemId', description: 'Existing item ID' }) + @ApiBody({ type: StorageAddExistingItemRequestDto }) + @ApiOperation({ summary: 'Add an instance of an existing item to storage' }) + @ApiOkResponse({ type: StorageStoredItemResponseDto }) + async addExistingItemToStorage( + @Param('itemId', ParseIntPipe) itemId: number, + @LoggedInUser() user: User, + @Body() body: StorageAddExistingItemRequestDto, + @CurrentStorage() storage: Storage, + ) { + return; + } + + @Get(':storageId') + @ApiParam({ name: 'storageId', description: 'Storage ID' }) + @ApiOperation({ summary: 'Get storage by ID' }) + @ApiOkResponse({ type: StorageResponseDto }) + async getStorage( + @CurrentStorage() storage: Storage, + ): Promise { + return this.service.formatStorageNoItems(storage); + } + + @Patch(':storageId') + @ApiParam({ name: 'storageId', description: 'Storage ID' }) + @ApiBody({ type: StorageUpdateRequestDto }) + @ApiOperation({ summary: 'Update storage by ID' }) + @ApiOkResponse({ type: StorageResponseDto }) + async updateStorage( + @CurrentStorage() storage: Storage, + @Body() body: StorageUpdateRequestDto, + ): Promise { + return this.service.updateStorage(storage, body); + } } diff --git a/src/app-storage/app-storage.service.ts b/src/app-storage/app-storage.service.ts index 0589015..1038b21 100644 --- a/src/app-storage/app-storage.service.ts +++ b/src/app-storage/app-storage.service.ts @@ -7,6 +7,8 @@ import { StorageSet } from 'src/objects/storage/entities/storage-set.entity'; import { Storage } from 'src/objects/storage/entities/storage.entity'; import { StorageService } from 'src/objects/storage/storage.service'; import { User } from 'src/objects/user/user.entity'; +import { StorageItemRequestQueryDto } from './dto/storage-item-request.dto'; +import { StorageItemSearchResponseDto } from './dto/storage-item-response.dto'; import { StorageCreateRequestDto, StorageUpdateRequestDto, @@ -98,6 +100,22 @@ export class AppStorageService { return this.formatStorageSetNoItems(set); } + async searchForItems(user: User, query: StorageItemRequestQueryDto) { + let responses: StorageItemSearchResponseDto[]; + if (query.searchTerm) { + responses = await this.storageService.searchForItem( + query.searchTerm, + user.sub, + ); + } else { + responses = await this.storageService.searchForItemByBarcode( + query.barcode, + user.sub, + ); + } + return responses; + } + formatActor(input: User): StorageActorResponse { return pick(input, ['name', 'sub', 'color']); } diff --git a/src/app-storage/dto/storage-add-item-request.dto.ts b/src/app-storage/dto/storage-add-item-request.dto.ts new file mode 100644 index 0000000..2411140 --- /dev/null +++ b/src/app-storage/dto/storage-add-item-request.dto.ts @@ -0,0 +1,143 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsDate, + IsEnum, + IsNumber, + IsObject, + IsOptional, + IsString, + MaxLength, + MinLength, + ValidateNested, +} from 'class-validator'; +import { ItemType } from 'src/objects/storage/enums/item-type.enum'; +import { TransactionType } from 'src/objects/storage/enums/transaction-type.enum'; + +export class StorageItemRequestDto { + @ApiProperty() + @IsString() + @MinLength(3) + @MaxLength(32) + displayName: string; + + @ApiProperty({ type: String, enum: ItemType }) + @IsEnum(ItemType) + type: ItemType; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + barcode?: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + consumable?: boolean; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + image?: string; + + @ApiPropertyOptional() + @IsNumber() + @IsOptional() + weight?: number; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + url?: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + notes?: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + public?: boolean; +} + +export class StorageStoredItemRequestDto { + @ApiProperty() + @IsString() + @IsOptional() + notes?: string; + + @ApiPropertyOptional() + @IsDate() + @IsOptional() + expiresAt?: Date; + + @ApiPropertyOptional() + @IsDate() + @IsOptional() + acquiredAt?: Date; + + @ApiPropertyOptional() + @IsDate() + @IsOptional() + consumedAt?: Date; +} + +export class StorageStoredItemTransactionRequestDto { + @ApiProperty({ type: String, enum: TransactionType }) + @IsEnum(TransactionType) + type: TransactionType; + + @ApiPropertyOptional() + @IsNumber() + @IsOptional() + price?: number; + + @ApiPropertyOptional() + @IsString() + @MaxLength(3) + @MinLength(3) + @IsOptional() + currency?: string; + + @ApiProperty() + @IsString() + @IsOptional() + notes?: string; + + @ApiPropertyOptional() + @IsDate() + @IsOptional() + actionAt?: Date; +} + +export class StorageAddExistingItemRequestDto { + @ApiPropertyOptional({ type: StorageStoredItemRequestDto }) + @Type(() => StorageStoredItemRequestDto) + @IsObject() + @IsOptional() + @ValidateNested() + additionalInfo: StorageStoredItemRequestDto; + + @ApiProperty({ type: StorageStoredItemTransactionRequestDto }) + @Type(() => StorageStoredItemTransactionRequestDto) + @IsObject() + @ValidateNested() + transactionInfo: StorageStoredItemTransactionRequestDto; +} + +export class StorageAddItemRequestDto extends StorageItemRequestDto { + @ApiPropertyOptional({ type: StorageStoredItemRequestDto }) + @Type(() => StorageStoredItemRequestDto) + @IsObject() + @IsOptional() + @ValidateNested() + additionalInfo: StorageStoredItemRequestDto; + + @ApiProperty({ type: StorageStoredItemTransactionRequestDto }) + @Type(() => StorageStoredItemTransactionRequestDto) + @IsObject() + @ValidateNested() + transactionInfo: StorageStoredItemTransactionRequestDto; +} diff --git a/src/app-storage/dto/storage-item-request.dto.ts b/src/app-storage/dto/storage-item-request.dto.ts new file mode 100644 index 0000000..7835d96 --- /dev/null +++ b/src/app-storage/dto/storage-item-request.dto.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, ValidateIf } from 'class-validator'; + +export class StorageItemRequestQueryDto { + @ApiPropertyOptional() + @IsString() + @ValidateIf((obj) => !obj.barcode) + searchTerm?: string; + + @ApiPropertyOptional() + @IsString() + @ValidateIf((obj) => !obj.searchTerm) + barcode?: string; +} diff --git a/src/app-storage/dto/storage-item-response.dto.ts b/src/app-storage/dto/storage-item-response.dto.ts new file mode 100644 index 0000000..8cf4d3e --- /dev/null +++ b/src/app-storage/dto/storage-item-response.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { StoredItemTransaction } from 'src/objects/storage/entities/item-transaction.entity'; +import { Item } from 'src/objects/storage/entities/item.entity'; +import { StoredItem } from 'src/objects/storage/entities/stored-item.entity'; +import { StorageActorResponse } from './storage-response.dto'; + +export class StorageItemResponseDto extends OmitType(Item, [ + 'instances', + 'addedBy', +]) { + @ApiProperty({ type: StorageActorResponse }) + addedBy: StorageActorResponse; +} + +export class StorageItemSearchResponseDto extends PickType(Item, [ + 'id', + 'displayName', + 'type', + 'barcode', + 'image', + 'createdAt', +]) {} + +export class StorageStoredItemResponseDto extends OmitType(StoredItem, [ + 'transactions', + 'storage', + 'item', +]) { + @ApiProperty({ type: StorageItemResponseDto }) + @Type(() => StorageItemResponseDto) + item: StorageItemResponseDto; +} + +export class StorageStoredItemTransactionDto extends OmitType( + StoredItemTransaction, + ['storedItem'], +) { + @ApiProperty({ type: StorageStoredItemResponseDto }) + @Type(() => StorageStoredItemResponseDto) + storedItem: StorageStoredItemResponseDto; +} diff --git a/src/objects/storage/entities/stored-item.entity.ts b/src/objects/storage/entities/stored-item.entity.ts index 3a8a75b..43d411f 100644 --- a/src/objects/storage/entities/stored-item.entity.ts +++ b/src/objects/storage/entities/stored-item.entity.ts @@ -19,10 +19,6 @@ export class StoredItem { @PrimaryGeneratedColumn() id: number; - @ApiProperty() - @Column() - displayName: string; - @ApiProperty({ type: () => Item }) @ManyToOne(() => Item, { onDelete: 'CASCADE', diff --git a/src/objects/storage/storage.service.ts b/src/objects/storage/storage.service.ts index 1603c81..91550d0 100644 --- a/src/objects/storage/storage.service.ts +++ b/src/objects/storage/storage.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { ILike, Repository } from 'typeorm'; import { BuildingService } from '../building/building.service'; import { GroupService } from '../group/group.service'; import { UserService } from '../user/user.service'; @@ -157,6 +157,65 @@ export class StorageService { }); } + async searchForItem(searchTerm: string, sub: string) { + const displayName = ILike(`%${searchTerm}%`); + return this.itemRepository.find({ + where: [ + { + displayName, + addedBy: { + sub, + }, + }, + { + displayName, + public: true, + }, + { + displayName, + addedBy: { + groups: { + members: { + sub, + }, + }, + }, + }, + ], + take: 10, + select: ['id', 'displayName', 'type', 'barcode', 'image', 'createdAt'], + }); + } + + async searchForItemByBarcode(barcode: string, sub: string) { + return this.itemRepository.find({ + where: [ + { + barcode, + addedBy: { + sub, + }, + }, + { + barcode, + public: true, + }, + { + barcode, + addedBy: { + groups: { + members: { + sub, + }, + }, + }, + }, + ], + take: 10, + select: ['id', 'displayName', 'type', 'barcode', 'image', 'createdAt'], + }); + } + async saveStorage(data: Partial) { const newStorage = new Storage(); Object.assign(newStorage, data);