Spaces:
Sleeping
Sleeping
| // ββ 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'; | |
| ('Reports') | |
| () | |
| (AuthGuard('jwt')) | |
| ('reports') | |
| export class ProceduresController { | |
| constructor(private svc: ProceduresService) {} | |
| ('top-diagnoses') | |
| ({ summary: 'Most used ICD-10 diagnostic codes' }) | |
| ({ name: 'limit', required: false }) | |
| getTopDiagnoses(('limit') limit = 10) { | |
| return this.svc.getTopDiagnoses(+limit); | |
| } | |
| ('top-procedures') | |
| ({ summary: 'Most used CPT procedure codes' }) | |
| ({ name: 'limit', required: false }) | |
| getTopProcedures(('limit') limit = 10) { | |
| return this.svc.getTopProcedures(+limit); | |
| } | |
| ('encounters-by-month') | |
| ({ summary: 'Encounter volume by month' }) | |
| getEncountersByMonth() { | |
| return this.svc.getEncountersByMonth(); | |
| } | |
| ('status-breakdown') | |
| ({ summary: 'Encounter status breakdown' }) | |
| getStatusBreakdown() { | |
| return this.svc.getStatusBreakdown(); | |
| } | |
| ('export/pdf') | |
| ({ summary: 'Export encounters report as PDF' }) | |
| async exportPDF(() filters: any, () 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(); | |
| } | |
| ('export/excel') | |
| ({ summary: 'Export encounters report as Excel' }) | |
| async exportExcel(() filters: any, () 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(); | |
| } | |
| } |