Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
251 changes: 247 additions & 4 deletions backend/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^7.3.0",
"@nestjs/typeorm": "^10.0.2",
"@types/multer": "^2.0.0",
"@types/uuid": "^10.0.0",
"bcryptjs": "^3.0.2",
"bwip-js": "^4.7.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"date-fns": "^4.1.0",
"json2csv": "^6.0.0-alpha.2",
"multer": "^2.0.2",
"nestjs-i18n": "^10.5.1",
Expand All @@ -42,6 +46,7 @@
"passport-jwt": "^4.0.1",
"pdfkit": "^0.17.2",
"pg": "^8.11.3",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
Expand Down
10 changes: 0 additions & 10 deletions backend/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,4 @@ export class AppController {
Timestamp: Date.now(),
};
}

@Get('hello')
getHello() {
return this.appService.getHello();
}

@Get('not-found')
getNotFound() {
return this.appService.getNotFoundError();
}
}
12 changes: 10 additions & 2 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { SearchModule } from './search/search.module';
import { AuthModule } from './auth/auth.module';
import { RiskModule } from './risk/risk.module';
import { ReportingModule } from './reporting/reporting.module';
import { AssetTransfersModule } from './asset-transfers/asset-transfers.module';
import { FileUpload } from './file-uploads/entities/file-upload.entity';
import { Asset } from './assets/entities/assest.entity';
import { Supplier } from './suppliers/entities/supplier.entity';
import { QrBarcodeModule } from './qr-barcode/qr-barcode.module';
import { VendorContractsModule } from './vendor-contracts/vendor-contracts.module';

@Module({
imports: [
Expand Down Expand Up @@ -50,8 +56,10 @@ import { ReportingModule } from './reporting/reporting.module';
AuthModule,
RiskModule,
ReportingModule,
QrBarcodeModule,
VendorContractsModule,
],
controllers: [AppController, NotificationsController],
providers: [AppService, NotificationsService],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
13 changes: 1 addition & 12 deletions backend/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
getHello(): string {
// The language is automatically detected from the request context
return this.i18n.t('translation.GREETING', { lang: I18nContext.current().lang });
}

getNotFoundError(): string {
// This will throw an exception with a translated message
throw new NotFoundException(
this.i18n.t('translation.ERROR.NOT_FOUND', { lang: I18nContext.current().lang }),
);
}
}
4 changes: 1 addition & 3 deletions backend/src/asset-transfers/asset-transfers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetTransfersService } from './asset-transfers.service';
import { AssetTransfersController } from './asset-transfers.controller';
import { AssetTransfer } from './entities/asset-transfer.entity';
import { InventoryItem } from '../../inventory-items/entities/inventory-item.entity';
import { InventoryItem } from 'src/inventory/entities/inventory-item.entity';

@Module({
imports: [TypeOrmModule.forFeature([AssetTransfer, InventoryItem])],
controllers: [AssetTransfersController],
providers: [AssetTransfersService],
})
export class AssetTransfersModule {}


8 changes: 4 additions & 4 deletions backend/src/asset-transfers/asset-transfers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AssetTransfer } from './entities/asset-transfer.entity';
import { InitiateTransferDto } from './dto/initiate-transfer.dto';
import { InventoryItem } from '../../inventory-items/entities/inventory-item.entity';
import { InventoryItem } from 'src/inventory/entities/inventory-item.entity';

@Injectable()
export class AssetTransfersService {
Expand All @@ -15,7 +15,9 @@ export class AssetTransfersService {
) {}

async initiateTransfer(dto: InitiateTransferDto): Promise<AssetTransfer> {
const asset = await this.inventoryRepository.findOne({ where: { id: dto.assetId } });
const asset = await this.inventoryRepository.findOne({
where: { id: dto.assetId },
});
if (!asset) {
throw new NotFoundException(`Asset ${dto.assetId} not found`);
}
Expand All @@ -38,5 +40,3 @@ export class AssetTransfersService {
return await this.transferRepository.save(transfer);
}
}


26 changes: 17 additions & 9 deletions backend/src/assets/assets.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Asset } from './entities/asset.entity';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { Supplier } from '../suppliers/entities/supplier.entity';
import { Department } from '../departments/entities/department.entity';
import { Category } from '../categories/entities/category.entity';
import { Asset } from './entities/assest.entity';
import { Department } from 'src/departments/department.entity';
import { AssetCategory } from 'src/asset-categories/asset-category.entity';

@Injectable()
export class AssetsService {
Expand All @@ -17,8 +17,8 @@ export class AssetsService {
private supplierRepo: Repository<Supplier>,
@InjectRepository(Department)
private departmentRepo: Repository<Department>,
@InjectRepository(Category)
private categoryRepo: Repository<Category>,
@InjectRepository(AssetCategory)
private categoryRepo: Repository<AssetCategory>,
) {}

async create(dto: CreateAssetDto): Promise<Asset> {
Expand All @@ -30,7 +30,9 @@ export class AssetsService {

let department: Department = null;
if (dto.assignedDepartmentId) {
department = await this.departmentRepo.findOneBy({ id: dto.assignedDepartmentId });
department = await this.departmentRepo.findOneBy({
id: dto.assignedDepartmentId,
});
if (!department) throw new NotFoundException('Department not found');
}

Expand Down Expand Up @@ -65,13 +67,19 @@ export class AssetsService {
const asset = await this.findOne(id);

if (dto.supplierId) {
asset.supplier = await this.supplierRepo.findOneBy({ id: dto.supplierId });
asset.supplier = await this.supplierRepo.findOneBy({
id: dto.supplierId,
});
}
if (dto.categoryId) {
asset.category = await this.categoryRepo.findOneBy({ id: dto.categoryId });
asset.category = await this.categoryRepo.findOneBy({
id: dto.categoryId,
});
}
if (dto.assignedDepartmentId) {
asset.assignedDepartment = await this.departmentRepo.findOneBy({ id: dto.assignedDepartmentId });
asset.assignedDepartment = await this.departmentRepo.findOneBy({
id: dto.assignedDepartmentId,
});
}

Object.assign(asset, dto);
Expand Down
13 changes: 13 additions & 0 deletions backend/src/assets/entities/assest.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,17 @@ export class Asset {

@UpdateDateColumn()
updatedAt: Date;

// New columns for QR/Barcode
@Column({ type: 'text', nullable: true })
qrCodeBase64?: string | null; // data:image/png;base64,...

@Column({ type: 'text', nullable: true })
barcodeBase64?: string | null; // data:image/png;base64,...

@Column({ type: 'varchar', length: 255, nullable: true })
qrCodeFilename?: string | null; // optional file path if you save to disk

@Column({ type: 'varchar', length: 255, nullable: true })
barcodeFilename?: string | null; // optional file path if you save to disk
}
18 changes: 18 additions & 0 deletions backend/src/notifications/console-notification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable, Logger } from '@nestjs/common';
import {
ContractExpiryPayload,
NotificationService,
} from './interfaces/notification.interface';

@Injectable()
export class ConsoleNotificationService implements NotificationService {
private readonly logger = new Logger(ConsoleNotificationService.name);

async notifyContractExpiring(payload: ContractExpiryPayload) {
// Replace with real email/push logic
this.logger.warn(
`Contract expiring soon: ${payload.contractName} (id=${payload.contractId}) for supplier ${payload.supplierId}. Expires in ${payload.daysUntilExpiry} days on ${payload.endDate}`,
);
// e.g., call mailerService.sendMail(...) or push to queue
}
}
Empty file.
16 changes: 16 additions & 0 deletions backend/src/qr-barcode/dto/generate-qr-barcode.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IsIn, IsOptional } from 'class-validator';

export class GenerateCodeDto {
// what to generate: 'qr', 'barcode' or 'both'
@IsOptional()
@IsIn(['qr', 'barcode', 'both'])
type?: 'qr' | 'barcode' | 'both' = 'both';

// whether to persist base64 into DB (default true)
@IsOptional()
persist?: boolean = true;

// whether to save PNG files to disk
@IsOptional()
saveToDisk?: boolean = false;
}
Empty file.
1 change: 1 addition & 0 deletions backend/src/qr-barcode/entities/qr-barcode.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class QrBarcode {}
20 changes: 20 additions & 0 deletions backend/src/qr-barcode/qr-barcode.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { QrBarcodeController } from './qr-barcode.controller';
import { QrBarcodeService } from './qr-barcode.service';

describe('QrBarcodeController', () => {
let controller: QrBarcodeController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [QrBarcodeController],
providers: [QrBarcodeService],
}).compile();

controller = module.get<QrBarcodeController>(QrBarcodeController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
72 changes: 72 additions & 0 deletions backend/src/qr-barcode/qr-barcode.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
Controller,
Post,
Param,
Get,
Query,
Body,
Res,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { GenerateCodeDto } from './dto/generate-qr-barcode.dto';
import { QrBarcodeService } from './qr-barcode.service';

@Controller('assets')
export class QrBarcodeController {
constructor(private readonly codeService: QrBarcodeService) {}

// POST /assets/:id/generate-code
@Post(':id/generate-code')
async generateCode(@Param('id') id: string, @Body() body: GenerateCodeDto) {
const result = await this.codeService.generateAndStoreForAsset(id, {
persist: body.persist,
saveToDisk: body.saveToDisk,
type: body.type,
});
return { success: true, data: result };
}

// GET /assets/:id/codes returns stored base64 strings
@Get(':id/codes')
async getCodes(@Param('id') id: string) {
const data = await this.codeService.getCodesForAsset(id);
return { success: true, data };
}

// GET /assets/:id/code/qr -> returns PNG directly
@Get(':id/code/qr')
async getQrImage(@Param('id') id: string, @Res() res: Response) {
const { qr } = await this.codeService.getCodesForAsset(id);
if (!qr)
return res
.status(HttpStatus.NOT_FOUND)
.json({ success: false, message: 'QR not found' });
// qr is a data URL -> convert to buffer
const base64 = qr.split(',')[1];
const buffer = Buffer.from(base64, 'base64');
res.setHeader('Content-Type', 'image/png');
res.send(buffer);
}

// GET /assets/:id/code/barcode -> returns PNG directly
@Get(':id/code/barcode')
async getBarcodeImage(@Param('id') id: string, @Res() res: Response) {
const { barcode } = await this.codeService.getCodesForAsset(id);
if (!barcode)
return res
.status(HttpStatus.NOT_FOUND)
.json({ success: false, message: 'Barcode not found' });
const base64 = barcode.split(',')[1];
const buffer = Buffer.from(base64, 'base64');
res.setHeader('Content-Type', 'image/png');
res.send(buffer);
}

// GET /assets/verify?payload=<payload-string>
@Get('verify')
async verify(@Query('payload') payload: string) {
const result = await this.codeService.verifyPayload(payload);
return result;
}
}
13 changes: 13 additions & 0 deletions backend/src/qr-barcode/qr-barcode.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Asset } from 'src/assets/entities/assest.entity';
import { QrBarcodeController } from './qr-barcode.controller';
import { QrBarcodeService } from './qr-barcode.service';

@Module({
imports: [TypeOrmModule.forFeature([Asset])],
providers: [QrBarcodeService],
controllers: [QrBarcodeController],
exports: [QrBarcodeService],
})
export class QrBarcodeModule {}
18 changes: 18 additions & 0 deletions backend/src/qr-barcode/qr-barcode.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { QrBarcodeService } from './qr-barcode.service';

describe('QrBarcodeService', () => {
let service: QrBarcodeService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [QrBarcodeService],
}).compile();

service = module.get<QrBarcodeService>(QrBarcodeService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Loading