import { Body, Controller, Get, Param, ParseUUIDPipe, ParseIntPipe, Patch, Post, Query, Req, Res, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { CurrentAuth } from '../auth/decorators/current-auth.decorator'; import { RequirePermissions } from '../authorization/decorators/require-permissions.decorator'; import type { Response } from 'express'; import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context'; import { CreateInspectionReportFollowUpDto } from './dto/create-inspection-report-follow-up.dto'; import { ListInspectionReportsQueryDto } from './dto/list-inspection-reports-query.dto'; import { OfficializeInspectionReportDto } from './dto/officialize-inspection-report.dto'; import { UpdateInspectionReportDto } from './dto/update-inspection-report.dto'; import { InspectionReportWorkflowService, MAX_INSPECTION_REPORT_FILE_BYTES, type UploadedInspectionReportFile, } from './inspection-report-workflow.service'; import { InspectionReportsService } from './inspection-reports.service'; import { InspectionReportWordService } from './inspection-report-word.service'; import { InspectionActPdfService } from './inspection-act-pdf.service'; @Controller('inspection-reports') export class InspectionReportsController { constructor( private readonly reports: InspectionReportsService, private readonly word: InspectionReportWordService, private readonly workflow: InspectionReportWorkflowService, ) {} @Get() @RequirePermissions('inspection_reports.read') list(@Query() query: ListInspectionReportsQueryDto) { return this.reports.list(query); } @Get('pending') @RequirePermissions('inspection_reports.read') pending(@Query() query: ListInspectionReportsQueryDto) { return this.reports.listPending(query); } @Post(':id/word') @RequirePermissions('inspection_reports.generate') async generateWord(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) { await this.word.ensure(id); return this.reports.get(id); } @Get(':id/word') @RequirePermissions('inspection_reports.read') async wordContent( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Res() response: Response, ) { const content = await this.word.content(id); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Disposition', `attachment; filename="${content.originalName.replaceAll('"', '')}"`); return response.sendFile(content.filePath); } @Get(':id/consolidated-word') @RequirePermissions('inspection_reports.read') async consolidatedWordContent( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Res() response: Response, ) { const content = await this.word.consolidatedContent(id); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Disposition', `attachment; filename="${content.originalName.replaceAll('"', '')}"`); response.setHeader('Cache-Control', 'private, no-store'); response.setHeader('X-Content-Type-Options', 'nosniff'); return response.sendFile(content.filePath); } @Get(':id/consolidated-word/revisions/:version') @RequirePermissions('inspection_reports.read') async consolidatedWordRevision( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Param('version', ParseIntPipe) version: number, @Res() response: Response, ) { const content = await this.word.consolidatedRevisionContent(id, version); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Disposition', `attachment; filename="${content.originalName.replaceAll('"', '')}"`); response.setHeader('Cache-Control', 'private, no-store'); return response.sendFile(content.filePath); } @Get(':id/gedo-pdf') @RequirePermissions('inspection_reports.read') async gedoPdfContent( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Res() response: Response, ) { const content = await this.workflow.officialPdfContent(id); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Disposition', `attachment; filename="${content.originalName.replaceAll('"', '')}"`); return response.sendFile(content.filePath); } @Patch(':id') @RequirePermissions('inspection_reports.generate') updateNarrative( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Body() dto: UpdateInspectionReportDto, @CurrentAuth() principal: AuthPrincipal, @Req() request: RequestWithContext, ) { return this.workflow.updateNarrative(id, dto, principal, request); } @Post(':id/officialize') @RequirePermissions('inspection_reports.generate') @UseInterceptors(FileInterceptor('file', { limits: { fileSize: MAX_INSPECTION_REPORT_FILE_BYTES, files: 1 }, })) officialize( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Body() dto: OfficializeInspectionReportDto, @UploadedFile() file: UploadedInspectionReportFile | undefined, @CurrentAuth() principal: AuthPrincipal, @Req() request: RequestWithContext, ) { return this.workflow.officialize(id, dto, file, principal, request); } @Get(':id/follow-ups') @RequirePermissions('inspection_reports.read') followUps(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) { return this.workflow.listFollowUps(id); } @Get(':id/follow-ups/:followUpId/content') @RequirePermissions('inspection_reports.read') async followUpContent( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Param('followUpId', new ParseUUIDPipe({ version: '4' })) followUpId: string, @Res() response: Response, ): Promise { const file = await this.workflow.followUpContent(id, followUpId); const safeName = file.originalName.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_'); response.setHeader('Content-Type', file.mimeType); response.setHeader('Content-Length', String(file.sizeBytes)); response.setHeader('Content-Disposition', `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(file.originalName)}`); response.setHeader('Cache-Control', 'private, no-store'); response.setHeader('X-Content-Type-Options', 'nosniff'); await new Promise((resolveSend, rejectSend) => response.sendFile(file.filePath, (error) => error ? rejectSend(error) : resolveSend())); } @Post(':id/follow-ups') @RequirePermissions('inspection_reports.generate') @UseInterceptors(FileInterceptor('file', { limits: { fileSize: MAX_INSPECTION_REPORT_FILE_BYTES, files: 1 }, })) addFollowUp( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, @Body() dto: CreateInspectionReportFollowUpDto, @UploadedFile() file: UploadedInspectionReportFile | undefined, @CurrentAuth() principal: AuthPrincipal, @Req() request: RequestWithContext, ) { return this.workflow.addFollowUp(id, dto, file, principal, request); } @Get(':id') @RequirePermissions('inspection_reports.read') get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) { return this.reports.get(id); } } @Controller('inspection-acts/:actId/report') export class InspectionActReportController { constructor(private readonly reports: InspectionReportsService) {} @Post() @RequirePermissions('inspection_reports.generate') generate( @Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string, @CurrentAuth() principal: AuthPrincipal, @Req() request: RequestWithContext, ) { return this.reports.generate(actId, principal, request); } } @Controller('inspection-acts/:actId/pdf') export class InspectionActPdfController { constructor(private readonly pdf: InspectionActPdfService) {} @Get() @RequirePermissions('inspection_acts.read') async content( @Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string, @Res() response: Response, ) { await this.pdf.ensure(actId); const content = await this.pdf.content(actId); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Length', String(content.buffer.length)); response.setHeader('Content-Disposition', `inline; filename="${content.originalName.replaceAll('"', '')}"`); response.setHeader('Cache-Control', 'private, no-store'); response.setHeader('X-Content-Type-Options', 'nosniff'); return response.send(content.buffer); } } @Controller('inspection-acts/:actId/consolidated-pdf') export class InspectionActConsolidatedPdfController { constructor(private readonly pdf: InspectionActPdfService) {} @Get('revisions/:version') @RequirePermissions('inspection_acts.read') async revision( @Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string, @Param('version', ParseIntPipe) version: number, @Res() response: Response, ) { const content = await this.pdf.consolidatedRevisionContent(actId, version); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Length', String(content.buffer.length)); response.setHeader('Content-Disposition', `inline; filename="${content.originalName.replaceAll('"', '')}"`); response.setHeader('Cache-Control', 'private, no-store'); return response.send(content.buffer); } @Get() @RequirePermissions('inspection_acts.read') async content( @Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string, @Res() response: Response, ) { const content = await this.pdf.consolidatedContent(actId); response.setHeader('Content-Type', content.mimeType); response.setHeader('Content-Length', String(content.buffer.length)); response.setHeader('Content-Disposition', `inline; filename="${content.originalName.replaceAll('"', '')}"`); response.setHeader('Cache-Control', 'private, no-store'); response.setHeader('X-Content-Type-Options', 'nosniff'); return response.send(content.buffer); } }