Skip to content

Commit c2e6797

Browse files
committed
feat: implement comprehensive report generation and export system
1 parent f8e499a commit c2e6797

20 files changed

+6538
-1502
lines changed

backend/package-lock.json

Lines changed: 4469 additions & 1455 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"test:e2e": "jest --config ./test/jest-e2e.json"
2121
},
2222
"dependencies": {
23+
"@aws-sdk/client-s3": "^3.975.0",
24+
"@nestjs/bull": "^11.0.4",
2325
"@nestjs/common": "^10.0.0",
2426
"@nestjs/config": "^3.3.0",
2527
"@nestjs/core": "^10.0.0",
@@ -38,13 +40,17 @@
3840
"@types/uuid": "^10.0.0",
3941
"axios": "^1.6.0",
4042
"bcryptjs": "^3.0.3",
43+
"bull": "^4.16.5",
4144
"bwip-js": "^4.7.0",
4245
"class-transformer": "^0.5.1",
4346
"class-validator": "^0.14.3",
4447
"date-fns": "^4.1.0",
48+
"exceljs": "^4.4.0",
4549
"json2csv": "^6.0.0-alpha.2",
4650
"multer": "^2.0.2",
4751
"nestjs-i18n": "^10.5.1",
52+
"node-cron": "^4.2.1",
53+
"nodemailer": "^7.0.12",
4854
"otplib": "^13.1.1",
4955
"papaparse": "^5.5.3",
5056
"passport": "^0.7.0",
@@ -66,11 +72,14 @@
6672
"@nestjs/schematics": "^10.0.0",
6773
"@nestjs/testing": "^10.0.0",
6874
"@types/bcryptjs": "^3.0.0",
75+
"@types/bull": "^3.15.9",
6976
"@types/express": "^5.0.0",
7077
"@types/jest": "^29.5.2",
7178
"@types/json2csv": "^5.0.7",
7279
"@types/jsonwebtoken": "^9.0.10",
7380
"@types/node": "^20.3.1",
81+
"@types/node-cron": "^3.0.11",
82+
"@types/nodemailer": "^7.0.5",
7483
"@types/otplib": "^7.0.0",
7584
"@types/papaparse": "^5.3.16",
7685
"@types/passport-jwt": "^4.0.1",

backend/src/app.module.ts

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// src/app.module.ts
12
import { Module } from '@nestjs/common';
23
import { ConfigModule, ConfigService } from '@nestjs/config';
34
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -7,19 +8,6 @@ import { AppController } from './app.controller';
78
import { AppService } from './app.service';
89
import { UserModule } from './users/user.module';
910
import { AuthModule } from './auth/auth.module';
10-
// import { ApiKeysModule } from "./api-keys/api-keys.module";
11-
// import { OrganizationUnitsModule } from "./organization-units/organization-units.module";
12-
// import { ChangeLogModule } from "./change-log/change-log.module";
13-
// import { BarcodeModule } from "./barcode/barcode.module";
14-
// import { ComplianceModule } from "./compliance/compliance.module";
15-
// import { MobileDevicesModule } from "./mobile-devices/mobile-devices.module";
16-
// import { PolicyDocumentsModule } from "./policy-documents/policy-documents.module";
17-
// import { DeviceHealthModule } from "./device-health/device-health.module";
18-
// import { QRCodeModule } from "./QR-Code/qrcode.module";
19-
// import { NotificationsModule } from "./notifications/notifications.module";
20-
// import { StatusHistoryModule } from "./status-history/status-history.module";
21-
// import { DisposalRegistryModule } from "./disposal-registry/disposal-registry.module";
22-
// import { VendorDirectoryModule } from "./vendor-directory/vendor-directory.module";
2311
import { WebhooksModule } from './webhooks/webhooks.module';
2412
import { AuditLogsModule } from './audit-logs/audit-logs.module';
2513
import { AuditLoggingInterceptor } from './audit-logs/audit-logging.interceptor';
@@ -29,17 +17,23 @@ import { Department } from './departments/entities/department.entity';
2917
import { User } from './users/entities/user.entity';
3018
import { FileUpload } from './file-uploads/entities/file-upload.entity';
3119
import { Asset } from './assets/entities/asset.entity';
32-
// import { Supplier } from './suppliers/entities/supplier.entity';
3320
import { Supplier } from './suppliers/entities/supplier.entity';
3421
import { AssetCategoriesModule } from './asset-categories/asset-categories.module';
35-
// import { DepartmentsModule } from './departments/departments.module';
36-
// import { AssetTransfersModule } from './asset-transfers/asset-transfers.module';
37-
// import { SearchModule } from './search/search.module';
38-
// import { ApiKeyModule } from './api-key/api-key.module';
39-
// import { NestModule } from './scheduled-jobs/nest/nest.module';
40-
// import { ScheduledJobsModule } from './scheduled-jobs/scheduled-jobs.module';
4122
import { AssetsModule } from './assets/assets.module';
4223
import { AnalyticsModule } from './analytics/analytics.module';
24+
import { ReportsModule } from './reports/reports.module';
25+
26+
// Import Report entities
27+
import { Report } from './reports/entities/report.entity';
28+
import { ScheduledReport } from './reports/entities/scheduled-report.entity';
29+
import { ReportExecution } from './reports/entities/report-execution.entity';
30+
31+
// Import Document entities (referenced in your original app.module)
32+
// Make sure these exist or remove if not needed
33+
// import { Document } from './documents/entities/document.entity';
34+
// import { DocumentVersion } from './documents/entities/document-version.entity';
35+
// import { DocumentAccessPermission } from './documents/entities/document-access-permission.entity';
36+
// import { DocumentAuditLog } from './documents/entities/document-audit-log.entity';
4337

4438
@Module({
4539
imports: [
@@ -66,44 +60,28 @@ import { AnalyticsModule } from './analytics/analytics.module';
6660
User,
6761
FileUpload,
6862
Asset,
69-
// Supplier,
7063
Supplier,
71-
Document,
72-
DocumentVersion,
73-
DocumentAccessPermission,
74-
DocumentAuditLog,
64+
Report,
65+
ScheduledReport,
66+
ReportExecution,
67+
// Document,
68+
// DocumentVersion,
69+
// DocumentAccessPermission,
70+
// DocumentAuditLog,
7571
],
76-
synchronize: configService.get('NODE_ENV') !== 'production', // Only for development
72+
synchronize: configService.get('NODE_ENV') !== 'production',
7773
}),
7874
inject: [ConfigService],
7975
}),
8076

8177
AssetCategoriesModule,
82-
// DepartmentsModule,
83-
// AssetTransfersModule,
8478
UserModule,
85-
// SearchModule,
8679
AuthModule,
87-
// ApiKeysModule,
88-
// OrganizationUnitsModule,
89-
// ChangeLogModule,
90-
// BarcodeModule,
91-
// ComplianceModule,
92-
// MobileDevicesModule,
93-
// PolicyDocumentsModule,
94-
// DeviceHealthModule,
95-
// QRCodeModule,
96-
// NotificationsModule,
97-
// StatusHistoryModule,
98-
// DisposalRegistryModule,
99-
// VendorDirectoryModule,
10080
WebhooksModule,
10181
AuditLogsModule,
102-
// ApiKeyModule,
103-
// NestModule,
104-
// ScheduledJobsModule,
10582
AssetsModule,
106-
AnalyticsModule
83+
AnalyticsModule,
84+
ReportsModule, // Add the Reports Module
10785
],
10886
controllers: [AppController],
10987
providers: [
@@ -114,4 +92,4 @@ import { AnalyticsModule } from './analytics/analytics.module';
11492
AppService,
11593
],
11694
})
117-
export class AppModule {}
95+
export class AppModule {}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
Controller,
3+
Get,
4+
Delete,
5+
Param,
6+
UseGuards,
7+
Request,
8+
Res,
9+
StreamableFile,
10+
NotFoundException,
11+
HttpCode,
12+
HttpStatus,
13+
} from '@nestjs/common';
14+
import { Response } from 'express';
15+
import { InjectRepository } from '@nestjs/typeorm';
16+
import { Repository } from 'typeorm';
17+
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
18+
import { ReportExecution } from '../entities/report-execution.entity';
19+
import { FileStorageService } from '../services/file-storage.service';
20+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
21+
22+
@ApiTags('Report Executions')
23+
@ApiBearerAuth()
24+
@Controller('api/v1/report-executions')
25+
@UseGuards(JwtAuthGuard)
26+
export class ReportExecutionsController {
27+
constructor(
28+
@InjectRepository(ReportExecution)
29+
private executionRepository: Repository<ReportExecution>,
30+
private fileStorageService: FileStorageService,
31+
) {}
32+
33+
@Get()
34+
@ApiOperation({ summary: 'List executions' })
35+
async findAll(@Request() req) {
36+
return this.executionRepository.find({
37+
where: { executedBy: { id: req.user.id } },
38+
relations: ['report', 'executedBy'],
39+
order: { startedAt: 'DESC' },
40+
take: 50,
41+
});
42+
}
43+
44+
@Get(':id')
45+
@ApiOperation({ summary: 'Get execution details' })
46+
async findOne(@Param('id') id: string, @Request() req) {
47+
const execution = await this.executionRepository.findOne({
48+
where: { id },
49+
relations: ['report', 'executedBy'],
50+
});
51+
52+
if (!execution) {
53+
throw new NotFoundException(`Execution with ID ${id} not found`);
54+
}
55+
56+
if (execution.executedBy.id !== req.user.id) {
57+
throw new NotFoundException('Access denied');
58+
}
59+
60+
return execution;
61+
}
62+
63+
@Get(':id/download')
64+
@ApiOperation({ summary: 'Download generated file' })
65+
async download(
66+
@Param('id') id: string,
67+
@Request() req,
68+
@Res({ passthrough: true }) res: Response,
69+
) {
70+
const execution = await this.findOne(id, req);
71+
72+
if (!execution.fileUrl || execution.status !== 'COMPLETED') {
73+
throw new NotFoundException('Report file not available');
74+
}
75+
76+
// Extract filename from URL
77+
const filename = execution.fileUrl.split('/').pop();
78+
const buffer = await this.fileStorageService.getFile(filename);
79+
80+
// Set appropriate headers
81+
const mimeTypes = {
82+
PDF: 'application/pdf',
83+
EXCEL: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
84+
CSV: 'text/csv',
85+
JSON: 'application/json',
86+
};
87+
88+
const extensions = {
89+
PDF: 'pdf',
90+
EXCEL: 'xlsx',
91+
CSV: 'csv',
92+
JSON: 'json',
93+
};
94+
95+
res.set({
96+
'Content-Type': mimeTypes[execution.format],
97+
'Content-Disposition': `attachment; filename="${execution.report.name}.${extensions[execution.format]}"`,
98+
});
99+
100+
return new StreamableFile(buffer);
101+
}
102+
103+
@Delete(':id')
104+
@HttpCode(HttpStatus.NO_CONTENT)
105+
@ApiOperation({ summary: 'Cancel running execution' })
106+
async cancel(@Param('id') id: string, @Request() req) {
107+
const execution = await this.findOne(id, req);
108+
109+
if (execution.status === 'RUNNING') {
110+
execution.status = 'FAILED';
111+
execution.error = 'Cancelled by user';
112+
execution.completedAt = new Date();
113+
await this.executionRepository.save(execution);
114+
}
115+
}
116+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// src/reports/controllers/reports.controller.ts
2+
import {
3+
Controller,
4+
Get,
5+
Post,
6+
Put,
7+
Delete,
8+
Body,
9+
Param,
10+
Query,
11+
UseGuards,
12+
Request,
13+
HttpCode,
14+
HttpStatus,
15+
Res,
16+
StreamableFile,
17+
} from '@nestjs/common';
18+
import { Response } from 'express';
19+
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
20+
import { ReportsService } from '../services/reports.service';
21+
import { CreateReportDto, UpdateReportDto, ExecuteReportDto, ShareReportDto } from '../dto/create-report.dto';
22+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
23+
24+
@ApiTags('Reports')
25+
@ApiBearerAuth()
26+
@Controller('api/v1/reports')
27+
@UseGuards(JwtAuthGuard)
28+
export class ReportsController {
29+
constructor(private readonly reportsService: ReportsService) {}
30+
31+
@Post()
32+
@ApiOperation({ summary: 'Create custom report' })
33+
async create(@Body() createReportDto: CreateReportDto, @Request() req) {
34+
return this.reportsService.create(createReportDto, req.user);
35+
}
36+
37+
@Get()
38+
@ApiOperation({ summary: 'List all reports (user\'s + public)' })
39+
async findAll(@Request() req) {
40+
return this.reportsService.findAll(req.user);
41+
}
42+
43+
@Get('templates')
44+
@ApiOperation({ summary: 'List predefined report templates' })
45+
async getTemplates() {
46+
return this.reportsService.getTemplates();
47+
}
48+
49+
@Get(':id')
50+
@ApiOperation({ summary: 'Get report configuration' })
51+
async findOne(@Param('id') id: string, @Request() req) {
52+
return this.reportsService.findOne(id, req.user);
53+
}
54+
55+
@Put(':id')
56+
@ApiOperation({ summary: 'Update report' })
57+
async update(
58+
@Param('id') id: string,
59+
@Body() updateReportDto: UpdateReportDto,
60+
@Request() req,
61+
) {
62+
return this.reportsService.update(id, updateReportDto, req.user);
63+
}
64+
65+
@Delete(':id')
66+
@HttpCode(HttpStatus.NO_CONTENT)
67+
@ApiOperation({ summary: 'Delete report' })
68+
async remove(@Param('id') id: string, @Request() req) {
69+
await this.reportsService.remove(id, req.user);
70+
}
71+
72+
@Post(':id/execute')
73+
@ApiOperation({ summary: 'Execute report' })
74+
async execute(
75+
@Param('id') id: string,
76+
@Body() executeReportDto: ExecuteReportDto,
77+
@Request() req,
78+
) {
79+
return this.reportsService.execute(
80+
id,
81+
executeReportDto.format,
82+
executeReportDto.parameters || {},
83+
req.user,
84+
);
85+
}
86+
87+
@Get(':id/preview')
88+
@ApiOperation({ summary: 'Preview report (first 100 rows)' })
89+
async preview(@Param('id') id: string, @Request() req) {
90+
return this.reportsService.preview(id, req.user);
91+
}
92+
93+
@Post(':id/share')
94+
@ApiOperation({ summary: 'Share report with users' })
95+
async share(
96+
@Param('id') id: string,
97+
@Body() shareReportDto: ShareReportDto,
98+
@Request() req,
99+
) {
100+
return this.reportsService.share(id, shareReportDto.userIds, req.user);
101+
}
102+
}

0 commit comments

Comments
 (0)