Spaces:
Sleeping
Sleeping
| 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' ; | |
| ({ | |
| selector: 'app-dashboard', | |
| standalone: true, | |
| imports: [CommonModule, RouterLink, ChatBotComponent], | |
| template: ` | |
| <div class="dashboard"> | |
| <app-chat-bot></app-chat-bot> | |
| <header class="page-header"> | |
| <div> | |
| <h1>Dashboard</h1> | |
| <p>Welcome back, {{ auth.currentUser()?.name }}</p> | |
| </div> | |
| <div class="header-actions"> | |
| <a routerLink="/reports" class="btn-outline">📊 Reports</a> | |
| <a routerLink="/patients" class="btn-primary">+ New Patient</a> | |
| </div> | |
| </header> | |
| <!-- Stats --> | |
| <div class="stats-grid"> | |
| <div class="stat-card highlight"> | |
| <div class="stat-icon blue"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/> | |
| </svg> | |
| </div> | |
| <div class="stat-body"> | |
| <span class="stat-label">Total Patients</span> | |
| <span class="stat-value">{{ stats()?.totalPatients ?? '—' }}</span> | |
| </div> | |
| </div> | |
| <div class="stat-card highlight"> | |
| <div class="stat-icon green"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> | |
| <polyline points="14 2 14 8 20 8"/> | |
| </svg> | |
| </div> | |
| <div class="stat-body"> | |
| <span class="stat-label">Total Encounters</span> | |
| <span class="stat-value">{{ stats()?.totalEncounters ?? '—' }}</span> | |
| </div> | |
| </div> | |
| @for (s of statusStats(); track s._id) { | |
| <div class="stat-card"> | |
| <div class="stat-icon" [class]="statusColor(s._id)"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="9 11 12 14 22 4"/> | |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> | |
| </svg> | |
| </div> | |
| <div class="stat-body"> | |
| <span class="stat-label">{{ s._id | titlecase }}</span> | |
| <span class="stat-value">{{ s.count }}</span> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| <!-- Charts row --> | |
| <div class="charts-row"> | |
| <!-- Encounters by month --> | |
| <div class="chart-card"> | |
| <h3 class="chart-title">Encounters by Month</h3> | |
| <div class="bar-chart"> | |
| @if (encountersByMonth().length) { | |
| @for (m of encountersByMonth(); track m._id) { | |
| <div class="bar-col"> | |
| <div class="bar-wrap"> | |
| <div class="bar" [style.height.%]="barHeight(m.count)" | |
| [title]="m.count + ' encounters'"> | |
| <span class="bar-val">{{ m.count }}</span> | |
| </div> | |
| </div> | |
| <span class="bar-label">{{ monthLabel(m._id) }}</span> | |
| </div> | |
| } | |
| } @else { | |
| <div class="chart-empty">No data yet</div> | |
| } | |
| </div> | |
| </div> | |
| <!-- Top diagnoses --> | |
| <div class="chart-card"> | |
| <h3 class="chart-title">Top ICD-10 Diagnoses</h3> | |
| <div class="rank-list"> | |
| @if (topDiagnoses().length) { | |
| @for (d of topDiagnoses().slice(0,6); track d._id; let i = $index) { | |
| <div class="rank-item"> | |
| <span class="rank-num">{{ i + 1 }}</span> | |
| <div class="rank-info"> | |
| <span class="rank-code icd10">{{ d._id }}</span> | |
| <span class="rank-desc">{{ d.description }}</span> | |
| </div> | |
| <div class="rank-bar-wrap"> | |
| <div class="rank-bar icd10" [style.width.%]="rankWidth(d.count, topDiagnoses())"></div> | |
| </div> | |
| <span class="rank-count">{{ d.count }}</span> | |
| </div> | |
| } | |
| } @else { | |
| <div class="chart-empty">No data yet</div> | |
| } | |
| </div> | |
| </div> | |
| <!-- Top procedures --> | |
| <div class="chart-card"> | |
| <h3 class="chart-title">Top CPT Procedures</h3> | |
| <div class="rank-list"> | |
| @if (topProcedures().length) { | |
| @for (p of topProcedures().slice(0,6); track p._id; let i = $index) { | |
| <div class="rank-item"> | |
| <span class="rank-num">{{ i + 1 }}</span> | |
| <div class="rank-info"> | |
| <span class="rank-code cpt">{{ p._id }}</span> | |
| <span class="rank-desc">{{ p.description }}</span> | |
| </div> | |
| <div class="rank-bar-wrap"> | |
| <div class="rank-bar cpt" [style.width.%]="rankWidth(p.count, topProcedures())"></div> | |
| </div> | |
| <span class="rank-count">{{ p.count }}</span> | |
| </div> | |
| } | |
| } @else { | |
| <div class="chart-empty">No data yet</div> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Quick actions --> | |
| <div class="section-title">Quick Actions</div> | |
| <div class="quick-actions"> | |
| <a routerLink="/codes" class="action-card"> | |
| <div class="action-icon blue"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3>Search ICD-10 / CPT</h3> | |
| <p>Look up diagnostic and procedure codes instantly</p> | |
| </div> | |
| </a> | |
| <a routerLink="/patients" class="action-card"> | |
| <div class="action-icon green"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 5v14M5 12h14"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3>New Encounter</h3> | |
| <p>Start coding a patient encounter</p> | |
| </div> | |
| </a> | |
| <a routerLink="/reports" class="action-card"> | |
| <div class="action-icon purple"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <h3>View Reports</h3> | |
| <p>Analytics, exports PDF & Excel</p> | |
| </div> | |
| </a> | |
| </div> | |
| </div> | |
| `, | |
| 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<any>(null); | |
| statusStats = signal<any[]>([]); | |
| encountersByMonth = signal<any[]>([]); | |
| topDiagnoses = signal<any[]>([]); | |
| topProcedures = signal<any[]>([]); | |
| 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<string, string> = { 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; | |
| } | |
| } |