import { Component, inject, OnInit, signal, AfterViewInit, ElementRef, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { PatientsService } from '../../core/services/patients.service'; import { ReportsService } from '../../core/services/reports.service'; import { AuthService } from '../../core/services/auth.service'; import {ChatBotComponent} from '../chat/chat-bot.component' ; @Component({ selector: 'app-dashboard', standalone: true, imports: [CommonModule, RouterLink, ChatBotComponent], template: `
Total Patients {{ stats()?.totalPatients ?? '—' }}
Total Encounters {{ stats()?.totalEncounters ?? '—' }}
@for (s of statusStats(); track s._id) {
{{ s._id | titlecase }} {{ s.count }}
}

Encounters by Month

@if (encountersByMonth().length) { @for (m of encountersByMonth(); track m._id) {
{{ m.count }}
{{ monthLabel(m._id) }}
} } @else {
No data yet
}

Top ICD-10 Diagnoses

@if (topDiagnoses().length) { @for (d of topDiagnoses().slice(0,6); track d._id; let i = $index) {
{{ i + 1 }}
{{ d._id }} {{ d.description }}
{{ d.count }}
} } @else {
No data yet
}

Top CPT Procedures

@if (topProcedures().length) { @for (p of topProcedures().slice(0,6); track p._id; let i = $index) {
{{ i + 1 }}
{{ p._id }} {{ p.description }}
{{ p.count }}
} } @else {
No data yet
}
Quick Actions

Search ICD-10 / CPT

Look up diagnostic and procedure codes instantly

New Encounter

Start coding a patient encounter

View Reports

Analytics, exports PDF & Excel

`, styles: [` .dashboard { padding:2rem; max-width:1400px; font-family:'Inter',system-ui,sans-serif; } .page-header { display:flex; align-items:flex-start; justify-content:space-between; margin-bottom:2rem; } .page-header h1 { font-size:1.75rem; font-weight:700; color:#0f172a; margin:0 0 0.25rem; } .page-header p { color:#64748b; margin:0; } .header-actions { display:flex; gap:0.75rem; } .btn-primary { padding:0.65rem 1.25rem; background:#0EA5E9; color:white; border-radius:8px; font-weight:600; font-size:0.9rem; text-decoration:none; border:none; cursor:pointer; } .btn-primary:hover { background:#0284c7; } .btn-outline { padding:0.6rem 1.25rem; background:white; color:#374151; border:1px solid #d1d5db; border-radius:8px; font-weight:600; font-size:0.9rem; text-decoration:none; } .btn-outline:hover { border-color:#0EA5E9; color:#0EA5E9; } .stats-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:1rem; margin-bottom:2rem; } .stat-card { background:white; border-radius:12px; padding:1.25rem; display:flex; align-items:center; gap:1rem; box-shadow:0 1px 3px rgba(0,0,0,0.07); border:1px solid #f1f5f9; } .stat-card.highlight { border-color:#e0f2fe; } .stat-icon { width:44px; height:44px; border-radius:10px; display:flex; align-items:center; justify-content:center; flex-shrink:0; } .stat-icon svg { width:22px; height:22px; } .stat-icon.blue { background:#eff6ff; color:#3b82f6; } .stat-icon.green { background:#f0fdf4; color:#22c55e; } .stat-icon.yellow { background:#fefce8; color:#eab308; } .stat-icon.purple { background:#faf5ff; color:#a855f7; } .stat-icon.teal { background:#f0fdfa; color:#14b8a6; } .stat-body { display:flex; flex-direction:column; } .stat-label { font-size:0.78rem; color:#64748b; font-weight:500; } .stat-value { font-size:1.75rem; font-weight:700; color:#0f172a; line-height:1.1; } .charts-row { display:grid; grid-template-columns:1.2fr 1fr 1fr; gap:1rem; margin-bottom:2rem; } .chart-card { background:white; border-radius:12px; padding:1.25rem; border:1px solid #e2e8f0; } .chart-title { font-size:0.875rem; font-weight:600; color:#374151; margin:0 0 1.25rem; } .chart-empty { color:#94a3b8; font-size:0.85rem; text-align:center; padding:2rem 0; } /* Bar chart */ .bar-chart { display:flex; align-items:flex-end; gap:0.5rem; height:140px; padding-bottom:1.5rem; position:relative; } .bar-col { display:flex; flex-direction:column; align-items:center; flex:1; height:100%; } .bar-wrap { flex:1; display:flex; align-items:flex-end; width:100%; } .bar { width:100%; background:linear-gradient(to top,#0EA5E9,#38bdf8); border-radius:4px 4px 0 0; min-height:4px; position:relative; transition:height 0.3s; cursor:pointer; } .bar:hover { background:linear-gradient(to top,#0284c7,#0EA5E9); } .bar-val { position:absolute; top:-18px; left:50%; transform:translateX(-50%); font-size:0.65rem; font-weight:700; color:#0f172a; white-space:nowrap; } .bar-label { font-size:0.65rem; color:#94a3b8; margin-top:0.25rem; white-space:nowrap; } /* Rank list */ .rank-list { display:flex; flex-direction:column; gap:0.5rem; } .rank-item { display:flex; align-items:center; gap:0.5rem; } .rank-num { width:18px; font-size:0.72rem; font-weight:700; color:#94a3b8; text-align:right; flex-shrink:0; } .rank-info { width:100px; flex-shrink:0; display:flex; flex-direction:column; gap:0.1rem; } .rank-code { font-size:0.72rem; font-weight:700; padding:0.1rem 0.4rem; border-radius:4px; } .rank-code.icd10 { background:#eff6ff; color:#1d4ed8; } .rank-code.cpt { background:#f0fdf4; color:#15803d; } .rank-desc { font-size:0.68rem; color:#64748b; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .rank-bar-wrap { flex:1; height:6px; background:#f1f5f9; border-radius:3px; overflow:hidden; } .rank-bar { height:100%; border-radius:3px; } .rank-bar.icd10 { background:#3b82f6; } .rank-bar.cpt { background:#10b981; } .rank-count { width:24px; font-size:0.72rem; font-weight:700; color:#374151; text-align:right; flex-shrink:0; } .section-title { font-size:1rem; font-weight:600; color:#374151; margin-bottom:1rem; } .quick-actions { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:1rem; } .action-card { background:white; border-radius:12px; padding:1.5rem; display:flex; align-items:center; gap:1.25rem; text-decoration:none; border:1px solid #e2e8f0; transition:border-color 0.15s,box-shadow 0.15s; } .action-card:hover { border-color:#0EA5E9; box-shadow:0 0 0 3px rgba(14,165,233,0.1); } .action-icon { width:48px; height:48px; border-radius:10px; display:flex; align-items:center; justify-content:center; flex-shrink:0; } .action-icon svg { width:24px; height:24px; } .action-icon.blue { background:#f0f9ff; color:#0EA5E9; } .action-icon.green { background:#f0fdf4; color:#10b981; } .action-icon.purple { background:#faf5ff; color:#8b5cf6; } .action-card h3 { font-size:0.95rem; font-weight:600; color:#0f172a; margin:0 0 0.25rem; } .action-card p { font-size:0.825rem; color:#64748b; margin:0; } @media (max-width:900px) { .charts-row { grid-template-columns:1fr; } } `], }) export class DashboardComponent implements OnInit { auth = inject(AuthService); private patientsService = inject(PatientsService); private reportsService = inject(ReportsService); stats = signal(null); statusStats = signal([]); encountersByMonth = signal([]); topDiagnoses = signal([]); topProcedures = signal([]); ngOnInit() { this.patientsService.getStats().subscribe(data => { this.stats.set(data); this.statusStats.set(data.byStatus || []); }); this.reportsService.getEncountersByMonth().subscribe(data => this.encountersByMonth.set(data || [])); this.reportsService.getTopDiagnoses(6).subscribe(data => this.topDiagnoses.set(data || [])); this.reportsService.getTopProcedures(6).subscribe(data => this.topProcedures.set(data || [])); } statusColor(status: string) { const map: Record = { draft: 'yellow', coded: 'blue', billed: 'purple', paid: 'teal' }; return map[status] || 'blue'; } barHeight(count: number): number { const max = Math.max(...this.encountersByMonth().map(m => m.count), 1); return (count / max) * 100; } 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; } rankWidth(count: number, list: any[]): number { const max = Math.max(...list.map(i => i.count), 1); return (count / max) * 100; } }