Spaces:
Sleeping
Sleeping
| 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[]; | |
| } | |
| ({ | |
| 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 <your_token></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, | |
| }, | |
| ], | |
| }, | |
| ]; | |
| } |