Spaces:
Sleeping
Sleeping
File size: 7,759 Bytes
c98875e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | // ββ 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();
}
} |