// ── procedures.controller.ts (reports) ──────────────────────────────────────── import { Controller, Get, Post, Body, Query, Res, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; import { Response } from 'express'; import { ProceduresService } from './procedures.service'; @ApiTags('Reports') @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @Controller('reports') export class ProceduresController { constructor(private svc: ProceduresService) {} @Get('top-diagnoses') @ApiOperation({ summary: 'Most used ICD-10 diagnostic codes' }) @ApiQuery({ name: 'limit', required: false }) getTopDiagnoses(@Query('limit') limit = 10) { return this.svc.getTopDiagnoses(+limit); } @Get('top-procedures') @ApiOperation({ summary: 'Most used CPT procedure codes' }) @ApiQuery({ name: 'limit', required: false }) getTopProcedures(@Query('limit') limit = 10) { return this.svc.getTopProcedures(+limit); } @Get('encounters-by-month') @ApiOperation({ summary: 'Encounter volume by month' }) getEncountersByMonth() { return this.svc.getEncountersByMonth(); } @Get('status-breakdown') @ApiOperation({ summary: 'Encounter status breakdown' }) getStatusBreakdown() { return this.svc.getStatusBreakdown(); } @Post('export/pdf') @ApiOperation({ summary: 'Export encounters report as PDF' }) async exportPDF(@Body() filters: any, @Res() res: Response) { const encounters = await this.svc.exportEncountersPDF(); const PDFDocument = require('pdfkit'); const doc = new PDFDocument({ margin: 50 }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="medicode-report-${new Date().toISOString().substring(0,10)}.pdf"`); doc.pipe(res); // Header doc.fontSize(20).fillColor('#0EA5E9').text('MediCode', 50, 50); doc.fontSize(12).fillColor('#64748b').text('Medical Coding Report', 50, 75); doc.fontSize(10).fillColor('#94a3b8').text(`Generated: ${new Date().toLocaleDateString()}`, 50, 92); doc.moveTo(50, 110).lineTo(545, 110).strokeColor('#e2e8f0').stroke(); // Stats doc.fontSize(14).fillColor('#0f172a').text('Encounters Summary', 50, 125); doc.fontSize(10).fillColor('#374151'); let y = 150; encounters.slice(0, 50).forEach((enc: any, i: number) => { if (y > 700) { doc.addPage(); y = 50; } const patient = enc.patient as any; const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Unknown'; const date = enc.encounterDate ? new Date(enc.encounterDate).toLocaleDateString() : '—'; const status = enc.status?.toUpperCase() || 'DRAFT'; doc.fillColor(i % 2 === 0 ? '#f8fafc' : 'white') .rect(50, y - 3, 495, 18).fill(); doc.fillColor('#0f172a').text(`${i + 1}.`, 55, y, { width: 20 }); doc.fillColor('#0f172a').text(patientName, 75, y, { width: 140 }); doc.fillColor('#64748b').text(date, 220, y, { width: 80 }); doc.fillColor('#64748b').text(enc.provider || '—', 305, y, { width: 140 }); const statusColors: Record = { DRAFT: '#b45309', CODED: '#1d4ed8', BILLED: '#7c3aed', PAID: '#065f46' }; doc.fillColor(statusColors[status] || '#374151').text(status, 450, y, { width: 60 }); const dx = (enc.diagnosticCodes || []).slice(0, 2).map((c: any) => c.code).join(', '); if (dx) { doc.fillColor('#94a3b8').fontSize(8).text(`ICD-10: ${dx}`, 75, y + 8, { width: 300 }); } doc.fontSize(10); y += dx ? 24 : 18; }); doc.end(); } @Post('export/excel') @ApiOperation({ summary: 'Export encounters report as Excel' }) async exportExcel(@Body() filters: any, @Res() res: Response) { const encounters = await this.svc.exportEncountersExcel(); const ExcelJS = require('exceljs'); const workbook = new ExcelJS.Workbook(); workbook.creator = 'MediCode'; workbook.created = new Date(); const sheet = workbook.addWorksheet('Encounters', { pageSetup: { paperSize: 9, orientation: 'landscape' }, }); // Columns sheet.columns = [ { header: '#', key: 'num', width: 5 }, { header: 'Patient', key: 'patient', width: 25 }, { header: 'MRN', key: 'mrn', width: 14 }, { header: 'Date', key: 'date', width: 14 }, { header: 'Provider', key: 'provider', width: 22 }, { header: 'Status', key: 'status', width: 12 }, { header: 'ICD-10 Codes', key: 'icd10', width: 30 }, { header: 'CPT Codes', key: 'cpt', width: 30 }, { header: 'Insurance', key: 'insurance', width: 22 }, { header: 'Coded By', key: 'codedBy', width: 18 }, { header: 'Notes', key: 'notes', width: 40 }, ]; // Header style sheet.getRow(1).eachCell((cell: any) => { cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF0EA5E9' } }; cell.alignment = { vertical: 'middle', horizontal: 'center' }; cell.border = { bottom: { style: 'medium', color: { argb: 'FF0284C7' } } }; }); sheet.getRow(1).height = 28; // Data rows const statusColors: Record = { draft: 'FFFEF3C7', coded: 'FFEFF6FF', billed: 'FFF3E8FF', paid: 'FFECFDF5', }; encounters.forEach((enc: any, i: number) => { const patient = enc.patient as any; const row = sheet.addRow({ num: i + 1, patient: patient ? `${patient.firstName} ${patient.lastName}` : '—', mrn: patient?.mrn || '—', date: enc.encounterDate ? new Date(enc.encounterDate).toLocaleDateString() : '—', provider: enc.provider || '—', status: enc.status?.toUpperCase() || 'DRAFT', icd10: (enc.diagnosticCodes || []).map((c: any) => `${c.code}: ${c.description}`).join('\n'), cpt: (enc.procedureCodes || []).map((c: any) => `${c.code}: ${c.description}`).join('\n'), insurance: patient?.insurance || '—', codedBy: (enc.codedBy as any)?.name || '—', notes: enc.notes || '', }); row.height = Math.max(20, (enc.diagnosticCodes?.length || 1) * 15); row.eachCell((cell: any) => { cell.alignment = { vertical: 'top', wrapText: true }; if (i % 2 === 1) { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF8FAFC' } }; } }); // Status color const statusCell = row.getCell('status'); const bgColor = statusColors[enc.status] || 'FFFFFFFF'; statusCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: bgColor } }; statusCell.font = { bold: true }; statusCell.alignment = { horizontal: 'center', vertical: 'middle' }; }); // Add summary sheet const summarySheet = workbook.addWorksheet('Summary'); summarySheet.addRow(['MediCode Report Summary']); summarySheet.addRow(['Generated', new Date().toLocaleString()]); summarySheet.addRow(['Total Encounters', encounters.length]); const statusCounts: Record = {}; encounters.forEach((e: any) => { statusCounts[e.status] = (statusCounts[e.status] || 0) + 1; }); Object.entries(statusCounts).forEach(([status, count]) => { summarySheet.addRow([status.toUpperCase(), count]); }); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename="medicode-report-${new Date().toISOString().substring(0,10)}.xlsx"`); await workbook.xlsx.write(res); res.end(); } }