From cf73033f8761521608a7a7404b24f19c17959971 Mon Sep 17 00:00:00 2001 From: Olufunbi Date: Thu, 2 Oct 2025 21:33:52 +0100 Subject: [PATCH] feat:[BACKEND] Implement QR Code / Barcode Generator Module #296 --- backend/package-lock.json | 198 +++++++++++++++++- backend/package.json | 3 + backend/src/app.controller.ts | 10 - backend/src/app.module.ts | 10 +- backend/src/app.service.ts | 13 +- .../asset-transfers/asset-transfers.module.ts | 4 +- .../asset-transfers.service.ts | 8 +- backend/src/assets/assets.service.ts | 26 ++- backend/src/assets/entities/assest.entity.ts | 13 ++ .../dto/create-inventory-item.dto.ts | 0 .../entities/inventory-item.entity.ts | 0 .../inventory-items.controller.ts | 0 .../inventory-items/inventory-items.module.ts | 0 .../inventory-items.service.ts | 0 .../dto/create-movement.dto.ts | 0 .../entities/inventory-movement.entity.ts | 0 .../inventory-controller.controller.ts | 0 .../inventory-movements/inventory-module.ts | 0 .../inventory-movements.service.ts | 0 .../qr-barcode/dto/generate-qr-barcode.dto.ts | 16 ++ .../qr-barcode/dto/update-qr-barcode.dto.ts | 4 + .../qr-barcode/entities/qr-barcode.entity.ts | 1 + .../qr-barcode/qr-barcode.controller.spec.ts | 20 ++ .../src/qr-barcode/qr-barcode.controller.ts | 72 +++++++ backend/src/qr-barcode/qr-barcode.module.ts | 13 ++ .../src/qr-barcode/qr-barcode.service.spec.ts | 18 ++ backend/src/qr-barcode/qr-barcode.service.ts | 140 +++++++++++++ backend/src/search/search.module.ts | 8 +- backend/src/search/search.service.ts | 43 ++-- backend/src/users/entities/user.entity.ts | 37 ++-- 30 files changed, 578 insertions(+), 79 deletions(-) rename backend/{ => src}/inventory-items/dto/create-inventory-item.dto.ts (100%) rename backend/{ => src}/inventory-items/entities/inventory-item.entity.ts (100%) rename backend/{ => src}/inventory-items/inventory-items.controller.ts (100%) rename backend/{ => src}/inventory-items/inventory-items.module.ts (100%) rename backend/{ => src}/inventory-items/inventory-items.service.ts (100%) rename backend/{ => src}/inventory-movements/dto/create-movement.dto.ts (100%) rename backend/{ => src}/inventory-movements/entities/inventory-movement.entity.ts (100%) rename backend/{ => src}/inventory-movements/inventory-controller.controller.ts (100%) rename backend/{ => src}/inventory-movements/inventory-module.ts (100%) rename backend/{ => src}/inventory-movements/inventory-movements.service.ts (100%) create mode 100644 backend/src/qr-barcode/dto/generate-qr-barcode.dto.ts create mode 100644 backend/src/qr-barcode/dto/update-qr-barcode.dto.ts create mode 100644 backend/src/qr-barcode/entities/qr-barcode.entity.ts create mode 100644 backend/src/qr-barcode/qr-barcode.controller.spec.ts create mode 100644 backend/src/qr-barcode/qr-barcode.controller.ts create mode 100644 backend/src/qr-barcode/qr-barcode.module.ts create mode 100644 backend/src/qr-barcode/qr-barcode.service.spec.ts create mode 100644 backend/src/qr-barcode/qr-barcode.service.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 0b0a5a2..4d6a2ea 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@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", @@ -21,6 +22,7 @@ "@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", "json2csv": "^6.0.0-alpha.2", @@ -31,6 +33,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", @@ -1711,6 +1714,19 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz", + "integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/jwt": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", @@ -3455,6 +3471,15 @@ "node": ">=10.16.0" } }, + "node_modules/bwip-js": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-4.7.0.tgz", + "integrity": "sha512-b7oQcgbWUl8rpcZayQ32SQrBCNteiZFuLkimKKBRlPwIHCeUN2VNeUE3HCMYShe04Evxd+ucS9uUAOsvNKjQbA==", + "license": "MIT", + "bin": { + "bwip-js": "bin/bwip-js.js" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3520,7 +3545,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -3990,6 +4014,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", @@ -4115,6 +4148,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4576,6 +4615,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -7260,7 +7305,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -7358,7 +7402,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -7632,6 +7675,15 @@ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7792,6 +7844,127 @@ } ] }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -7929,6 +8102,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -8273,6 +8452,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -9780,6 +9965,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -9820,7 +10011,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", diff --git a/backend/package.json b/backend/package.json index ae3d2ca..5831764 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "@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", @@ -32,6 +33,7 @@ "@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", "json2csv": "^6.0.0-alpha.2", @@ -42,6 +44,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", diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts index f7b6196..265479a 100644 --- a/backend/src/app.controller.ts +++ b/backend/src/app.controller.ts @@ -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(); - } } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index fbe8ac1..368cc3a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -14,6 +14,11 @@ 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'; @Module({ imports: [ @@ -50,8 +55,9 @@ import { ReportingModule } from './reporting/reporting.module'; AuthModule, RiskModule, ReportingModule, + QrBarcodeModule, ], - controllers: [AppController, NotificationsController], - providers: [AppService, NotificationsService], + controllers: [AppController], + providers: [AppService], }) export class AppModule {} diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts index deb6341..7864ede 100644 --- a/backend/src/app.service.ts +++ b/backend/src/app.service.ts @@ -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 }), - ); - } } diff --git a/backend/src/asset-transfers/asset-transfers.module.ts b/backend/src/asset-transfers/asset-transfers.module.ts index 8ce4851..03a9675 100644 --- a/backend/src/asset-transfers/asset-transfers.module.ts +++ b/backend/src/asset-transfers/asset-transfers.module.ts @@ -3,7 +3,7 @@ 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])], @@ -11,5 +11,3 @@ import { InventoryItem } from '../../inventory-items/entities/inventory-item.ent providers: [AssetTransfersService], }) export class AssetTransfersModule {} - - diff --git a/backend/src/asset-transfers/asset-transfers.service.ts b/backend/src/asset-transfers/asset-transfers.service.ts index 13d1801..dd20a55 100644 --- a/backend/src/asset-transfers/asset-transfers.service.ts +++ b/backend/src/asset-transfers/asset-transfers.service.ts @@ -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 { @@ -15,7 +15,9 @@ export class AssetTransfersService { ) {} async initiateTransfer(dto: InitiateTransferDto): Promise { - 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`); } @@ -38,5 +40,3 @@ export class AssetTransfersService { return await this.transferRepository.save(transfer); } } - - diff --git a/backend/src/assets/assets.service.ts b/backend/src/assets/assets.service.ts index 7d37a77..f787620 100644 --- a/backend/src/assets/assets.service.ts +++ b/backend/src/assets/assets.service.ts @@ -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 { @@ -17,8 +17,8 @@ export class AssetsService { private supplierRepo: Repository, @InjectRepository(Department) private departmentRepo: Repository, - @InjectRepository(Category) - private categoryRepo: Repository, + @InjectRepository(AssetCategory) + private categoryRepo: Repository, ) {} async create(dto: CreateAssetDto): Promise { @@ -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'); } @@ -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); diff --git a/backend/src/assets/entities/assest.entity.ts b/backend/src/assets/entities/assest.entity.ts index a2a3e78..14535dd 100644 --- a/backend/src/assets/entities/assest.entity.ts +++ b/backend/src/assets/entities/assest.entity.ts @@ -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 } diff --git a/backend/inventory-items/dto/create-inventory-item.dto.ts b/backend/src/inventory-items/dto/create-inventory-item.dto.ts similarity index 100% rename from backend/inventory-items/dto/create-inventory-item.dto.ts rename to backend/src/inventory-items/dto/create-inventory-item.dto.ts diff --git a/backend/inventory-items/entities/inventory-item.entity.ts b/backend/src/inventory-items/entities/inventory-item.entity.ts similarity index 100% rename from backend/inventory-items/entities/inventory-item.entity.ts rename to backend/src/inventory-items/entities/inventory-item.entity.ts diff --git a/backend/inventory-items/inventory-items.controller.ts b/backend/src/inventory-items/inventory-items.controller.ts similarity index 100% rename from backend/inventory-items/inventory-items.controller.ts rename to backend/src/inventory-items/inventory-items.controller.ts diff --git a/backend/inventory-items/inventory-items.module.ts b/backend/src/inventory-items/inventory-items.module.ts similarity index 100% rename from backend/inventory-items/inventory-items.module.ts rename to backend/src/inventory-items/inventory-items.module.ts diff --git a/backend/inventory-items/inventory-items.service.ts b/backend/src/inventory-items/inventory-items.service.ts similarity index 100% rename from backend/inventory-items/inventory-items.service.ts rename to backend/src/inventory-items/inventory-items.service.ts diff --git a/backend/inventory-movements/dto/create-movement.dto.ts b/backend/src/inventory-movements/dto/create-movement.dto.ts similarity index 100% rename from backend/inventory-movements/dto/create-movement.dto.ts rename to backend/src/inventory-movements/dto/create-movement.dto.ts diff --git a/backend/inventory-movements/entities/inventory-movement.entity.ts b/backend/src/inventory-movements/entities/inventory-movement.entity.ts similarity index 100% rename from backend/inventory-movements/entities/inventory-movement.entity.ts rename to backend/src/inventory-movements/entities/inventory-movement.entity.ts diff --git a/backend/inventory-movements/inventory-controller.controller.ts b/backend/src/inventory-movements/inventory-controller.controller.ts similarity index 100% rename from backend/inventory-movements/inventory-controller.controller.ts rename to backend/src/inventory-movements/inventory-controller.controller.ts diff --git a/backend/inventory-movements/inventory-module.ts b/backend/src/inventory-movements/inventory-module.ts similarity index 100% rename from backend/inventory-movements/inventory-module.ts rename to backend/src/inventory-movements/inventory-module.ts diff --git a/backend/inventory-movements/inventory-movements.service.ts b/backend/src/inventory-movements/inventory-movements.service.ts similarity index 100% rename from backend/inventory-movements/inventory-movements.service.ts rename to backend/src/inventory-movements/inventory-movements.service.ts diff --git a/backend/src/qr-barcode/dto/generate-qr-barcode.dto.ts b/backend/src/qr-barcode/dto/generate-qr-barcode.dto.ts new file mode 100644 index 0000000..38e5c32 --- /dev/null +++ b/backend/src/qr-barcode/dto/generate-qr-barcode.dto.ts @@ -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; +} diff --git a/backend/src/qr-barcode/dto/update-qr-barcode.dto.ts b/backend/src/qr-barcode/dto/update-qr-barcode.dto.ts new file mode 100644 index 0000000..f78924b --- /dev/null +++ b/backend/src/qr-barcode/dto/update-qr-barcode.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateQrBarcodeDto } from './generate-qr-barcode.dto'; + +export class UpdateQrBarcodeDto extends PartialType(CreateQrBarcodeDto) {} diff --git a/backend/src/qr-barcode/entities/qr-barcode.entity.ts b/backend/src/qr-barcode/entities/qr-barcode.entity.ts new file mode 100644 index 0000000..00daeb2 --- /dev/null +++ b/backend/src/qr-barcode/entities/qr-barcode.entity.ts @@ -0,0 +1 @@ +export class QrBarcode {} diff --git a/backend/src/qr-barcode/qr-barcode.controller.spec.ts b/backend/src/qr-barcode/qr-barcode.controller.spec.ts new file mode 100644 index 0000000..80f5057 --- /dev/null +++ b/backend/src/qr-barcode/qr-barcode.controller.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/qr-barcode/qr-barcode.controller.ts b/backend/src/qr-barcode/qr-barcode.controller.ts new file mode 100644 index 0000000..3307700 --- /dev/null +++ b/backend/src/qr-barcode/qr-barcode.controller.ts @@ -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= + @Get('verify') + async verify(@Query('payload') payload: string) { + const result = await this.codeService.verifyPayload(payload); + return result; + } +} diff --git a/backend/src/qr-barcode/qr-barcode.module.ts b/backend/src/qr-barcode/qr-barcode.module.ts new file mode 100644 index 0000000..1e56b9f --- /dev/null +++ b/backend/src/qr-barcode/qr-barcode.module.ts @@ -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 {} diff --git a/backend/src/qr-barcode/qr-barcode.service.spec.ts b/backend/src/qr-barcode/qr-barcode.service.spec.ts new file mode 100644 index 0000000..15cdda0 --- /dev/null +++ b/backend/src/qr-barcode/qr-barcode.service.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/qr-barcode/qr-barcode.service.ts b/backend/src/qr-barcode/qr-barcode.service.ts new file mode 100644 index 0000000..5f51fcd --- /dev/null +++ b/backend/src/qr-barcode/qr-barcode.service.ts @@ -0,0 +1,140 @@ +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as QRCode from 'qrcode'; +import * as bwipjs from 'bwip-js'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Asset } from 'src/assets/entities/assest.entity'; + +@Injectable() +export class QrBarcodeService { + private readonly logger = new Logger(QrBarcodeService.name); + + constructor( + @InjectRepository(Asset) + private readonly assetRepo: Repository, + ) {} + + // ✅ Generates a QR code data URL + async generateQrDataUrl(text: string): Promise { + const dataUrl = await QRCode.toDataURL(text, { errorCorrectionLevel: 'M' }); + return dataUrl; // e.g., data:image/png;base64,... + } + + // ✅ Generates a barcode buffer (Code128) + async generateBarcodeBuffer(text: string): Promise { + return await bwipjs.toBuffer({ + bcid: 'code128', + text: String(text), + scale: 3, + height: 10, + includetext: true, + textxalign: 'center', + }); + } + + bufferToDataUrl(buffer: Buffer, mime = 'image/png') { + return `data:${mime};base64,${buffer.toString('base64')}`; + } + + async saveBufferToDisk(buffer: Buffer, filename: string): Promise { + const uploadDir = path.resolve(process.cwd(), 'uploads', 'codes'); + await fs.promises.mkdir(uploadDir, { recursive: true }); + const filepath = path.join(uploadDir, filename); + await fs.promises.writeFile(filepath, buffer); + return filepath; + } + + // ✅ Main generator for asset + async generateAndStoreForAsset( + assetId: string, + options: { + persist?: boolean; + saveToDisk?: boolean; + type?: 'qr' | 'barcode' | 'both'; + } = {}, + ) { + const asset = await this.assetRepo.findOne({ where: { id: assetId } }); + if (!asset) throw new NotFoundException('Asset not found'); + + const result: { + qr?: string; + barcode?: string; + qrFilename?: string; + barcodeFilename?: string; + } = {}; + + const { persist = true, saveToDisk = false, type = 'both' } = options; + + // Content encoded in the codes + const payload = JSON.stringify({ id: asset.id, name: asset.name }); + + if (type === 'qr' || type === 'both') { + const qrDataUrl = await this.generateQrDataUrl(payload); + result.qr = qrDataUrl; + + if (saveToDisk) { + const base64 = qrDataUrl.split(',')[1]; + const buffer = Buffer.from(base64, 'base64'); + const filename = `${asset.id}-qr.png`; + const filepath = await this.saveBufferToDisk(buffer, filename); + result.qrFilename = filepath; + asset.qrCodeFilename = filepath; + } + + if (persist) { + asset.qrCodeBase64 = qrDataUrl; + } + } + + if (type === 'barcode' || type === 'both') { + const barcodeBuffer = await this.generateBarcodeBuffer(asset.id); + const barcodeDataUrl = this.bufferToDataUrl(barcodeBuffer); + result.barcode = barcodeDataUrl; + + if (saveToDisk) { + const filename = `${asset.id}-barcode.png`; + const filepath = await this.saveBufferToDisk(barcodeBuffer, filename); + result.barcodeFilename = filepath; + asset.barcodeFilename = filepath; + } + + if (persist) { + asset.barcodeBase64 = barcodeDataUrl; + } + } + + if (persist) { + await this.assetRepo.save(asset); + } + + return result; + } + + // ✅ Retrieve stored codes + async getCodesForAsset(assetId: string) { + const asset = await this.assetRepo.findOne({ where: { id: assetId } }); + if (!asset) throw new NotFoundException('Asset not found'); + return { + qr: asset.qrCodeBase64 ?? null, + barcode: asset.barcodeBase64 ?? null, + qrFilename: asset.qrCodeFilename ?? null, + barcodeFilename: asset.barcodeFilename ?? null, + }; + } + + // ✅ Verify scanned payload + async verifyPayload(payloadString: string) { + try { + const payload = JSON.parse(payloadString); + if (!payload.id) return { valid: false }; + const asset = await this.assetRepo.findOne({ where: { id: payload.id } }); + if (!asset) return { valid: false }; + return { valid: true, asset }; + } catch (err) { + this.logger.debug('verifyPayload error: ' + (err?.message ?? err)); + return { valid: false }; + } + } +} diff --git a/backend/src/search/search.module.ts b/backend/src/search/search.module.ts index 33176d1..8e3fbad 100644 --- a/backend/src/search/search.module.ts +++ b/backend/src/search/search.module.ts @@ -2,15 +2,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SearchController } from './search.controller'; import { SearchService } from './search.service'; -import { Asset } from 'src/assets/entity/asset.entity'; import { InventoryItem } from 'src/inventory/entities/inventory-item.entity'; +import { Asset } from 'src/assets/entities/assest.entity'; @Module({ - imports: [ - TypeOrmModule.forFeature([Asset, InventoryItem]), - ], + imports: [TypeOrmModule.forFeature([Asset, InventoryItem])], controllers: [SearchController], providers: [SearchService], exports: [SearchService], }) -export class SearchModule {} \ No newline at end of file +export class SearchModule {} diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index fa47d93..df6d90b 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -4,8 +4,8 @@ import { Repository, SelectQueryBuilder } from 'typeorm'; import { SearchQueryDto, SearchEntity } from './dto/search-query.dto'; import { SearchResponseDto, SearchMetadata } from './dto/search-response.dto'; import { SearchResultDto } from './dto/search-result.dto'; -import { Asset } from 'src/assets/entity/asset.entity'; -import { InventoryItem } from 'src/inventory/entities/inventory-item.entity'; +import { InventoryItem } from 'src/inventory/entities/inventory-item.entity'; +import { Asset } from 'src/assets/entities/assest.entity'; @Injectable() export class SearchService { @@ -44,7 +44,10 @@ export class SearchService { total += assetResults.total; } - if (entityType === SearchEntity.ALL || entityType === SearchEntity.INVENTORY) { + if ( + entityType === SearchEntity.ALL || + entityType === SearchEntity.INVENTORY + ) { const inventoryResults = await this.searchInventory(searchQuery); results.push(...inventoryResults.data); total += inventoryResults.total; @@ -106,7 +109,8 @@ export class SearchService { private async searchInventory( searchQuery: SearchQueryDto, ): Promise<{ data: SearchResultDto[]; total: number }> { - const queryBuilder = this.inventoryRepository.createQueryBuilder('inventory'); + const queryBuilder = + this.inventoryRepository.createQueryBuilder('inventory'); // Apply filters this.applyFilters(queryBuilder, searchQuery, 'inventory'); @@ -141,9 +145,12 @@ export class SearchService { // Category filter if (category) { - queryBuilder.andWhere(`LOWER(${entityAlias}.category) = LOWER(:category)`, { - category, - }); + queryBuilder.andWhere( + `LOWER(${entityAlias}.category) = LOWER(:category)`, + { + category, + }, + ); } // Department filter @@ -156,16 +163,22 @@ export class SearchService { // Supplier filter if (supplier) { - queryBuilder.andWhere(`LOWER(${entityAlias}.supplier) = LOWER(:supplier)`, { - supplier, - }); + queryBuilder.andWhere( + `LOWER(${entityAlias}.supplier) = LOWER(:supplier)`, + { + supplier, + }, + ); } // Location filter if (location) { - queryBuilder.andWhere(`LOWER(${entityAlias}.location) = LOWER(:location)`, { - location, - }); + queryBuilder.andWhere( + `LOWER(${entityAlias}.location) = LOWER(:location)`, + { + location, + }, + ); } } @@ -245,7 +258,7 @@ export class SearchService { ]; // Repeat for other fields... - + return { categories, departments: [], // Implement similar logic @@ -253,4 +266,4 @@ export class SearchService { locations: [], // Implement similar logic }; } -} \ No newline at end of file +} diff --git a/backend/src/users/entities/user.entity.ts b/backend/src/users/entities/user.entity.ts index 973d4c3..b59183f 100644 --- a/backend/src/users/entities/user.entity.ts +++ b/backend/src/users/entities/user.entity.ts @@ -1,4 +1,11 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + UpdateDateColumn, + CreateDateColumn, +} from 'typeorm'; @Entity('users') export class User { @@ -17,20 +24,20 @@ export class User { @Column() passwordHash: string; - @Column({ type: 'enum', enum: ['admin', 'user', 'manager'], default: 'user' }) - role: 'admin' | 'user' | 'manager'; - // Department relation temporarily commented out due to import error - // @ManyToOne(() => Department, { nullable: true }) - // department?: Department; - @Column({ nullable: true }) - companyId?: number; - @Column({ nullable: true }) - branchId?: number; - - @CreateDateColumn() - createdAt: Date; - @UpdateDateColumn() - updatedAt: Date; + @Column({ type: 'enum', enum: ['admin', 'user', 'manager'], default: 'user' }) + role: 'admin' | 'user' | 'manager'; + // Department relation temporarily commented out due to import error + // @ManyToOne(() => Department, { nullable: true }) + // department?: Department; + @Column({ nullable: true }) + companyId?: number; + @Column({ nullable: true }) + branchId?: number; + + @CreateDateColumn() + createdAt: Date; + @UpdateDateColumn() + updatedAt: Date; // Relations to company, department, branch (to be defined) // @ManyToOne(() => Company, company => company.users)