From 9ebcb34d64f318c8252c6b571bd7c0f51c6a1e4f Mon Sep 17 00:00:00 2001 From: tolulopedd26 Date: Fri, 26 Jun 2026 11:05:35 +0100 Subject: [PATCH] feat: asset notes, QR/barcode, bulk ops, check-in/out - Implement asset notes CRUD (#900) - Add QR code and barcode generation endpoints (#901) - Implement bulk status update, delete, transfer, export (#902) - Add check-in/check-out workflow with due date tracking (#903) --- backend/src/app.module.ts | 4 + backend/src/assets/assets-ops.controller.ts | 65 ++++++++++ backend/src/assets/assets-ops.module.ts | 14 +++ backend/src/assets/assets-ops.service.ts | 111 ++++++++++++++++++ backend/src/assets/dtos/bulk-delete.dto.ts | 6 + backend/src/assets/dtos/bulk-status.dto.ts | 9 ++ backend/src/assets/dtos/bulk-transfer.dto.ts | 18 +++ backend/src/assets/dtos/create-note.dto.ts | 6 + .../src/assets/entities/asset-note.entity.ts | 32 +++++ backend/src/assets/entities/asset.entity.ts | 67 +++++++++++ backend/src/auth/auth.module.ts | 13 +- backend/src/auth/strategies/jwt.strategy.ts | 27 +++++ backend/src/checkin/checkin.controller.ts | 31 +++++ backend/src/checkin/checkin.entity.ts | 53 +++++++++ backend/src/checkin/checkin.module.ts | 14 +++ backend/src/checkin/checkin.service.ts | 84 +++++++++++++ backend/src/checkin/dtos/checkin.dto.ts | 10 ++ backend/src/checkin/dtos/checkout.dto.ts | 18 +++ backend/src/users/users.service.ts | 4 + 19 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 backend/src/assets/assets-ops.controller.ts create mode 100644 backend/src/assets/assets-ops.module.ts create mode 100644 backend/src/assets/assets-ops.service.ts create mode 100644 backend/src/assets/dtos/bulk-delete.dto.ts create mode 100644 backend/src/assets/dtos/bulk-status.dto.ts create mode 100644 backend/src/assets/dtos/bulk-transfer.dto.ts create mode 100644 backend/src/assets/dtos/create-note.dto.ts create mode 100644 backend/src/assets/entities/asset-note.entity.ts create mode 100644 backend/src/assets/entities/asset.entity.ts create mode 100644 backend/src/auth/strategies/jwt.strategy.ts create mode 100644 backend/src/checkin/checkin.controller.ts create mode 100644 backend/src/checkin/checkin.entity.ts create mode 100644 backend/src/checkin/checkin.module.ts create mode 100644 backend/src/checkin/checkin.service.ts create mode 100644 backend/src/checkin/dtos/checkin.dto.ts create mode 100644 backend/src/checkin/dtos/checkout.dto.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 414286bd..50065307 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -7,6 +7,8 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UsersModule } from './users/users.module'; import { AuthModule } from './auth/auth.module'; +import { AssetsOpsModule } from './assets/assets-ops.module'; +import { CheckinModule } from './checkin/checkin.module'; import { CacheService } from './cache/cache.service'; @Module({ @@ -56,6 +58,8 @@ import { CacheService } from './cache/cache.service'; }, }), + AssetsOpsModule, + CheckinModule, UsersModule, AuthModule, ], diff --git a/backend/src/assets/assets-ops.controller.ts b/backend/src/assets/assets-ops.controller.ts new file mode 100644 index 00000000..e729c9f8 --- /dev/null +++ b/backend/src/assets/assets-ops.controller.ts @@ -0,0 +1,65 @@ +import { Controller, Post, Get, Delete, Param, Body, Req, UseGuards, Query } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { AssetsOpsService } from './assets-ops.service'; +import { CreateNoteDto } from './dtos/create-note.dto'; +import { BulkStatusDto } from './dtos/bulk-status.dto'; +import { BulkDeleteDto } from './dtos/bulk-delete.dto'; +import { BulkTransferDto } from './dtos/bulk-transfer.dto'; + +@Controller('assets') +@UseGuards(AuthGuard('jwt')) +export class AssetsOpsController { + constructor(private readonly assetsOpsService: AssetsOpsService) {} + + @Post(':id/notes') + async createNote(@Param('id') id: string, @Body() dto: CreateNoteDto, @Req() req: any) { + return this.assetsOpsService.createNote(id, dto, req.user?.id); + } + + @Get(':id/notes') + async getNotes(@Param('id') id: string) { + return this.assetsOpsService.getNotes(id); + } + + @Delete(':id/notes/:noteId') + async deleteNote(@Param('id') id: string, @Param('noteId') noteId: string) { + await this.assetsOpsService.deleteNote(id, noteId); + return { message: 'Note deleted' }; + } + + @Post(':id/qrcode') + async generateQRCode(@Param('id') id: string) { + const qrCode = await this.assetsOpsService.generateQRCode(id); + return { qrCode }; + } + + @Post(':id/barcode') + async generateBarcode(@Param('id') id: string) { + const barcode = await this.assetsOpsService.generateBarcode(id); + return { barcode }; + } + + @Post('bulk/status') + async bulkStatusUpdate(@Body() dto: BulkStatusDto, @Req() req: any) { + const count = await this.assetsOpsService.bulkStatusUpdate(dto, req.user?.id); + return { updated: count }; + } + + @Post('bulk/delete') + async bulkDelete(@Body() dto: BulkDeleteDto) { + const count = await this.assetsOpsService.bulkDelete(dto); + return { deleted: count }; + } + + @Post('bulk/transfer') + async bulkTransfer(@Body() dto: BulkTransferDto, @Req() req: any) { + const count = await this.assetsOpsService.bulkTransfer(dto, req.user?.id); + return { transferred: count }; + } + + @Get('bulk/export') + async bulkExport(@Query('ids') ids?: string) { + const idArray = ids ? ids.split(',') : undefined; + return this.assetsOpsService.bulkExport(idArray); + } +} diff --git a/backend/src/assets/assets-ops.module.ts b/backend/src/assets/assets-ops.module.ts new file mode 100644 index 00000000..93b894f0 --- /dev/null +++ b/backend/src/assets/assets-ops.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Asset } from './entities/asset.entity'; +import { AssetNote } from './entities/asset-note.entity'; +import { AssetsOpsService } from './assets-ops.service'; +import { AssetsOpsController } from './assets-ops.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Asset, AssetNote])], + controllers: [AssetsOpsController], + providers: [AssetsOpsService], + exports: [AssetsOpsService], +}) +export class AssetsOpsModule {} diff --git a/backend/src/assets/assets-ops.service.ts b/backend/src/assets/assets-ops.service.ts new file mode 100644 index 00000000..a7537a58 --- /dev/null +++ b/backend/src/assets/assets-ops.service.ts @@ -0,0 +1,111 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, In } from 'typeorm'; +import { Asset } from './entities/asset.entity'; +import { AssetNote } from './entities/asset-note.entity'; +import { CreateNoteDto } from './dtos/create-note.dto'; +import { BulkStatusDto } from './dtos/bulk-status.dto'; +import { BulkDeleteDto } from './dtos/bulk-delete.dto'; +import { BulkTransferDto } from './dtos/bulk-transfer.dto'; +import * as QRCode from 'qrcode'; +import * as bwipjs from 'bwip-js'; + +@Injectable() +export class AssetsOpsService { + constructor( + @InjectRepository(Asset) + private readonly assetRepository: Repository, + @InjectRepository(AssetNote) + private readonly noteRepository: Repository, + ) {} + + async createNote(assetId: string, dto: CreateNoteDto, userId?: string): Promise { + const asset = await this.assetRepository.findOne({ where: { id: assetId } }); + if (!asset) throw new NotFoundException('Asset not found'); + + const note = this.noteRepository.create({ + assetId, + content: dto.content, + createdById: userId, + }); + return this.noteRepository.save(note); + } + + async getNotes(assetId: string): Promise { + return this.noteRepository.find({ + where: { assetId }, + relations: ['createdBy'], + order: { createdAt: 'DESC' }, + }); + } + + async deleteNote(assetId: string, noteId: string): Promise { + const note = await this.noteRepository.findOne({ where: { id: noteId, assetId } }); + if (!note) throw new NotFoundException('Note not found'); + await this.noteRepository.remove(note); + } + + async generateQRCode(assetId: string): Promise { + const asset = await this.assetRepository.findOne({ where: { id: assetId } }); + if (!asset) throw new NotFoundException('Asset not found'); + + const qrData = JSON.stringify({ id: asset.id, assetId: asset.assetId, name: asset.name }); + const qrCode = await QRCode.toDataURL(qrData); + + await this.assetRepository.update(assetId, { qrCode }); + return qrCode; + } + + async generateBarcode(assetId: string): Promise { + const asset = await this.assetRepository.findOne({ where: { id: assetId } }); + if (!asset) throw new NotFoundException('Asset not found'); + + const barcode = await new Promise((resolve, reject) => { + bwipjs.toBuffer({ + bcid: 'code128', + text: asset.assetId, + scale: 3, + height: 10, + includetext: true, + textxalign: 'center', + }, (err: Error | null, buffer?: Buffer) => { + if (err) reject(err); + else resolve(`data:image/png;base64,${buffer.toString('base64')}`); + }); + }); + + await this.assetRepository.update(assetId, { barcode }); + return barcode; + } + + async bulkStatusUpdate(dto: BulkStatusDto, userId?: string): Promise { + const result = await this.assetRepository.update( + { id: In(dto.ids) }, + { status: dto.status, updatedById: userId }, + ); + return result.affected || 0; + } + + async bulkDelete(dto: BulkDeleteDto): Promise { + const result = await this.assetRepository.softDelete({ id: In(dto.ids) }); + return result.affected || 0; + } + + async bulkTransfer(dto: BulkTransferDto, userId?: string): Promise { + const updateData: Partial = { updatedById: userId }; + if (dto.assignedToId) updateData.assignedToId = dto.assignedToId; + if (dto.departmentId) updateData.departmentId = dto.departmentId; + if (dto.location) updateData.location = dto.location; + + const result = await this.assetRepository.update( + { id: In(dto.ids) }, + updateData, + ); + return result.affected || 0; + } + + async bulkExport(ids?: string[]) { + const where = ids ? { id: In(ids) } : {}; + return this.assetRepository.find({ where, relations: ['assignedTo', 'createdBy'] }); + } +} diff --git a/backend/src/assets/dtos/bulk-delete.dto.ts b/backend/src/assets/dtos/bulk-delete.dto.ts new file mode 100644 index 00000000..55fa55fa --- /dev/null +++ b/backend/src/assets/dtos/bulk-delete.dto.ts @@ -0,0 +1,6 @@ +import { IsArray } from 'class-validator'; + +export class BulkDeleteDto { + @IsArray() + ids: string[]; +} diff --git a/backend/src/assets/dtos/bulk-status.dto.ts b/backend/src/assets/dtos/bulk-status.dto.ts new file mode 100644 index 00000000..c8b6e18a --- /dev/null +++ b/backend/src/assets/dtos/bulk-status.dto.ts @@ -0,0 +1,9 @@ +import { IsArray, IsString } from 'class-validator'; + +export class BulkStatusDto { + @IsArray() + ids: string[]; + + @IsString() + status: string; +} diff --git a/backend/src/assets/dtos/bulk-transfer.dto.ts b/backend/src/assets/dtos/bulk-transfer.dto.ts new file mode 100644 index 00000000..bfe2b677 --- /dev/null +++ b/backend/src/assets/dtos/bulk-transfer.dto.ts @@ -0,0 +1,18 @@ +import { IsArray, IsOptional, IsString } from 'class-validator'; + +export class BulkTransferDto { + @IsArray() + ids: string[]; + + @IsOptional() + @IsString() + assignedToId?: string; + + @IsOptional() + @IsString() + departmentId?: string; + + @IsOptional() + @IsString() + location?: string; +} diff --git a/backend/src/assets/dtos/create-note.dto.ts b/backend/src/assets/dtos/create-note.dto.ts new file mode 100644 index 00000000..03cc8d59 --- /dev/null +++ b/backend/src/assets/dtos/create-note.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class CreateNoteDto { + @IsString() + content: string; +} diff --git a/backend/src/assets/entities/asset-note.entity.ts b/backend/src/assets/entities/asset-note.entity.ts new file mode 100644 index 00000000..3efa91c1 --- /dev/null +++ b/backend/src/assets/entities/asset-note.entity.ts @@ -0,0 +1,32 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Asset } from './asset.entity'; +import { User } from '../../users/entities/user.entity'; + +@Entity('asset_notes') +export class AssetNote { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + assetId: string; + + @ManyToOne(() => Asset, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'assetId' }) + asset: Asset; + + @Column({ type: 'text' }) + content: string; + + @Column({ nullable: true }) + createdById: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'createdById' }) + createdBy: User; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/assets/entities/asset.entity.ts b/backend/src/assets/entities/asset.entity.ts new file mode 100644 index 00000000..e02f4c74 --- /dev/null +++ b/backend/src/assets/entities/asset.entity.ts @@ -0,0 +1,67 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from '../../users/entities/user.entity'; + +@Entity('assets') +export class Asset { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + assetId: string; + + @Column() + name: string; + + @Column({ nullable: true, type: 'text' }) + description: string; + + @Column({ nullable: true }) + categoryId: string; + + @Column({ nullable: true }) + serialNumber: string; + + @Column({ default: 'ACTIVE' }) + status: string; + + @Column({ default: 'NEW' }) + condition: string; + + @Column({ nullable: true }) + departmentId: string; + + @Column({ nullable: true }) + location: string; + + @Column({ nullable: true }) + assignedToId: string; + + @Column({ nullable: true }) + barcode: string; + + @Column({ nullable: true }) + qrCode: string; + + @Column({ nullable: true, type: 'text' }) + notes: string; + + @Column({ nullable: true }) + createdById: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'createdById' }) + createdBy: User; + + @Column({ nullable: true }) + updatedById: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'updatedById' }) + updatedBy: User; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 1e522df2..8ee822f6 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -1,9 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { PassportModule } from '@nestjs/passport'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { GoogleStrategy } from './strategies/google.strategy'; +import { JwtStrategy } from './strategies/jwt.strategy'; import { PasswordResetToken } from './entities/password-reset-token.entity'; import { UsersModule } from '../users/users.module'; import { MailModule } from '../mail/mail.module'; @@ -12,11 +15,19 @@ import { MailModule } from '../mail/mail.module'; imports: [ TypeOrmModule.forFeature([PasswordResetToken]), PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET', 'change-me-in-env'), + signOptions: { expiresIn: '15m' }, + }), + }), UsersModule, MailModule, ], controllers: [AuthController], - providers: [AuthService, GoogleStrategy], + providers: [AuthService, GoogleStrategy, JwtStrategy], exports: [AuthService], }) export class AuthModule {} diff --git a/backend/src/auth/strategies/jwt.strategy.ts b/backend/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 00000000..023e528f --- /dev/null +++ b/backend/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,27 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { UsersService } from '../../users/users.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor( + private readonly configService: ConfigService, + private readonly usersService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET', 'change-me-in-env'), + }); + } + + async validate(payload: { sub: string; email: string }) { + const user = await this.usersService.findById(payload.sub); + if (!user) { + throw new UnauthorizedException(); + } + return user; + } +} diff --git a/backend/src/checkin/checkin.controller.ts b/backend/src/checkin/checkin.controller.ts new file mode 100644 index 00000000..fa882ba4 --- /dev/null +++ b/backend/src/checkin/checkin.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Post, Get, Param, Body, Req, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { CheckinService } from './checkin.service'; +import { CheckoutDto } from './dtos/checkout.dto'; +import { CheckinDto } from './dtos/checkin.dto'; + +@Controller('checkin') +@UseGuards(AuthGuard('jwt')) +export class CheckinController { + constructor(private readonly checkinService: CheckinService) {} + + @Post('checkout') + async checkout(@Body() dto: CheckoutDto, @Req() req: any) { + return this.checkinService.checkout(dto, req.user?.id); + } + + @Post('checkin') + async checkin(@Body() dto: CheckinDto, @Req() req: any) { + return this.checkinService.checkin(dto, req.user?.id); + } + + @Get('active') + async getActiveCheckouts() { + return this.checkinService.getActiveCheckouts(); + } + + @Get('asset/:assetId') + async getAssetHistory(@Param('assetId') assetId: string) { + return this.checkinService.getAssetHistory(assetId); + } +} diff --git a/backend/src/checkin/checkin.entity.ts b/backend/src/checkin/checkin.entity.ts new file mode 100644 index 00000000..1356a0bb --- /dev/null +++ b/backend/src/checkin/checkin.entity.ts @@ -0,0 +1,53 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from '../users/entities/user.entity'; + +@Entity('checkin_records') +export class CheckinRecord { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + assetId: string; + + @Column({ nullable: true }) + assignedToId: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'assignedToId' }) + assignedTo: User; + + @Column({ nullable: true }) + checkedOutById: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'checkedOutById' }) + checkedOutBy: User; + + @Column({ nullable: true }) + checkedInById: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'checkedInById' }) + checkedInBy: User; + + @Column({ type: 'timestamp' }) + checkedOutAt: Date; + + @Column({ type: 'timestamp', nullable: true }) + checkedInAt: Date; + + @Column({ type: 'timestamp', nullable: true }) + dueDate: Date; + + @Column({ nullable: true, type: 'text' }) + notes: string; + + @Column({ default: 'CHECKED_OUT' }) + status: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/checkin/checkin.module.ts b/backend/src/checkin/checkin.module.ts new file mode 100644 index 00000000..e459c13c --- /dev/null +++ b/backend/src/checkin/checkin.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CheckinRecord } from './checkin.entity'; +import { Asset } from '../assets/entities/asset.entity'; +import { CheckinService } from './checkin.service'; +import { CheckinController } from './checkin.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([CheckinRecord, Asset])], + controllers: [CheckinController], + providers: [CheckinService], + exports: [CheckinService], +}) +export class CheckinModule {} diff --git a/backend/src/checkin/checkin.service.ts b/backend/src/checkin/checkin.service.ts new file mode 100644 index 00000000..12ff5cc6 --- /dev/null +++ b/backend/src/checkin/checkin.service.ts @@ -0,0 +1,84 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CheckinRecord } from './checkin.entity'; +import { Asset } from '../assets/entities/asset.entity'; +import { CheckoutDto } from './dtos/checkout.dto'; +import { CheckinDto } from './dtos/checkin.dto'; + +@Injectable() +export class CheckinService { + constructor( + @InjectRepository(CheckinRecord) + private readonly checkinRepository: Repository, + @InjectRepository(Asset) + private readonly assetRepository: Repository, + ) {} + + async checkout(dto: CheckoutDto, userId?: string): Promise { + const asset = await this.assetRepository.findOne({ where: { id: dto.assetId } }); + if (!asset) throw new NotFoundException('Asset not found'); + + const active = await this.checkinRepository.findOne({ + where: { assetId: dto.assetId, status: 'CHECKED_OUT' }, + }); + if (active) throw new BadRequestException('Asset is already checked out'); + + const record = this.checkinRepository.create({ + assetId: dto.assetId, + assignedToId: dto.assignedToId, + checkedOutById: userId, + checkedOutAt: new Date(), + dueDate: dto.dueDate ? new Date(dto.dueDate) : null, + notes: dto.notes, + status: 'CHECKED_OUT', + }); + await this.checkinRepository.save(record); + + await this.assetRepository.update(dto.assetId, { + status: 'ASSIGNED', + assignedToId: dto.assignedToId, + updatedById: userId, + }); + + return record; + } + + async checkin(dto: CheckinDto, userId?: string): Promise { + const active = await this.checkinRepository.findOne({ + where: { assetId: dto.assetId, status: 'CHECKED_OUT' }, + order: { checkedOutAt: 'DESC' }, + }); + if (!active) throw new BadRequestException('Asset is not checked out'); + + active.checkedInAt = new Date(); + active.checkedInById = userId; + active.status = 'CHECKED_IN'; + if (dto.notes) active.notes = dto.notes; + await this.checkinRepository.save(active); + + await this.assetRepository.update(dto.assetId, { + status: 'ACTIVE', + assignedToId: null, + updatedById: userId, + }); + + return active; + } + + async getActiveCheckouts() { + return this.checkinRepository.find({ + where: { status: 'CHECKED_OUT' }, + relations: ['assignedTo', 'checkedOutBy'], + order: { checkedOutAt: 'DESC' }, + }); + } + + async getAssetHistory(assetId: string) { + return this.checkinRepository.find({ + where: { assetId }, + relations: ['assignedTo', 'checkedOutBy', 'checkedInBy'], + order: { checkedOutAt: 'DESC' }, + }); + } +} diff --git a/backend/src/checkin/dtos/checkin.dto.ts b/backend/src/checkin/dtos/checkin.dto.ts new file mode 100644 index 00000000..945451a6 --- /dev/null +++ b/backend/src/checkin/dtos/checkin.dto.ts @@ -0,0 +1,10 @@ +import { IsString, IsOptional } from 'class-validator'; + +export class CheckinDto { + @IsString() + assetId: string; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/checkin/dtos/checkout.dto.ts b/backend/src/checkin/dtos/checkout.dto.ts new file mode 100644 index 00000000..b2b8ecdb --- /dev/null +++ b/backend/src/checkin/dtos/checkout.dto.ts @@ -0,0 +1,18 @@ +import { IsString, IsOptional } from 'class-validator'; + +export class CheckoutDto { + @IsString() + assetId: string; + + @IsOptional() + @IsString() + assignedToId?: string; + + @IsOptional() + @IsString() + dueDate?: string; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index dae01d6b..31e317a7 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -14,6 +14,10 @@ export class UsersService { return this.userRepository.findOne({ where: { email } }); } + async findById(id: string): Promise { + return this.userRepository.findOne({ where: { id } }); + } + async findByGoogleId(googleId: string): Promise { return this.userRepository.findOne({ where: { googleId } }); }