medicodeapp / frontend /src /app /features /api /api_reference.component.ts
Denisijcu's picture
upload files
c98875e
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
interface Endpoint {
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
path: string;
description: string;
auth: boolean;
body?: string;
response?: string;
}
interface ApiGroup {
id: string;
title: string;
tag: string;
tagColor: string;
baseUrl: string;
endpoints: Endpoint[];
}
@Component({
selector: 'app-api',
standalone: true,
imports: [CommonModule],
template: `
<div class="api-page">
<!-- Header -->
<div class="api-header">
<div class="api-header-content">
<div class="api-eyebrow">
<span class="version-pill">v1.0</span>
<span class="rest-pill">REST API</span>
</div>
<h1>API Reference</h1>
<p>Base URL: <code class="base-url">https://api.medicode.io/v1</code></p>
<p class="api-subtitle">All endpoints require JWT authentication via Bearer token unless stated otherwise.</p>
</div>
<div class="auth-card">
<div class="auth-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Authentication
</div>
<code class="auth-snippet">Authorization: Bearer &lt;your_token&gt;</code>
<div class="auth-note">Obtain a token via <strong>POST /auth/login</strong></div>
</div>
</div>
<!-- Body -->
<div class="api-body">
<!-- Sidebar -->
<aside class="api-sidebar">
<div class="sidebar-section-title">Endpoints</div>
@for (g of groups; track g.id) {
<div
class="sidebar-group"
[class.active]="activeGroup() === g.id"
(click)="setGroup(g.id)"
>
<span class="sidebar-tag" [class]="'tag-' + g.tagColor">{{ g.tag }}</span>
<span class="sidebar-label">{{ g.title }}</span>
</div>
}
<div class="sidebar-divider"></div>
<div class="sidebar-section-title">Resources</div>
<a class="sidebar-link" href="#">Postman Collection</a>
<a class="sidebar-link" href="#">OpenAPI Spec</a>
<a class="sidebar-link" href="#">Rate Limits</a>
<a class="sidebar-link" href="#">Error Codes</a>
</aside>
<!-- Main panel -->
<div class="api-main">
@for (g of groups; track g.id) {
@if (activeGroup() === g.id) {
<div class="group-header">
<span class="group-tag" [class]="'tag-' + g.tagColor">{{ g.tag }}</span>
<div>
<h2>{{ g.title }}</h2>
<code class="group-base">{{ g.baseUrl }}</code>
</div>
</div>
@for (ep of g.endpoints; track ep.path) {
<div class="endpoint-card" [class.open]="isOpen(g.id + ep.path)" (click)="toggle(g.id + ep.path)">
<div class="endpoint-row">
<span class="method" [class]="'method-' + ep.method.toLowerCase()">{{ ep.method }}</span>
<code class="ep-path">{{ ep.path }}</code>
<span class="ep-desc">{{ ep.description }}</span>
@if (ep.auth) {
<svg class="lock-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
}
<svg class="chevron" [class.rotated]="isOpen(g.id + ep.path)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
@if (isOpen(g.id + ep.path)) {
<div class="endpoint-body" (click)="$event.stopPropagation()">
@if (ep.body) {
<div class="code-block-wrap">
<div class="code-block-label">
<span>Request Body</span>
<button class="copy-btn" (click)="copy(ep.body!)">Copy</button>
</div>
<pre class="code-block"><code>{{ ep.body }}</code></pre>
</div>
}
@if (ep.response) {
<div class="code-block-wrap">
<div class="code-block-label">
<span>Response <span class="status-200">200 OK</span></span>
<button class="copy-btn" (click)="copy(ep.response!)">Copy</button>
</div>
<pre class="code-block"><code>{{ ep.response }}</code></pre>
</div>
}
</div>
}
</div>
}
}
}
</div>
</div>
</div>
`,
styles: [`
.api-page {
max-width: 1400px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
}
/* ── Header ────────────────────────────────────── */
.api-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 2rem;
padding: 2.5rem 2rem 2rem;
background: #0f172a;
flex-wrap: wrap;
}
.api-eyebrow { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
.version-pill, .rest-pill {
font-size: 0.7rem;
font-weight: 700;
padding: 0.2rem 0.6rem;
border-radius: 99px;
letter-spacing: 0.05em;
}
.version-pill { background: #1e293b; color: #94a3b8; border: 1px solid #334155; }
.rest-pill { background: rgba(14,165,233,0.15); color: #38bdf8; border: 1px solid rgba(14,165,233,0.3); }
.api-header h1 {
font-size: 1.75rem;
font-weight: 800;
color: #f8fafc;
margin: 0 0 0.5rem;
letter-spacing: -0.02em;
}
.base-url {
background: #1e293b;
color: #38bdf8;
padding: 0.2rem 0.6rem;
border-radius: 5px;
font-size: 0.875rem;
}
.api-subtitle { font-size: 0.85rem; color: #64748b; margin: 0.5rem 0 0; }
/* Auth card */
.auth-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.25rem 1.5rem;
min-width: 280px;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.auth-card-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.auth-snippet {
display: block;
background: #0f172a;
color: #38bdf8;
padding: 0.6rem 0.85rem;
border-radius: 7px;
font-size: 0.8rem;
border: 1px solid #334155;
}
.auth-note { font-size: 0.78rem; color: #64748b; }
.auth-note strong { color: #94a3b8; }
/* ── Body ──────────────────────────────────────── */
.api-body {
display: grid;
grid-template-columns: 220px 1fr;
gap: 0;
min-height: calc(100vh - 200px);
}
/* Sidebar */
.api-sidebar {
background: #f8fafc;
border-right: 1px solid #e2e8f0;
padding: 1.5rem 1rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
position: sticky;
top: 0;
align-self: start;
height: 100vh;
overflow-y: auto;
}
.sidebar-section-title {
font-size: 0.68rem;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 0.25rem 0.5rem;
margin-bottom: 0.25rem;
}
.sidebar-group {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 0.75rem;
border-radius: 7px;
cursor: pointer;
transition: background 0.12s;
}
.sidebar-group:hover { background: #e2e8f0; }
.sidebar-group.active { background: #e0f2fe; }
.sidebar-group.active .sidebar-label { color: #0369a1; font-weight: 600; }
.sidebar-label { font-size: 0.85rem; color: #374151; }
.sidebar-tag {
font-size: 0.62rem;
font-weight: 700;
padding: 0.15rem 0.4rem;
border-radius: 4px;
flex-shrink: 0;
}
.tag-blue { background: #eff6ff; color: #1d4ed8; }
.tag-green { background: #f0fdf4; color: #15803d; }
.tag-purple { background: #faf5ff; color: #7c3aed; }
.tag-sky { background: #f0f9ff; color: #0369a1; }
.tag-orange { background: #fff7ed; color: #c2410c; }
.sidebar-divider { height: 1px; background: #e2e8f0; margin: 0.75rem 0; }
.sidebar-link {
font-size: 0.82rem;
color: #64748b;
text-decoration: none;
padding: 0.35rem 0.75rem;
border-radius: 6px;
transition: all 0.12s;
}
.sidebar-link:hover { background: #e2e8f0; color: #0f172a; }
/* Main */
.api-main { padding: 2rem; display: flex; flex-direction: column; gap: 1rem; }
.group-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.group-tag {
font-size: 0.75rem;
font-weight: 700;
padding: 0.3rem 0.75rem;
border-radius: 6px;
}
.group-header h2 {
font-size: 1.25rem;
font-weight: 700;
color: #0f172a;
margin: 0 0 0.15rem;
}
.group-base {
font-size: 0.78rem;
color: #64748b;
background: #f1f5f9;
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
/* Endpoint cards */
.endpoint-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.endpoint-card:hover { border-color: #0EA5E9; box-shadow: 0 0 0 3px rgba(14,165,233,0.07); }
.endpoint-row {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.85rem 1.1rem;
}
.method {
font-size: 0.7rem;
font-weight: 800;
padding: 0.25rem 0.55rem;
border-radius: 5px;
letter-spacing: 0.05em;
flex-shrink: 0;
min-width: 52px;
text-align: center;
}
.method-get { background: #f0fdf4; color: #15803d; border: 1px solid #bbf7d0; }
.method-post { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
.method-patch { background: #fff7ed; color: #c2410c; border: 1px solid #fed7aa; }
.method-delete { background: #fff1f2; color: #be123c; border: 1px solid #fecdd3; }
.ep-path {
font-size: 0.88rem;
color: #1e293b;
font-family: 'Fira Code', 'Consolas', monospace;
flex-shrink: 0;
}
.ep-desc { font-size: 0.82rem; color: #64748b; }
.lock-icon { width: 14px; height: 14px; color: #94a3b8; margin-left: auto; flex-shrink: 0; }
.chevron {
width: 16px;
height: 16px;
color: #94a3b8;
flex-shrink: 0;
transition: transform 0.2s;
margin-left: auto;
}
.chevron.rotated { transform: rotate(180deg); }
.lock-icon + .chevron { margin-left: 0; }
/* Expanded body */
.endpoint-body {
border-top: 1px solid #f1f5f9;
padding: 1.1rem;
background: #fafafa;
display: flex;
flex-direction: column;
gap: 1rem;
}
.code-block-wrap { display: flex; flex-direction: column; gap: 0; border-radius: 8px; overflow: hidden; border: 1px solid #e2e8f0; }
.code-block-label {
display: flex;
align-items: center;
justify-content: space-between;
background: #f1f5f9;
padding: 0.5rem 0.85rem;
font-size: 0.75rem;
font-weight: 600;
color: #64748b;
border-bottom: 1px solid #e2e8f0;
}
.status-200 {
background: #f0fdf4;
color: #15803d;
padding: 0.1rem 0.4rem;
border-radius: 4px;
font-size: 0.68rem;
margin-left: 0.4rem;
}
.copy-btn {
background: none;
border: 1px solid #e2e8f0;
border-radius: 5px;
font-size: 0.7rem;
color: #64748b;
padding: 0.15rem 0.5rem;
cursor: pointer;
transition: all 0.12s;
}
.copy-btn:hover { border-color: #0EA5E9; color: #0EA5E9; background: #f0f9ff; }
.code-block {
background: #0f172a;
color: #e2e8f0;
margin: 0;
padding: 1rem 1.1rem;
font-size: 0.8rem;
font-family: 'Fira Code', 'Consolas', monospace;
overflow-x: auto;
line-height: 1.6;
}
@media (max-width: 900px) {
.api-body { grid-template-columns: 1fr; }
.api-sidebar { display: none; }
}
`],
})
export class ApiReferenceComponent {
activeGroup = signal('auth');
openCards = signal<Set<string>>(new Set());
setGroup(id: string) { this.activeGroup.set(id); }
toggle(key: string) {
const s = new Set(this.openCards());
s.has(key) ? s.delete(key) : s.add(key);
this.openCards.set(s);
}
isOpen(key: string) { return this.openCards().has(key); }
copy(text: string) { navigator.clipboard.writeText(text); }
groups: ApiGroup[] = [
{
id: 'auth',
title: 'Authentication',
tag: 'AUTH',
tagColor: 'purple',
baseUrl: '/auth',
endpoints: [
{
method: 'POST',
path: '/auth/register',
description: 'Register a new user account',
auth: false,
body: `{
"name": "Jane Doe",
"email": "jane@clinic.com",
"password": "securepass123",
"role": "coder"
}`,
response: `{
"message": "User registered successfully",
"user": {
"_id": "64f3a...",
"name": "Jane Doe",
"email": "jane@clinic.com",
"role": "coder"
}
}`,
},
{
method: 'POST',
path: '/auth/login',
description: 'Authenticate and receive JWT token',
auth: false,
body: `{
"email": "jane@clinic.com",
"password": "securepass123"
}`,
response: `{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"_id": "64f3a...",
"name": "Jane Doe",
"role": "coder"
}
}`,
},
{
method: 'GET',
path: '/auth/profile',
description: 'Get authenticated user profile',
auth: true,
response: `{
"_id": "64f3a...",
"name": "Jane Doe",
"email": "jane@clinic.com",
"role": "coder",
"createdAt": "2024-01-15T10:30:00Z"
}`,
},
],
},
{
id: 'patients',
title: 'Patients',
tag: 'PATIENTS',
tagColor: 'blue',
baseUrl: '/patients',
endpoints: [
{
method: 'GET',
path: '/patients',
description: 'List all patients (paginated)',
auth: true,
response: `{
"data": [
{
"_id": "64f3b...",
"firstName": "John",
"lastName": "Smith",
"dob": "1985-04-12",
"mrn": "MRN-001"
}
],
"total": 120,
"page": 1,
"limit": 20
}`,
},
{
method: 'POST',
path: '/patients',
description: 'Create a new patient record',
auth: true,
body: `{
"firstName": "John",
"lastName": "Smith",
"dob": "1985-04-12",
"gender": "male",
"mrn": "MRN-001",
"phone": "+1-305-555-0100"
}`,
response: `{
"_id": "64f3b...",
"firstName": "John",
"lastName": "Smith",
"mrn": "MRN-001",
"createdAt": "2024-01-15T11:00:00Z"
}`,
},
{
method: 'GET',
path: '/patients/:id',
description: 'Get a single patient by ID',
auth: true,
response: `{
"_id": "64f3b...",
"firstName": "John",
"lastName": "Smith",
"encounters": [ { "_id": "64f4c...", "status": "coded" } ]
}`,
},
{
method: 'PATCH',
path: '/patients/:id',
description: 'Update patient information',
auth: true,
body: `{
"phone": "+1-305-555-0199",
"address": "123 Medical Blvd, Miami FL"
}`,
},
{
method: 'DELETE',
path: '/patients/:id',
description: 'Delete a patient record',
auth: true,
response: `{ "message": "Patient deleted successfully" }`,
},
],
},
{
id: 'encounters',
title: 'Encounters',
tag: 'ENCOUNTERS',
tagColor: 'green',
baseUrl: '/patients/:id/encounters',
endpoints: [
{
method: 'GET',
path: '/patients/:id/encounters',
description: 'List all encounters for a patient',
auth: true,
response: `[
{
"_id": "64f4c...",
"date": "2024-01-10",
"status": "coded",
"icd10Codes": ["J06.9", "Z00.00"],
"cptCodes": ["99213"]
}
]`,
},
{
method: 'POST',
path: '/patients/:id/encounters',
description: 'Create a new encounter',
auth: true,
body: `{
"date": "2024-01-10",
"notes": "Routine checkup",
"icd10Codes": ["Z00.00"],
"cptCodes": ["99213"]
}`,
},
{
method: 'PATCH',
path: '/patients/:id/encounters/:eid',
description: 'Update encounter codes or status',
auth: true,
body: `{
"status": "billed",
"icd10Codes": ["J06.9", "Z00.00"],
"cptCodes": ["99213", "36415"]
}`,
},
],
},
{
id: 'codes',
title: 'Code Search',
tag: 'CODES',
tagColor: 'sky',
baseUrl: '/codes',
endpoints: [
{
method: 'GET',
path: '/codes/icd10?q=diabetes',
description: 'Search ICD-10-CM diagnostic codes',
auth: true,
response: `[
{
"code": "E11.9",
"description": "Type 2 diabetes mellitus without complications",
"category": "Endocrine"
},
{
"code": "E10.9",
"description": "Type 1 diabetes mellitus without complications",
"category": "Endocrine"
}
]`,
},
{
method: 'GET',
path: '/codes/cpt?q=office+visit',
description: 'Search CPT procedure codes',
auth: true,
response: `[
{
"code": "99213",
"description": "Office or other outpatient visit, established patient, low complexity",
"category": "E&M"
}
]`,
},
],
},
{
id: 'reports',
title: 'Reports',
tag: 'REPORTS',
tagColor: 'orange',
baseUrl: '/reports',
endpoints: [
{
method: 'GET',
path: '/reports/encounters-by-month',
description: 'Encounters grouped by month',
auth: true,
response: `[
{ "_id": "2024-01", "count": 34 },
{ "_id": "2024-02", "count": 51 }
]`,
},
{
method: 'GET',
path: '/reports/top-diagnoses?limit=10',
description: 'Most used ICD-10 codes',
auth: true,
response: `[
{ "_id": "J06.9", "description": "Acute upper respiratory infection", "count": 42 }
]`,
},
{
method: 'GET',
path: '/reports/top-procedures?limit=10',
description: 'Most used CPT codes',
auth: true,
},
{
method: 'GET',
path: '/reports/export/pdf',
description: 'Export full report as PDF',
auth: true,
},
{
method: 'GET',
path: '/reports/export/excel',
description: 'Export full report as Excel',
auth: true,
},
],
},
];
}