Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 { AssetsModule } from './assets/assets.module';
import { QueueModule } from './queue/queue.module';
import { StorageModule } from './storage/storage.module';
Expand Down Expand Up @@ -62,6 +64,8 @@ import { CacheService } from './cache/cache.service';
},
}),

AssetsOpsModule,
CheckinModule,
AssetsModule,
QueueModule,
StorageModule,
Expand Down
65 changes: 65 additions & 0 deletions backend/src/assets/assets-ops.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
14 changes: 14 additions & 0 deletions backend/src/assets/assets-ops.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
111 changes: 111 additions & 0 deletions backend/src/assets/assets-ops.service.ts
Original file line number Diff line number Diff line change
@@ -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<Asset>,
@InjectRepository(AssetNote)
private readonly noteRepository: Repository<AssetNote>,
) {}

async createNote(assetId: string, dto: CreateNoteDto, userId?: string): Promise<AssetNote> {
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<AssetNote[]> {
return this.noteRepository.find({
where: { assetId },
relations: ['createdBy'],
order: { createdAt: 'DESC' },
});
}

async deleteNote(assetId: string, noteId: string): Promise<void> {
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<string> {
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<string> {
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
if (!asset) throw new NotFoundException('Asset not found');

const barcode = await new Promise<string>((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<number> {
const result = await this.assetRepository.update(
{ id: In(dto.ids) },
{ status: dto.status, updatedById: userId },
);
return result.affected || 0;
}

async bulkDelete(dto: BulkDeleteDto): Promise<number> {
const result = await this.assetRepository.softDelete({ id: In(dto.ids) });
return result.affected || 0;
}

async bulkTransfer(dto: BulkTransferDto, userId?: string): Promise<number> {
const updateData: Partial<Asset> = { 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'] });
}
}
6 changes: 6 additions & 0 deletions backend/src/assets/dtos/bulk-delete.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsArray } from 'class-validator';

export class BulkDeleteDto {
@IsArray()
ids: string[];
}
9 changes: 9 additions & 0 deletions backend/src/assets/dtos/bulk-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsArray, IsString } from 'class-validator';

export class BulkStatusDto {
@IsArray()
ids: string[];

@IsString()
status: string;
}
18 changes: 18 additions & 0 deletions backend/src/assets/dtos/bulk-transfer.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 6 additions & 0 deletions backend/src/assets/dtos/create-note.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';

export class CreateNoteDto {
@IsString()
content: string;
}
32 changes: 32 additions & 0 deletions backend/src/assets/entities/asset-note.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
67 changes: 67 additions & 0 deletions backend/src/assets/entities/asset.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
31 changes: 31 additions & 0 deletions backend/src/checkin/checkin.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading