medicodeapp / frontend /src /app /features /dashboard /dashboard.component.ts
Denisijcu's picture
upload files
c98875e
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: `
<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;
}
}