import { Component, inject, OnInit, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ReportsService } from '../../core/services/reports.service'; @Component({ selector: 'app-reports', standalone: true, imports: [CommonModule, FormsModule], template: `

Encounter Status Breakdown

@for (s of statusBreakdown(); track s._id) {
{{ s._id | titlecase }} {{ getPct(s.count) }}%
{{ s.count }} encounters
}

Top ICD-10 Diagnoses

@for (d of topDiagnoses(); track d._id; let i = $index) { } @if (!topDiagnoses().length) { }
#CodeDescriptionCount
{{ i + 1 }} {{ d._id }} {{ d.description }} {{ d.count }}
No data

Top CPT Procedures

@for (p of topProcedures(); track p._id; let i = $index) { } @if (!topProcedures().length) { }
#CodeDescriptionCount
{{ i + 1 }} {{ p._id }} {{ p.description }} {{ p.count }}
No data

Encounter Volume by Month

@for (m of encountersByMonth(); track m._id) {
{{ m.count }}
{{ monthLabel(m._id) }}
} @if (!encountersByMonth().length) {
No data available
}
`, styles: [` .reports-page { padding:2rem; font-family:'Inter',system-ui,sans-serif; max-width:1200px; } .page-header { display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:2rem; flex-wrap:wrap; gap:1rem; } .page-header h1 { font-size:1.75rem; font-weight:700; color:#0f172a; margin:0 0 0.25rem; } .page-header p { color:#64748b; margin:0; } .export-btns { display:flex; gap:0.75rem; } .btn-export { padding:0.65rem 1.25rem; border:none; border-radius:8px; font-weight:600; font-size:0.9rem; cursor:pointer; display:flex; align-items:center; gap:0.5rem; } .btn-export:disabled { opacity:0.6; cursor:not-allowed; } .btn-export.pdf { background:#ef4444; color:white; } .btn-export.pdf:hover:not(:disabled) { background:#dc2626; } .btn-export.excel { background:#16a34a; color:white; } .btn-export.excel:hover:not(:disabled) { background:#15803d; } .spinner-sm { width:14px; height:14px; border:2px solid rgba(255,255,255,0.4); border-top-color:white; border-radius:50%; animation:spin 0.6s linear infinite; display:inline-block; } @keyframes spin { to { transform:rotate(360deg); } } .report-grid { display:grid; grid-template-columns:1fr 1fr; gap:1.25rem; } .report-card { background:white; border-radius:12px; padding:1.5rem; border:1px solid #e2e8f0; } .report-card.full { grid-column:1/-1; } .report-card h3 { font-size:0.9rem; font-weight:600; color:#374151; margin:0 0 1.25rem; } /* Status breakdown */ .status-breakdown { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:1rem; } .status-item { display:flex; flex-direction:column; gap:0.4rem; } .status-header { display:flex; align-items:center; justify-content:space-between; } .status-pill { padding:0.2rem 0.7rem; border-radius:999px; font-size:0.75rem; font-weight:700; text-transform:uppercase; } .status-pill.draft { background:#fef3c7; color:#b45309; } .status-pill.coded { background:#eff6ff; color:#1d4ed8; } .status-pill.billed { background:#f3e8ff; color:#7c3aed; } .status-pill.paid { background:#ecfdf5; color:#065f46; } .status-pct { font-size:0.85rem; font-weight:700; color:#0f172a; } .progress-bar { height:8px; background:#f1f5f9; border-radius:4px; overflow:hidden; } .progress-fill { height:100%; border-radius:4px; transition:width 0.6s; } .progress-fill.draft { background:#f59e0b; } .progress-fill.coded { background:#3b82f6; } .progress-fill.billed { background:#8b5cf6; } .progress-fill.paid { background:#10b981; } .status-count { font-size:0.78rem; color:#64748b; } /* Tables */ .report-table { width:100%; border-collapse:collapse; font-size:0.85rem; } .report-table th { padding:0.6rem 0.75rem; text-align:left; font-weight:600; color:#374151; border-bottom:2px solid #e2e8f0; font-size:0.78rem; text-transform:uppercase; letter-spacing:0.04em; } .report-table td { padding:0.6rem 0.75rem; border-bottom:1px solid #f8fafc; color:#1e293b; } .report-table tr:last-child td { border-bottom:none; } .report-table tr:hover td { background:#f8fafc; } .rank { color:#94a3b8; font-weight:600; width:30px; } .code-badge { padding:0.15rem 0.5rem; border-radius:4px; font-weight:700; font-size:0.78rem; white-space:nowrap; font-family:'JetBrains Mono',monospace; } .code-badge.icd10 { background:#eff6ff; color:#1d4ed8; } .code-badge.cpt { background:#f0fdf4; color:#15803d; } .desc { color:#64748b; max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .count { font-weight:700; text-align:right; } .empty { text-align:center; color:#94a3b8; padding:2rem; } /* Month chart */ .month-chart { display:flex; align-items:flex-end; gap:1rem; height:160px; padding:0 0 1.5rem; } .month-col { display:flex; flex-direction:column; align-items:center; flex:1; height:100%; } .month-count { font-size:0.72rem; font-weight:700; color:#374151; margin-bottom:0.25rem; } .month-bar { width:100%; background:linear-gradient(to top,#0EA5E9,#38bdf8); border-radius:4px 4px 0 0; min-height:4px; } .month-label { font-size:0.7rem; color:#94a3b8; margin-top:0.4rem; } @media (max-width:768px) { .report-grid { grid-template-columns:1fr; } } `], }) export class ReportsComponent implements OnInit { private reportsSvc = inject(ReportsService); statusBreakdown = signal([]); topDiagnoses = signal([]); topProcedures = signal([]); encountersByMonth = signal([]); exporting = signal(false); get totalEncounters() { return this.statusBreakdown().reduce((sum, s) => sum + s.count, 0); } ngOnInit() { this.reportsSvc.getStatusBreakdown().subscribe(d => this.statusBreakdown.set(d || [])); this.reportsSvc.getTopDiagnoses(10).subscribe(d => this.topDiagnoses.set(d || [])); this.reportsSvc.getTopProcedures(10).subscribe(d => this.topProcedures.set(d || [])); this.reportsSvc.getEncountersByMonth().subscribe(d => this.encountersByMonth.set(d || [])); } getPct(count: number): number { const total = this.totalEncounters; return total ? Math.round((count / total) * 100) : 0; } barHeight(count: number): number { const max = Math.max(...this.encountersByMonth().map(m => m.count), 1); return Math.max((count / max) * 120, 4); } monthLabel(id: string): string { if (!id) return ''; const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const parts = id.split('-'); return months[parseInt(parts[1]) - 1] || id; } exportPDF() { this.exporting.set('pdf'); this.reportsSvc.exportEncountersPDF().subscribe({ next: (blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `medicode-report-${new Date().toISOString().substring(0,10)}.pdf`; a.click(); URL.revokeObjectURL(url); this.exporting.set(false); }, error: () => this.exporting.set(false), }); } exportExcel() { this.exporting.set('excel'); this.reportsSvc.exportEncountersExcel().subscribe({ next: (blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `medicode-report-${new Date().toISOString().substring(0,10)}.xlsx`; a.click(); URL.revokeObjectURL(url); this.exporting.set(false); }, error: () => this.exporting.set(false), }); } }