medicodeapp / backend /src /modules /procedures /procedures.controller.ts
Denisijcu's picture
upload files
c98875e
// ── 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<string, string> = { 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<string, string> = {
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<string, number> = {};
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();
}
}