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
5,924 changes: 4,469 additions & 1,455 deletions backend/package-lock.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.975.0",
"@nestjs/bull": "^11.0.4",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
Expand All @@ -38,13 +40,17 @@
"@types/uuid": "^10.0.0",
"axios": "^1.6.0",
"bcryptjs": "^3.0.3",
"bull": "^4.16.5",
"bwip-js": "^4.7.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"date-fns": "^4.1.0",
"exceljs": "^4.4.0",
"json2csv": "^6.0.0-alpha.2",
"multer": "^2.0.2",
"nestjs-i18n": "^10.5.1",
"node-cron": "^4.2.1",
"nodemailer": "^7.0.12",
"otplib": "^13.1.1",
"papaparse": "^5.5.3",
"passport": "^0.7.0",
Expand All @@ -66,11 +72,14 @@
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcryptjs": "^3.0.0",
"@types/bull": "^3.15.9",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/json2csv": "^5.0.7",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20.3.1",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^7.0.5",
"@types/otplib": "^7.0.0",
"@types/papaparse": "^5.3.16",
"@types/passport-jwt": "^4.0.1",
Expand Down
72 changes: 25 additions & 47 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
Expand All @@ -7,19 +8,6 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './users/user.module';
import { AuthModule } from './auth/auth.module';
// import { ApiKeysModule } from "./api-keys/api-keys.module";
// import { OrganizationUnitsModule } from "./organization-units/organization-units.module";
// import { ChangeLogModule } from "./change-log/change-log.module";
// import { BarcodeModule } from "./barcode/barcode.module";
// import { ComplianceModule } from "./compliance/compliance.module";
// import { MobileDevicesModule } from "./mobile-devices/mobile-devices.module";
// import { PolicyDocumentsModule } from "./policy-documents/policy-documents.module";
// import { DeviceHealthModule } from "./device-health/device-health.module";
// import { QRCodeModule } from "./QR-Code/qrcode.module";
// import { NotificationsModule } from "./notifications/notifications.module";
// import { StatusHistoryModule } from "./status-history/status-history.module";
// import { DisposalRegistryModule } from "./disposal-registry/disposal-registry.module";
// import { VendorDirectoryModule } from "./vendor-directory/vendor-directory.module";
import { WebhooksModule } from './webhooks/webhooks.module';
import { AuditLogsModule } from './audit-logs/audit-logs.module';
import { AuditLoggingInterceptor } from './audit-logs/audit-logging.interceptor';
Expand All @@ -29,17 +17,23 @@ import { Department } from './departments/entities/department.entity';
import { User } from './users/entities/user.entity';
import { FileUpload } from './file-uploads/entities/file-upload.entity';
import { Asset } from './assets/entities/asset.entity';
// import { Supplier } from './suppliers/entities/supplier.entity';
import { Supplier } from './suppliers/entities/supplier.entity';
import { AssetCategoriesModule } from './asset-categories/asset-categories.module';
// import { DepartmentsModule } from './departments/departments.module';
// import { AssetTransfersModule } from './asset-transfers/asset-transfers.module';
// import { SearchModule } from './search/search.module';
// import { ApiKeyModule } from './api-key/api-key.module';
// import { NestModule } from './scheduled-jobs/nest/nest.module';
// import { ScheduledJobsModule } from './scheduled-jobs/scheduled-jobs.module';
import { AssetsModule } from './assets/assets.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { ReportsModule } from './reports/reports.module';

// Import Report entities
import { Report } from './reports/entities/report.entity';
import { ScheduledReport } from './reports/entities/scheduled-report.entity';
import { ReportExecution } from './reports/entities/report-execution.entity';

// Import Document entities (referenced in your original app.module)
// Make sure these exist or remove if not needed
// import { Document } from './documents/entities/document.entity';
// import { DocumentVersion } from './documents/entities/document-version.entity';
// import { DocumentAccessPermission } from './documents/entities/document-access-permission.entity';
// import { DocumentAuditLog } from './documents/entities/document-audit-log.entity';

@Module({
imports: [
Expand All @@ -66,44 +60,28 @@ import { AnalyticsModule } from './analytics/analytics.module';
User,
FileUpload,
Asset,
// Supplier,
Supplier,
Document,
DocumentVersion,
DocumentAccessPermission,
DocumentAuditLog,
Report,
ScheduledReport,
ReportExecution,
// Document,
// DocumentVersion,
// DocumentAccessPermission,
// DocumentAuditLog,
],
synchronize: configService.get('NODE_ENV') !== 'production', // Only for development
synchronize: configService.get('NODE_ENV') !== 'production',
}),
inject: [ConfigService],
}),

AssetCategoriesModule,
// DepartmentsModule,
// AssetTransfersModule,
UserModule,
// SearchModule,
AuthModule,
// ApiKeysModule,
// OrganizationUnitsModule,
// ChangeLogModule,
// BarcodeModule,
// ComplianceModule,
// MobileDevicesModule,
// PolicyDocumentsModule,
// DeviceHealthModule,
// QRCodeModule,
// NotificationsModule,
// StatusHistoryModule,
// DisposalRegistryModule,
// VendorDirectoryModule,
WebhooksModule,
AuditLogsModule,
// ApiKeyModule,
// NestModule,
// ScheduledJobsModule,
AssetsModule,
AnalyticsModule
AnalyticsModule,
ReportsModule, // Add the Reports Module
],
controllers: [AppController],
providers: [
Expand All @@ -114,4 +92,4 @@ import { AnalyticsModule } from './analytics/analytics.module';
AppService,
],
})
export class AppModule {}
export class AppModule {}
116 changes: 116 additions & 0 deletions backend/src/reports/controllers/report-executions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
Controller,
Get,
Delete,
Param,
UseGuards,
Request,
Res,
StreamableFile,
NotFoundException,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { ReportExecution } from '../entities/report-execution.entity';
import { FileStorageService } from '../services/file-storage.service';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';

@ApiTags('Report Executions')
@ApiBearerAuth()
@Controller('api/v1/report-executions')
@UseGuards(JwtAuthGuard)
export class ReportExecutionsController {
constructor(
@InjectRepository(ReportExecution)
private executionRepository: Repository<ReportExecution>,
private fileStorageService: FileStorageService,
) {}

@Get()
@ApiOperation({ summary: 'List executions' })
async findAll(@Request() req) {
return this.executionRepository.find({
where: { executedBy: { id: req.user.id } },
relations: ['report', 'executedBy'],
order: { startedAt: 'DESC' },
take: 50,
});
}

@Get(':id')
@ApiOperation({ summary: 'Get execution details' })
async findOne(@Param('id') id: string, @Request() req) {
const execution = await this.executionRepository.findOne({
where: { id },
relations: ['report', 'executedBy'],
});

if (!execution) {
throw new NotFoundException(`Execution with ID ${id} not found`);
}

if (execution.executedBy.id !== req.user.id) {
throw new NotFoundException('Access denied');
}

return execution;
}

@Get(':id/download')
@ApiOperation({ summary: 'Download generated file' })
async download(
@Param('id') id: string,
@Request() req,
@Res({ passthrough: true }) res: Response,
) {
const execution = await this.findOne(id, req);

if (!execution.fileUrl || execution.status !== 'COMPLETED') {
throw new NotFoundException('Report file not available');
}

// Extract filename from URL
const filename = execution.fileUrl.split('/').pop();
const buffer = await this.fileStorageService.getFile(filename);

// Set appropriate headers
const mimeTypes = {
PDF: 'application/pdf',
EXCEL: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
CSV: 'text/csv',
JSON: 'application/json',
};

const extensions = {
PDF: 'pdf',
EXCEL: 'xlsx',
CSV: 'csv',
JSON: 'json',
};

res.set({
'Content-Type': mimeTypes[execution.format],
'Content-Disposition': `attachment; filename="${execution.report.name}.${extensions[execution.format]}"`,
});

return new StreamableFile(buffer);
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Cancel running execution' })
async cancel(@Param('id') id: string, @Request() req) {
const execution = await this.findOne(id, req);

if (execution.status === 'RUNNING') {
execution.status = 'FAILED';
execution.error = 'Cancelled by user';
execution.completedAt = new Date();
await this.executionRepository.save(execution);
}
}
}
102 changes: 102 additions & 0 deletions backend/src/reports/controllers/reports.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// src/reports/controllers/reports.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
Request,
HttpCode,
HttpStatus,
Res,
StreamableFile,
} from '@nestjs/common';
import { Response } from 'express';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { ReportsService } from '../services/reports.service';
import { CreateReportDto, UpdateReportDto, ExecuteReportDto, ShareReportDto } from '../dto/create-report.dto';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';

@ApiTags('Reports')
@ApiBearerAuth()
@Controller('api/v1/reports')
@UseGuards(JwtAuthGuard)
export class ReportsController {
constructor(private readonly reportsService: ReportsService) {}

@Post()
@ApiOperation({ summary: 'Create custom report' })
async create(@Body() createReportDto: CreateReportDto, @Request() req) {
return this.reportsService.create(createReportDto, req.user);
}

@Get()
@ApiOperation({ summary: 'List all reports (user\'s + public)' })
async findAll(@Request() req) {
return this.reportsService.findAll(req.user);
}

@Get('templates')
@ApiOperation({ summary: 'List predefined report templates' })
async getTemplates() {
return this.reportsService.getTemplates();
}

@Get(':id')
@ApiOperation({ summary: 'Get report configuration' })
async findOne(@Param('id') id: string, @Request() req) {
return this.reportsService.findOne(id, req.user);
}

@Put(':id')
@ApiOperation({ summary: 'Update report' })
async update(
@Param('id') id: string,
@Body() updateReportDto: UpdateReportDto,
@Request() req,
) {
return this.reportsService.update(id, updateReportDto, req.user);
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete report' })
async remove(@Param('id') id: string, @Request() req) {
await this.reportsService.remove(id, req.user);
}

@Post(':id/execute')
@ApiOperation({ summary: 'Execute report' })
async execute(
@Param('id') id: string,
@Body() executeReportDto: ExecuteReportDto,
@Request() req,
) {
return this.reportsService.execute(
id,
executeReportDto.format,
executeReportDto.parameters || {},
req.user,
);
}

@Get(':id/preview')
@ApiOperation({ summary: 'Preview report (first 100 rows)' })
async preview(@Param('id') id: string, @Request() req) {
return this.reportsService.preview(id, req.user);
}

@Post(':id/share')
@ApiOperation({ summary: 'Share report with users' })
async share(
@Param('id') id: string,
@Body() shareReportDto: ShareReportDto,
@Request() req,
) {
return this.reportsService.share(id, shareReportDto.userIds, req.user);
}
}
Loading
Loading