Menu

Change language

GitHub repo

Lab Results Viewer

blockhealthcare

Quest and Labcorp style diagnostic lab results report viewer: patient metadata header, clinical attention banner, grouped test panels (Lipid, Glucose, Renal) with LOINC codes, reference range linear scale bars, historical 6-month trend deltas, and doctor clinical impression card with electronic verification.

Installation

$npx uipkge-ng@latest add lab-results-viewer
Manifest:https://uipkge.dev/r/angular/lab-results-viewer.jsonInstalls to:src/app/components/blocks/lab-results-viewer/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

LabTest
interface LabTest {
  id: string
  name: string
  panel: 'lipid' | 'metabolic' | 'renal'
  panelLabel: string
  loinc: string
  value: number
  unit: string
  referenceRangeText: string
  status: TestStatus
  scaleMin: number
  scaleMax: number
  normalMin: number
  normalMax: number
  markerPercent: number
  rangeZoneLeft: number
  rangeZoneWidth: number
  trendDelta: string
  trendDirection: 'up' | 'down' | 'flat'
  trendStatus: 'warning' | 'success' | 'neutral'
  priorValue: string
  clinicalNote: string
}

Files installed (2)

  • src/app/components/blocks/lab-results-viewer/lab-results-viewer.component.ts29.3 kB
    // Quest and Labcorp style diagnostic lab results report viewer: patient metadata header,
    // clinical attention banner, grouped test panels (Lipid, Glucose, Renal) with LOINC codes,
    // reference range linear scale bars, historical 6-month trend deltas, and doctor clinical
    // impression card with electronic verification. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      AlertTriangle,
      CheckCircle2,
      FileCheck2,
      FileDown,
      FlaskConical,
      LucideAngularModule,
      MapPin,
      Printer,
      Search,
      Share2,
      ShieldCheck,
      Stethoscope,
      TrendingDown,
      TrendingUp,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'
    
    export type TestStatus = 'Normal' | 'High' | 'Low'
    
    export interface LabTest {
      id: string
      name: string
      panel: 'lipid' | 'metabolic' | 'renal'
      panelLabel: string
      loinc: string
      value: number
      unit: string
      referenceRangeText: string
      status: TestStatus
      scaleMin: number
      scaleMax: number
      normalMin: number
      normalMax: number
      markerPercent: number
      rangeZoneLeft: number
      rangeZoneWidth: number
      trendDelta: string
      trendDirection: 'up' | 'down' | 'flat'
      trendStatus: 'warning' | 'success' | 'neutral'
      priorValue: string
      clinicalNote: string
    }
    
    const patientInfo = {
      name: 'Sarah Jenkins',
      mrn: 'MRN-9482104',
      dob: 'Apr 12, 1984 (Age 42)',
      sex: 'Female',
      fasting: 'Fasting (12 hours)',
      accession: 'Q-94827104',
      orderedBy: 'Dr. Marcus Thorne, MD, FACP',
      facility: 'Quest Diagnostics Central Lab #842',
      facilityAddress: '100 Clifton Blvd, Clifton, NJ 07012 (CLIA: 31D0691234)',
      collectedDate: 'Aug 18, 2026 · 08:30 AM EDT',
      reportedDate: 'Aug 18, 2026 · 02:45 PM EDT',
      specimen: 'Serum & Plasma (Gold SST / Lavender EDTA)',
    }
    
    const defaultTests: LabTest[] = [
      {
        id: 'chol-total',
        name: 'Total Cholesterol',
        panel: 'lipid',
        panelLabel: 'Lipid Panel (Cardiovascular Risk)',
        loinc: '2093-3',
        value: 215,
        unit: 'mg/dL',
        referenceRangeText: '< 200 mg/dL',
        status: 'High',
        scaleMin: 100,
        scaleMax: 300,
        normalMin: 100,
        normalMax: 200,
        markerPercent: 57.5,
        rangeZoneLeft: 0,
        rangeZoneWidth: 50,
        trendDelta: '+12 mg/dL vs 6 mos ago',
        trendDirection: 'up',
        trendStatus: 'warning',
        priorValue: '203 mg/dL (Feb 2026)',
        clinicalNote: 'Mild elevation. Suggests elevated total circulating atherogenic lipoprotein particles.',
      },
      {
        id: 'chol-ldl',
        name: 'LDL Cholesterol (Calculated)',
        panel: 'lipid',
        panelLabel: 'Lipid Panel (Cardiovascular Risk)',
        loinc: '13457-7',
        value: 138,
        unit: 'mg/dL',
        referenceRangeText: '< 100 mg/dL',
        status: 'High',
        scaleMin: 50,
        scaleMax: 200,
        normalMin: 50,
        normalMax: 100,
        markerPercent: 58.7,
        rangeZoneLeft: 0,
        rangeZoneWidth: 33.3,
        trendDelta: '+18 mg/dL vs 6 mos ago',
        trendDirection: 'up',
        trendStatus: 'warning',
        priorValue: '120 mg/dL (Feb 2026)',
        clinicalNote: 'Above primary prevention target (< 100 mg/dL). Lifestyle modifications indicated.',
      },
      {
        id: 'chol-hdl',
        name: 'HDL Cholesterol',
        panel: 'lipid',
        panelLabel: 'Lipid Panel (Cardiovascular Risk)',
        loinc: '2085-9',
        value: 58,
        unit: 'mg/dL',
        referenceRangeText: '≥ 50 mg/dL',
        status: 'Normal',
        scaleMin: 20,
        scaleMax: 100,
        normalMin: 50,
        normalMax: 100,
        markerPercent: 47.5,
        rangeZoneLeft: 37.5,
        rangeZoneWidth: 62.5,
        trendDelta: '+2 mg/dL vs 6 mos ago',
        trendDirection: 'up',
        trendStatus: 'success',
        priorValue: '56 mg/dL (Feb 2026)',
        clinicalNote: 'Desirable cardio-protective level for adult female population.',
      },
      {
        id: 'triglycerides',
        name: 'Triglycerides',
        panel: 'lipid',
        panelLabel: 'Lipid Panel (Cardiovascular Risk)',
        loinc: '2571-8',
        value: 142,
        unit: 'mg/dL',
        referenceRangeText: '< 150 mg/dL',
        status: 'Normal',
        scaleMin: 50,
        scaleMax: 250,
        normalMin: 50,
        normalMax: 150,
        markerPercent: 46.0,
        rangeZoneLeft: 0,
        rangeZoneWidth: 50,
        trendDelta: '-15 mg/dL vs 6 mos ago',
        trendDirection: 'down',
        trendStatus: 'success',
        priorValue: '157 mg/dL (Feb 2026)',
        clinicalNote: 'Improved from borderline threshold into normal reference zone.',
      },
      {
        id: 'glucose-fasting',
        name: 'Fasting Glucose',
        panel: 'metabolic',
        panelLabel: 'Glucose Metabolism & Glycemic Control',
        loinc: '1558-6',
        value: 92,
        unit: 'mg/dL',
        referenceRangeText: '70 – 99 mg/dL',
        status: 'Normal',
        scaleMin: 50,
        scaleMax: 150,
        normalMin: 70,
        normalMax: 99,
        markerPercent: 42.0,
        rangeZoneLeft: 20,
        rangeZoneWidth: 29,
        trendDelta: '-3 mg/dL vs 6 mos ago',
        trendDirection: 'down',
        trendStatus: 'success',
        priorValue: '95 mg/dL (Feb 2026)',
        clinicalNote: 'Normal euglycemic fasting state. No prediabetes indicators.',
      },
      {
        id: 'egfr-renal',
        name: 'eGFR (Kidney Function)',
        panel: 'renal',
        panelLabel: 'Renal Function & Filtration',
        loinc: '33914-3',
        value: 104,
        unit: 'mL/min/1.73m²',
        referenceRangeText: '≥ 60 mL/min/1.73m²',
        status: 'Normal',
        scaleMin: 30,
        scaleMax: 130,
        normalMin: 60,
        normalMax: 130,
        markerPercent: 74.0,
        rangeZoneLeft: 30,
        rangeZoneWidth: 70,
        trendDelta: '+1 mL/min vs 6 mos ago',
        trendDirection: 'up',
        trendStatus: 'success',
        priorValue: '103 mL/min (Feb 2026)',
        clinicalNote: 'Calculated using CKD-EPI 2021 equation. Robust renal clearance performance.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-lab-results-viewer, [ui-lab-results-viewer]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiSeparatorComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
        UiTabsComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <div data-slot="lab-results-viewer" [class]="rootClass">
          <!-- Header: Diagnostic Pathology Report -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-header class="p-5 pb-4 sm:p-6 sm:pb-4">
              <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
                <div class="space-y-1.5">
                  <div class="flex flex-wrap items-center gap-2">
                    <span ui-badge variant="outline" class="gap-1.5 font-mono text-xs">
                      <lucide-icon [img]="FlaskConical" class="text-primary size-3.5" />
                      {{ patient.accession }}
                    </span>
                    <span ui-badge variant="success" class="gap-1 text-xs">
                      <lucide-icon [img]="FileCheck2" class="size-3" />
                      Final Diagnostic Report
                    </span>
                  </div>
                  <h3 ui-card-title class="text-foreground text-xl font-semibold tracking-tight sm:text-2xl">
                    Comprehensive Metabolic Panel & Lipid Panel
                  </h3>
                  <p ui-card-description class="text-muted-foreground text-xs sm:text-sm">
                    Clinical chemistry and lipid risk profiling conducted by {{ patient.facility }}
                  </p>
                </div>
    
                <div class="flex flex-wrap items-center gap-2 pt-1 lg:pt-0">
                  <button
                    ui-button
                    aria-label="Download attachment"
                    variant="outline"
                    size="sm"
                    class="gap-1.5 text-xs shadow-xs"
                    [disabled]="isDownloadingPdf()"
                    (click)="handleDownloadPdf()"
                  >
                    <lucide-icon [img]="FileDown" class="size-4" />
                    {{ isDownloadingPdf() ? 'Generating PDF...' : 'Download Official PDF' }}
                  </button>
                  <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs shadow-xs" (click)="handleShare()">
                    <lucide-icon [img]="Share2" class="size-4" />
                    {{ isCopiedShare() ? 'Link Copied!' : 'Share with Doctor' }}
                  </button>
                  <button
                    ui-button
                    variant="ghost"
                    size="sm"
                    class="text-muted-foreground hover:text-foreground hidden sm:inline-flex"
                    (click)="handlePrint()"
                  >
                    <lucide-icon [img]="Printer" class="size-4" />
                    <span class="sr-only">Print Report</span>
                  </button>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="p-5 pt-0 sm:p-6 sm:pt-0">
              <ui-separator class="mb-4" />
              <dl class="grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2 lg:grid-cols-4">
                <div class="space-y-0.5">
                  <dt class="text-muted-foreground text-xs font-medium">Patient Details</dt>
                  <dd class="text-foreground text-xs font-semibold sm:text-sm">{{ patient.name }}</dd>
                  <p class="text-muted-foreground text-xs">{{ patient.sex }} · {{ patient.dob }}</p>
                </div>
                <div class="space-y-0.5">
                  <dt class="text-muted-foreground text-xs font-medium">Ordering Physician</dt>
                  <dd class="text-foreground text-xs font-semibold sm:text-sm">{{ patient.orderedBy }}</dd>
                  <p class="text-muted-foreground text-xs">Internal Medicine & Cardiology</p>
                </div>
                <div class="space-y-0.5">
                  <dt class="text-muted-foreground text-xs font-medium">Collection & Fasting</dt>
                  <dd class="text-foreground text-xs font-semibold tabular-nums sm:text-sm">{{ patient.collectedDate }}</dd>
                  <p class="text-muted-foreground text-xs">{{ patient.fasting }}</p>
                </div>
                <div class="space-y-0.5">
                  <dt class="text-muted-foreground text-xs font-medium">Testing Facility</dt>
                  <dd class="text-foreground text-xs font-semibold sm:text-sm">{{ patient.facility }}</dd>
                  <p class="text-muted-foreground text-xs">CLIA ID: 31D0691234 · CAP Accredited</p>
                </div>
              </dl>
            </div>
          </div>
    
          <!-- Overall Summary Banner -->
          <div
            class="border-warning/30 bg-warning/10 text-foreground flex flex-col gap-4 rounded-xl border p-4 sm:flex-row sm:items-center sm:justify-between sm:p-5"
          >
            <div class="flex items-start gap-3.5">
              <div
                class="border-warning/30 bg-warning/20 text-warning mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg border"
              >
                <lucide-icon [img]="AlertTriangle" class="size-5" />
              </div>
              <div class="space-y-1">
                <div class="flex flex-wrap items-center gap-2">
                  <h3 class="text-foreground text-sm font-semibold sm:text-base">
                    14 tests normal · 2 tests out of reference range
                  </h3>
                  <span ui-badge variant="warning" class="text-xs font-medium">Clinical Review Advised</span>
                </div>
                <p class="text-muted-foreground text-xs leading-relaxed sm:text-sm">
                  Total Cholesterol and LDL Cholesterol exceed recommended reference thresholds. Fasting glucose metabolism
                  and renal function (eGFR) remain optimal.
                </p>
              </div>
            </div>
            <div class="flex shrink-0 flex-wrap items-center gap-2 sm:self-center">
              <div
                class="border-border bg-card/90 flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs shadow-2xs"
              >
                <span class="bg-success size-2 rounded-full"></span>
                <span class="text-muted-foreground font-medium">In Range:</span>
                <span class="text-foreground font-semibold tabular-nums">14</span>
              </div>
              <div
                class="border-border bg-card/90 flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs shadow-2xs"
              >
                <span class="bg-warning size-2 rounded-full"></span>
                <span class="text-muted-foreground font-medium">Out of Range:</span>
                <span class="text-warning font-semibold tabular-nums">2</span>
              </div>
            </div>
          </div>
    
          <!-- Interactive Filtering & Search Header -->
          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
            <ui-tabs [value]="activeTab()" (valueChange)="setActiveTab($event)" class="w-full sm:w-auto">
              <ui-tabs-list class="grid grid-cols-4 sm:inline-flex">
                <button ui-tabs-trigger value="all" class="text-xs font-medium">All ({{ tests().length }})</button>
                <button ui-tabs-trigger value="out-of-range" class="text-xs font-medium">
                  Out of Range ({{ outOfRangeCount() }})
                </button>
                <button ui-tabs-trigger value="lipid" class="text-xs font-medium">Lipid (4)</button>
                <button ui-tabs-trigger value="metabolic-renal" class="text-xs font-medium">Metabolic & Renal (2)</button>
              </ui-tabs-list>
            </ui-tabs>
    
            <div class="relative w-full sm:w-64">
              <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-3 size-3.5 -translate-y-1/2" />
              <input
                [value]="searchQuery()"
                (input)="searchQuery.set($any($event.target).value)"
                type="text"
                placeholder="Filter tests or LOINC..."
                class="border-border bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8.5 w-full rounded-lg border pr-3 pl-9 text-xs transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
              />
            </div>
          </div>
    
          <!-- Diagnostic Panels & Test Results Table -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-header class="border-border border-b p-4 sm:px-6">
              <div class="flex items-center justify-between">
                <div>
                  <h3 ui-card-title class="text-base font-semibold">Laboratory Test Panels</h3>
                  <p ui-card-description class="text-xs">
                    Quantitative measurements, standard LOINC reference intervals, and 6-month historical trend comparison.
                  </p>
                </div>
                <span class="text-muted-foreground hidden font-mono text-xs tabular-nums sm:inline-block">
                  Showing {{ filteredTests().length }} of {{ tests().length }} tests
                </span>
              </div>
            </div>
    
            <div ui-card-content class="p-0">
              <div class="overflow-x-auto">
                <table ui-table>
                  <thead ui-table-header class="bg-muted/40">
                    <tr ui-table-row>
                      <th ui-table-head class="min-w-[180px] text-xs font-semibold">Test Name & Code</th>
                      <th ui-table-head class="min-w-[120px] text-xs font-semibold">Measured Value</th>
                      <th ui-table-head class="w-24 text-xs font-semibold">Flag</th>
                      <th ui-table-head class="min-w-[120px] text-xs font-semibold">Reference Range</th>
                      <th ui-table-head class="min-w-[210px] text-xs font-semibold">Visual Scale</th>
                      <th ui-table-head class="min-w-[170px] text-right text-xs font-semibold">Historical 6-Mo Delta</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (test of filteredTests(); track test.id) {
                      <tr ui-table-row [class]="testRowClass(test)">
                        <td ui-table-cell class="py-3.5">
                          <div class="space-y-0.5">
                            <p class="text-foreground text-xs font-semibold sm:text-sm">{{ test.name }}</p>
                            <div class="flex items-center gap-1.5">
                              <span class="text-muted-foreground font-mono text-xs">LOINC: {{ test.loinc }}</span>
                              <span class="text-muted-foreground text-xs">{{ test.panelLabel }}</span>
                            </div>
                          </div>
                        </td>
                        <td ui-table-cell class="py-3.5">
                          <div class="flex items-baseline gap-1">
                            <span [class]="valueClass(test)">{{ test.value }}</span>
                            <span class="text-muted-foreground text-xs font-medium">{{ test.unit }}</span>
                          </div>
                        </td>
                        <td ui-table-cell class="py-3.5">
                          <span
                            ui-badge
                            [variant]="test.status === 'High' ? 'warning' : 'success'"
                            class="px-2 py-0.5 text-xs font-semibold"
                          >
                            {{ test.status }}
                          </span>
                        </td>
                        <td ui-table-cell class="py-3.5">
                          <span class="text-foreground font-mono text-xs font-medium tabular-nums">
                            {{ test.referenceRangeText }}
                          </span>
                        </td>
                        <td ui-table-cell class="py-3.5">
                          <div class="w-full max-w-[210px] space-y-1">
                            <div class="bg-muted/80 relative flex h-2.5 w-full items-center overflow-visible rounded-full">
                              <div
                                class="border-success/40 bg-success/25 absolute h-full rounded-full border-x"
                                [style.left.%]="test.rangeZoneLeft"
                                [style.width.%]="test.rangeZoneWidth"
                              ></div>
                              <div
                                class="absolute flex -translate-x-1/2 flex-col items-center"
                                [style.left.%]="test.markerPercent"
                              >
                                <span [class]="markerClass(test)"></span>
                              </div>
                            </div>
                            <div
                              class="text-muted-foreground flex items-center justify-between font-mono text-xs tabular-nums"
                            >
                              <span>{{ test.scaleMin }}</span>
                              <span class="text-success text-xs font-medium">Normal</span>
                              <span>{{ test.scaleMax }}</span>
                            </div>
                          </div>
                        </td>
                        <td ui-table-cell class="py-3.5 text-right">
                          <div class="flex flex-col items-end gap-0.5">
                            <div class="flex items-center gap-1">
                              @if (test.trendDirection === 'up') {
                                <lucide-icon [img]="TrendingUp" [class]="trendIconClass(test)" />
                              } @else {
                                <lucide-icon [img]="TrendingDown" [class]="trendIconClass(test)" />
                              }
                              <span [class]="trendDeltaClass(test)">{{ test.trendDelta }}</span>
                            </div>
                            <span class="text-muted-foreground text-xs tabular-nums">Prior: {{ test.priorValue }}</span>
                          </div>
                        </td>
                      </tr>
                    }
                    @if (filteredTests().length === 0) {
                      <tr ui-table-row>
                        <td ui-table-cell colspan="6" class="py-8 text-center">
                          <p class="text-muted-foreground text-xs">
                            No laboratory test records matched your filter criteria.
                          </p>
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
    
          <!-- Doctor's Clinical Impression Card -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-header class="p-5 pb-3 sm:p-6 sm:pb-4">
              <div class="flex flex-wrap items-center justify-between gap-3">
                <div class="flex items-center gap-2.5">
                  <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
                    <lucide-icon [img]="Stethoscope" class="size-4" />
                  </div>
                  <div>
                    <h3 ui-card-title class="text-base font-semibold">Doctor's Clinical Impression & Action Plan</h3>
                    <p ui-card-description class="text-xs">Physician interpretation provided by {{ patient.orderedBy }}</p>
                  </div>
                </div>
                <span ui-badge variant="outline" class="gap-1.5 text-xs font-normal">
                  <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" />
                  Verified Electronic Signature
                </span>
              </div>
            </div>
    
            <div ui-card-content class="space-y-4 p-5 pt-0 sm:p-6 sm:pt-0">
              <div class="border-border/70 bg-muted/30 rounded-lg border p-4">
                <p class="text-foreground text-xs leading-relaxed sm:text-sm">
                  “Lipid panel shows mild elevation in LDL Cholesterol (138 mg/dL) and Total Cholesterol (215 mg/dL).
                  Fasting blood glucose (92 mg/dL) and renal filtration markers (eGFR 104 mL/min/1.73m²) demonstrate
                  outstanding stability. Recommend continuing Mediterranean dietary pattern with emphasis on soluble fiber
                  and healthy monounsaturated fats, 150 minutes of moderate aerobic exercise weekly, and repeat lipid panel
                  in 3 months. Statin therapy is not warranted at the patient's current 10-year ASCVD risk threshold.”
                </p>
              </div>
    
              <div class="space-y-2">
                <span class="text-muted-foreground text-xs font-medium tracking-wide uppercase">
                  Recommended Clinical Action Items
                </span>
                <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-3">
                  <div class="border-border bg-card flex items-start gap-2.5 rounded-lg border p-3">
                    <lucide-icon [img]="CheckCircle2" class="text-primary mt-0.5 size-4 shrink-0" />
                    <div class="space-y-0.5">
                      <p class="text-foreground text-xs font-medium">Repeat Lipid Panel</p>
                      <p class="text-muted-foreground text-xs">Schedule re-test in 90 days (Nov 2026)</p>
                    </div>
                  </div>
                  <div class="border-border bg-card flex items-start gap-2.5 rounded-lg border p-3">
                    <lucide-icon [img]="CheckCircle2" class="text-primary mt-0.5 size-4 shrink-0" />
                    <div class="space-y-0.5">
                      <p class="text-foreground text-xs font-medium">Mediterranean Diet</p>
                      <p class="text-muted-foreground text-xs">Maintain high fiber and plant sterols intake</p>
                    </div>
                  </div>
                  <div class="border-border bg-card flex items-start gap-2.5 rounded-lg border p-3">
                    <lucide-icon [img]="CheckCircle2" class="text-primary mt-0.5 size-4 shrink-0" />
                    <div class="space-y-0.5">
                      <p class="text-foreground text-xs font-medium">Cardiovascular Wellness</p>
                      <p class="text-muted-foreground text-xs">150 mins weekly moderate aerobic activity</p>
                    </div>
                  </div>
                </div>
              </div>
    
              <ui-separator />
              <div class="flex flex-col gap-2 pt-1 sm:flex-row sm:items-center sm:justify-between">
                <div class="flex items-center gap-2">
                  <div
                    class="border-border bg-muted flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-semibold"
                  >
                    MT
                  </div>
                  <div>
                    <p class="text-foreground text-xs font-semibold">{{ patient.orderedBy }}</p>
                    <p class="text-muted-foreground text-xs">NPI #1849204812 · Medical License #20MD084721</p>
                  </div>
                </div>
                <p class="text-muted-foreground font-mono text-xs tabular-nums">
                  Digitally signed on Aug 18, 2026 · 15:45 EDT
                </p>
              </div>
            </div>
          </div>
    
          <!-- Lab Facility Certification Footer -->
          <div
            class="border-border/60 text-muted-foreground flex flex-col gap-2 rounded-lg border border-dashed px-4 py-3 text-xs sm:flex-row sm:items-center sm:justify-between"
          >
            <div class="flex items-center gap-1.5">
              <lucide-icon [img]="MapPin" class="size-3.5 shrink-0" />
              <span>{{ patient.facilityAddress }}</span>
            </div>
            <div class="flex items-center gap-2 font-mono text-xs">
              <span>Specimen: {{ patient.specimen }}</span>
              <span>·</span>
              <span>CLIA Certified</span>
            </div>
          </div>
        </div>
      `,
    })
    export class UiLabResultsViewerComponent {
      protected readonly AlertTriangle = AlertTriangle
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly FileCheck2 = FileCheck2
      protected readonly FileDown = FileDown
      protected readonly FlaskConical = FlaskConical
      protected readonly MapPin = MapPin
      protected readonly Printer = Printer
      protected readonly Search = Search
      protected readonly Share2 = Share2
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Stethoscope = Stethoscope
      protected readonly TrendingDown = TrendingDown
      protected readonly TrendingUp = TrendingUp
    
      protected readonly patient = patientInfo
    
      @Input() set initialTab(v: string) {
        this.activeTab.set(v ?? 'all')
      }
      @Input() set initialSearch(v: string) {
        this.searchQuery.set(v ?? '')
      }
      @Input('class') className?: string
    
      readonly tests = signal<LabTest[]>([...defaultTests])
      readonly activeTab = signal('all')
      readonly searchQuery = signal('')
      readonly isCopiedShare = signal(false)
      readonly isDownloadingPdf = signal(false)
      private shareTimer: ReturnType<typeof setTimeout> | undefined
      private pdfTimer: ReturnType<typeof setTimeout> | undefined
    
      readonly filteredTests = computed(() => {
        const tab = this.activeTab()
        const q = this.searchQuery().toLowerCase().trim()
        return this.tests().filter((test) => {
          if (tab === 'out-of-range' && test.status === 'Normal') return false
          if (tab === 'lipid' && test.panel !== 'lipid') return false
          if (tab === 'metabolic-renal' && test.panel !== 'metabolic' && test.panel !== 'renal') return false
          if (q) {
            return (
              test.name.toLowerCase().includes(q) ||
              test.loinc.toLowerCase().includes(q) ||
              test.panelLabel.toLowerCase().includes(q)
            )
          }
          return true
        })
      })
    
      readonly outOfRangeCount = computed(() => this.tests().filter((t) => t.status !== 'Normal').length)
    
      get rootClass(): string {
        return cn('text-foreground w-full space-y-6', this.className)
      }
    
      setActiveTab(value: string): void {
        this.activeTab.set(value)
      }
    
      testRowClass(test: LabTest): string {
        return cn('transition-colors', test.status === 'High' && 'bg-warning/5 hover:bg-warning/10')
      }
    
      valueClass(test: LabTest): string {
        return cn(
          'text-base font-semibold tabular-nums sm:text-lg',
          test.status === 'High' && 'text-warning font-semibold',
          test.status === 'Normal' && 'text-foreground',
        )
      }
    
      markerClass(test: LabTest): string {
        return cn(
          'border-background size-3.5 rounded-full border-2 shadow-xs transition-transform hover:scale-125',
          test.status === 'High' && 'bg-warning ring-warning/30 ring-2',
          test.status === 'Normal' && 'bg-success ring-success/30 ring-2',
          test.status === 'Low' && 'bg-info ring-info/30 ring-2',
        )
      }
    
      trendIconClass(test: LabTest): string {
        return cn(
          'size-3.5',
          test.trendStatus === 'warning' && 'text-warning',
          test.trendStatus === 'success' && 'text-success',
        )
      }
    
      trendDeltaClass(test: LabTest): string {
        return cn(
          'text-xs font-semibold tabular-nums',
          test.trendStatus === 'warning' && 'text-warning',
          test.trendStatus === 'success' && 'text-foreground',
        )
      }
    
      handleShare(): void {
        try {
          void navigator.clipboard?.writeText(
            `Lab Results for ${patientInfo.name} (${patientInfo.accession}) - ${patientInfo.collectedDate}`,
          )
          this.isCopiedShare.set(true)
          clearTimeout(this.shareTimer)
          this.shareTimer = setTimeout(() => this.isCopiedShare.set(false), 2000)
        } catch {
          // Clipboard fallback
        }
      }
    
      handleDownloadPdf(): void {
        if (this.isDownloadingPdf()) return
        this.isDownloadingPdf.set(true)
        clearTimeout(this.pdfTimer)
        this.pdfTimer = setTimeout(() => this.isDownloadingPdf.set(false), 1500)
      }
    
      handlePrint(): void {
        if (typeof window !== 'undefined') window.print()
      }
    }
    
  • src/app/components/blocks/lab-results-viewer/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/lab-results-viewer.json