Prescription Refill Manager
blockhealthcarePharmacy patient portal for managing active prescriptions, refill requests, dosage schedules, delivery tracking, and preferred pharmacy details with an integrated refill order dialog.
Installation
$pnpm dlx uipkge-ng@latest add prescription-refill-manager$npx uipkge-ng@latest add prescription-refill-manager$yarn dlx uipkge-ng@latest add prescription-refill-manager$bunx uipkge-ng@latest add prescription-refill-managerManifest:
https://uipkge.dev/r/angular/prescription-refill-manager.jsonInstalls to:src/app/components/blocks/prescription-refill-manager/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
Prescriptioninterface Prescription {
id: string
rxNumber: string
name: string
genericFor: string
dosageForm: string
instructions: string
refillsRemaining: number
totalRefills: number
lastFilledDate: string
nextDueDate: string
isDue: boolean
prescriber: { name: string; specialty: string; clinic: string; phone: string }
status: PrescriptionStatus
daysSupply: number
copayEst: number
ndc: string
packageTracking?: {
carrier: string
trackingNumber: string
status: string
estimatedArrival: string
steps: { title: string; time: string; done: boolean; current?: boolean }[]
}
}Pharmacyinterface Pharmacy {
id: string
name: string
subtitle: string
address: string
phone: string
fax: string
hours: string
isDriveThru: boolean
}TransferFormStateinterface TransferFormState {
pharmacyName: string
phone: string
rxNumber: string
medicationName: string
notes: string
}npm dependencies
Files installed (2)
src/app/components/blocks/prescription-refill-manager/prescription-refill-manager.component.ts56.5 kB
// Pharmacy patient portal: active prescriptions, refill requests, dosage schedules, // delivery tracking, and preferred pharmacy details with integrated dialogs. // Consolidates the 7 Vue files (manager + table + 5 dialogs + data + types) into one // standalone Angular block. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { AlertCircle, ArrowLeftRight, Building2, Calendar, Check, CheckCircle2, Clock, FileText, Info, LucideAngularModule, MapPin, MoreHorizontal, Phone, Pill, Plus, RefreshCw, Search, ShieldAlert, Store, Truck, } 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, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTextareaComponent } from '@/ui/textarea/textarea.component' export type PrescriptionStatus = 'ready' | 'needs_auth' | 'in_transit' | 'active' export interface Prescription { id: string rxNumber: string name: string genericFor: string dosageForm: string instructions: string refillsRemaining: number totalRefills: number lastFilledDate: string nextDueDate: string isDue: boolean prescriber: { name: string; specialty: string; clinic: string; phone: string } status: PrescriptionStatus daysSupply: number copayEst: number ndc: string packageTracking?: { carrier: string trackingNumber: string status: string estimatedArrival: string steps: { title: string; time: string; done: boolean; current?: boolean }[] } } export interface Pharmacy { id: string name: string subtitle: string address: string phone: string fax: string hours: string isDriveThru: boolean } export interface TransferFormState { pharmacyName: string phone: string rxNumber: string medicationName: string notes: string } const defaultPrescriptions: Prescription[] = [ { id: 'rx-1', rxNumber: 'RX-849201', name: 'Metformin HCl 500mg', genericFor: 'Generic for Glucophage', dosageForm: 'Oral Tablet', instructions: 'Take 1 tablet by mouth twice daily with meals', refillsRemaining: 2, totalRefills: 5, lastFilledDate: 'Jul 24, 2026', nextDueDate: 'Aug 24, 2026', isDue: true, prescriber: { name: 'Dr. Emily Vance, MD', specialty: 'Internal Medicine', clinic: 'Wilshire Medical Center', phone: '(310) 555-0142', }, status: 'ready', daysSupply: 30, copayEst: 10, ndc: '50090-0819-0', }, { id: 'rx-2', rxNumber: 'RX-849202', name: 'Lisinopril 20mg', genericFor: 'Generic for Prinivil / Zestril', dosageForm: 'Oral Tablet', instructions: 'Take 1 tablet by mouth daily in the morning', refillsRemaining: 3, totalRefills: 6, lastFilledDate: 'Jul 10, 2026', nextDueDate: 'Aug 10, 2026', isDue: true, prescriber: { name: 'Dr. Marcus Chen, MD', specialty: 'Cardiology', clinic: 'Pacific Heart & Vascular Institute', phone: '(310) 555-0188', }, status: 'ready', daysSupply: 30, copayEst: 10, ndc: '68180-0518-01', }, { id: 'rx-3', rxNumber: 'RX-774920', name: 'Albuterol HFA Inhaler', genericFor: 'ProAir HFA 90mcg Inhalation Aerosol', dosageForm: 'Inhalation Aerosol (8.5g canister)', instructions: 'Inhale 2 puffs every 4-6 hours as needed for wheezing or shortness of breath', refillsRemaining: 0, totalRefills: 3, lastFilledDate: 'Jun 15, 2026', nextDueDate: 'Aug 18, 2026', isDue: false, prescriber: { name: 'Dr. Sarah Lin, MD', specialty: 'Pulmonology', clinic: 'Westside Respiratory Clinic', phone: '(310) 555-0195', }, status: 'needs_auth', daysSupply: 30, copayEst: 15, ndc: '59310-0579-22', }, { id: 'rx-4', rxNumber: 'RX-910384', name: 'Atorvastatin Calcium 40mg', genericFor: 'Generic for Lipitor', dosageForm: 'Oral Tablet', instructions: 'Take 1 tablet by mouth once daily at bedtime', refillsRemaining: 1, totalRefills: 4, lastFilledDate: 'Aug 19, 2026', nextDueDate: 'Nov 19, 2026', isDue: false, prescriber: { name: 'Dr. Marcus Chen, MD', specialty: 'Cardiology', clinic: 'Pacific Heart & Vascular Institute', phone: '(310) 555-0188', }, status: 'in_transit', daysSupply: 90, copayEst: 20, ndc: '00071-0157-23', packageTracking: { carrier: 'USPS Priority Rx Express', trackingNumber: '9400 1118 9956 2831 4092 11', status: 'Out for Delivery', estimatedArrival: 'Today by 6:00 PM', steps: [ { title: 'Prescription Refill Ordered', time: 'Aug 19, 9:30 AM', done: true }, { title: 'Verified & Dispensed by Pharmacist', time: 'Aug 19, 2:15 PM', done: true }, { title: 'Shipped from Regional Fulfillment', time: 'Aug 20, 8:00 AM', done: true }, { title: 'Out for Delivery', time: 'Aug 21, 8:45 AM', done: true, current: true }, ], }, }, ] const defaultAvailablePharmacies: Pharmacy[] = [ { id: 'cvs-4829', name: 'CVS Pharmacy #4829', subtitle: '24-Hour Drive-Thru', address: '4500 Wilshire Blvd, Los Angeles, CA 90010', phone: '(213) 555-0198', fax: '(213) 555-0199', hours: 'Open 24 Hours · Drive-Thru Open', isDriveThru: true, }, { id: 'walgreens-1204', name: 'Walgreens Pharmacy #1204', subtitle: 'Full Service Pharmacy', address: '3724 W Olympic Blvd, Los Angeles, CA 90019', phone: '(323) 555-0144', fax: '(323) 555-0145', hours: 'Mon-Sun: 8:00 AM – 10:00 PM', isDriveThru: false, }, { id: 'kaiser-sunset', name: 'Kaiser Sunset Medical Center Pharmacy', subtitle: 'Specialty & Mail Hub', address: '4760 Sunset Blvd, Los Angeles, CA 90027', phone: '(323) 555-0177', fax: '(323) 555-0178', hours: 'Mon-Fri: 7:00 AM – 9:00 PM · Sat-Sun: 8:00 AM – 6:00 PM', isDriveThru: true, }, ] type StatusFilter = 'all' | 'ready' | 'needs_auth' | 'in_transit' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-prescription-refill-manager, [ui-prescription-refill-manager]', 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, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, UiTextareaComponent, ], template: ` <div data-slot="prescription-refill-manager" [class]="rootClass"> <!-- Header Section --> <header class="bg-card border-border flex flex-col justify-between gap-4 rounded-xl border p-5 shadow-xs sm:p-6 md:flex-row md:items-center" > <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Pill" class="size-4" /> </div> <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Prescriptions & Medications</h1> <span ui-badge variant="outline" class="border-primary/20 bg-primary/5 text-primary text-xs font-medium"> Active Care Plan </span> </div> <p class="text-muted-foreground text-sm"> Manage recurring refills, dosage schedules, and home delivery tracking. </p> </div> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button variant="outline" class="gap-1.5 text-xs font-medium" (click)="handleOpenTransfer()"> <lucide-icon [img]="ArrowLeftRight" class="size-3.5" /> Transfer Prescription </button> <button ui-button variant="default" class="gap-1.5 text-xs font-medium shadow-xs" (click)="openRefillModal()"> <lucide-icon [img]="Plus" class="size-3.5" /> Request Refill </button> </div> </header> <!-- 3 Medication Status Metric Cards --> <section class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div ui-card class="shadow-xs"> <div ui-card-content class="flex items-center justify-between p-5"> <div class="space-y-1"> <p class="text-muted-foreground text-xs font-medium">Active Prescriptions</p> <div class="flex items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">4</span> <span class="text-muted-foreground text-xs">Total on file</span> </div> <p class="text-muted-foreground text-xs">All active maintenance therapies</p> </div> <div class="bg-primary/10 text-primary flex size-11 shrink-0 items-center justify-center rounded-xl"> <lucide-icon [img]="Pill" class="size-5" /> </div> </div> </div> <div ui-card class="border-warning/30 bg-warning/5 bg-warning/10 shadow-xs"> <div ui-card-content class="flex items-center justify-between p-5"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <p class="text-warning text-xs font-semibold">Refills Ready for Order</p> <span ui-badge variant="outline" class="border-warning/40 bg-warning/15 text-warning text-xs font-medium" > 2 Action Needed </span> </div> <div class="flex items-baseline gap-2"> <span class="text-warning text-warning text-2xl font-bold tracking-tight tabular-nums">2</span> <span class="text-warning/80 text-xs">Meds eligible now</span> </div> <p class="text-warning/80 text-xs">Metformin HCl & Lisinopril due</p> </div> <div class="bg-warning/20 text-warning flex size-11 shrink-0 items-center justify-center rounded-xl"> <lucide-icon [img]="AlertCircle" class="size-5" /> </div> </div> </div> <div ui-card class="shadow-xs sm:col-span-2 lg:col-span-1"> <div ui-card-content class="flex items-center justify-between p-5"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <p class="text-muted-foreground text-xs font-medium">In Delivery / Transit</p> <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info gap-1 text-xs font-medium"> <span class="bg-info size-1.5 animate-pulse rounded-full"></span> 1 En Route </span> </div> <div class="flex items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">1</span> <span class="text-muted-foreground text-xs">Package tracked</span> </div> <p class="text-muted-foreground text-xs">Atorvastatin 40mg · USPS Priority</p> </div> <div class="bg-info/10 text-info flex size-11 shrink-0 items-center justify-center rounded-xl"> <lucide-icon [img]="Truck" class="size-5" /> </div> </div> </div> </section> <!-- Preferred Pharmacy Card --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-start gap-3"> <div class="bg-muted text-foreground mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="Store" class="text-primary size-4" /> </div> <div class="space-y-0.5"> <div class="flex flex-wrap items-center gap-2"> <h3 ui-card-title class="text-base font-semibold"> {{ activePharmacy().name }} · {{ activePharmacy().subtitle }} </h3> <span ui-badge variant="secondary" class="gap-1 text-xs font-medium"> <lucide-icon [img]="CheckCircle2" class="text-success size-3" /> Preferred Pharmacy </span> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium" > {{ activePharmacy().hours }} </span> </div> <p ui-card-description class="text-xs"> Default location for prescription fulfillment, drive-thru pick-ups, and transfers. </p> </div> </div> <button ui-button variant="outline" size="sm" class="shrink-0 gap-1.5 text-xs font-medium" (click)="handleChangePharmacy()" > <lucide-icon [img]="Building2" class="size-3.5" /> Change Pharmacy </button> </div> </div> <ui-separator /> <div ui-card-content class="pt-3"> <div class="grid grid-cols-1 gap-3 text-xs sm:grid-cols-3"> <div class="text-muted-foreground flex items-center gap-2"> <lucide-icon [img]="MapPin" class="text-primary size-3.5 shrink-0" /> <span class="text-foreground truncate font-medium">{{ activePharmacy().address }}</span> </div> <div class="text-muted-foreground flex items-center gap-2"> <lucide-icon [img]="Phone" class="text-primary size-3.5 shrink-0" /> <span >Phone: <strong class="text-foreground font-medium">{{ activePharmacy().phone }}</strong></span > </div> <div class="text-muted-foreground flex items-center gap-2"> <lucide-icon [img]="Clock" class="text-primary size-3.5 shrink-0" /> <span>Rx Counter: <strong class="text-foreground font-medium">Ready in 2 hrs</strong></span> </div> </div> </div> </div> <!-- Prescriptions List Table Section --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div> <h3 ui-card-title class="text-lg font-bold"> Active Prescriptions ({{ filteredPrescriptions().length }}) </h3> <p ui-card-description class="text-xs"> Review remaining refills, dosage instructions, and ordering eligibility. </p> </div> <div class="flex flex-wrap items-center gap-2"> <div class="relative w-full sm:w-64"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" placeholder="Search medication, Rx#, doctor..." class="h-8 w-full pr-3 pl-8 text-xs shadow-xs" /> </div> <div class="flex items-center gap-1"> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('all')" (click)="statusFilter.set('all')" > All ({{ prescriptions().length }}) </button> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('ready')" (click)="statusFilter.set('ready')" > Ready for Refill (2) </button> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('in_transit')" (click)="statusFilter.set('in_transit')" > In Transit (1) </button> <button ui-button variant="ghost" size="sm" [class]="filterButtonClass('needs_auth')" (click)="statusFilter.set('needs_auth')" > Needs Auth (1) </button> </div> </div> </div> </div> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="min-w-[220px] text-xs font-semibold">Medication & Rx#</th> <th ui-table-head class="min-w-[240px] text-xs font-semibold">Directions & Dosage</th> <th ui-table-head class="min-w-[170px] text-xs font-semibold">Refill Status</th> <th ui-table-head class="min-w-[160px] text-xs font-semibold">Fill Dates / Schedule</th> <th ui-table-head class="min-w-[180px] text-xs font-semibold">Prescribing Doctor</th> <th ui-table-head class="min-w-[150px] text-right text-xs font-semibold">Quick Action</th> </tr> </thead> <tbody ui-table-body> @for (rx of filteredPrescriptions(); track rx.id) { <tr ui-table-row [class]="prescriptionRowClass(rx)"> <td ui-table-cell class="py-4 align-top"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-semibold">{{ rx.name }}</span> </div> <p class="text-muted-foreground text-xs">{{ rx.genericFor }}</p> <div class="flex items-center gap-2 pt-0.5"> <span class="bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs font-medium" > {{ rx.rxNumber }} </span> <span class="text-muted-foreground text-xs">NDC: {{ rx.ndc }}</span> </div> </div> </td> <td ui-table-cell class="py-4 align-top"> <div class="space-y-1"> <p class="text-foreground text-xs leading-relaxed font-medium"> {{ rx.instructions }} </p> <p class="text-muted-foreground text-xs"> Form: <span class="text-foreground font-medium">{{ rx.dosageForm }}</span> · Qty: <span class="font-mono tabular-nums">{{ rx.daysSupply }} Days</span> </p> </div> </td> <td ui-table-cell class="py-4 align-top"> <div class="space-y-1.5"> @if (rx.status === 'ready') { <span ui-badge variant="outline" class="border-warning/40 bg-warning/10 text-warning gap-1 text-xs font-medium" > <lucide-icon [img]="AlertCircle" class="size-3" /> {{ rx.refillsRemaining }} refills remaining </span> } @else if (rx.status === 'needs_auth') { <span ui-badge variant="destructive" class="bg-destructive/15 text-destructive dark:text-destructive-foreground dark:bg-destructive/30 gap-1 border-transparent text-xs font-medium" > <lucide-icon [img]="ShieldAlert" class="size-3" /> 0 refills - Needs MD authorization </span> } @else if (rx.status === 'in_transit') { <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info gap-1 text-xs font-medium" > <lucide-icon [img]="Truck" class="size-3" /> In Transit · Est. Arrival Today </span> } @else { <span ui-badge variant="secondary" class="text-xs font-medium"> {{ rx.refillsRemaining }} of {{ rx.totalRefills }} remaining </span> } <p class="text-muted-foreground text-xs tabular-nums"> Est. Copay: <strong class="text-foreground">\${{ rx.copayEst }}.00</strong> </p> </div> </td> <td ui-table-cell class="py-4 align-top text-xs"> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Calendar" class="size-3 shrink-0" /> <span> Last Filled: <strong class="text-foreground font-medium tabular-nums">{{ rx.lastFilledDate }}</strong> </span> </div> <div [class]="dueDateClass(rx)"> <lucide-icon [img]="Clock" class="size-3 shrink-0" /> <span>Next Due: {{ rx.nextDueDate }}</span> </div> @if (rx.isDue) { <span class="text-warning inline-block text-xs font-medium"> Refill window open </span> } </div> </td> <td ui-table-cell class="py-4 align-top text-xs"> <div class="space-y-0.5"> <p class="text-foreground font-medium">{{ rx.prescriber.name }}</p> <p class="text-muted-foreground">{{ rx.prescriber.specialty }}</p> <p class="text-muted-foreground truncate text-xs">{{ rx.prescriber.clinic }}</p> </div> </td> <td ui-table-cell class="py-4 text-right align-top"> <div class="flex items-center justify-end gap-1.5"> @if (rx.status === 'ready') { <button ui-button variant="default" size="sm" class="h-8 gap-1 text-xs font-medium shadow-xs" (click)="openRefillModal(rx.id)" > <lucide-icon [img]="RefreshCw" class="size-3" /> Order Refill </button> } @else if (rx.status === 'needs_auth') { <button ui-button variant="outline" size="sm" class="border-destructive/30 text-destructive hover:bg-destructive/10 h-8 gap-1 text-xs font-medium" (click)="handleOpenAuthRequest(rx)" > <lucide-icon [img]="Clock" class="size-3" /> Request Auth </button> } @else if (rx.status === 'in_transit') { <button ui-button variant="secondary" size="sm" class="h-8 gap-1 text-xs font-medium" (click)="handleOpenTracking(rx)" > <lucide-icon [img]="Truck" class="text-info size-3" /> Track </button> } <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-8" [attr.aria-label]="'Prescription options for ' + rx.name" > <lucide-icon [img]="MoreHorizontal" class="size-4" /> </button> <ui-dropdown-menu-content align="end" class="w-52"> <ui-dropdown-menu-label class="text-xs font-semibold">Rx Options</ui-dropdown-menu-label> <ui-dropdown-menu-separator /> @if (rx.status === 'ready') { <ui-dropdown-menu-item class="gap-2 text-xs" (select)="openRefillModal(rx.id)"> <lucide-icon [img]="RefreshCw" class="text-primary size-3.5" /> <span>Order Refill (30/90 Days)</span> </ui-dropdown-menu-item> } @if (rx.status === 'needs_auth') { <ui-dropdown-menu-item class="text-destructive gap-2 text-xs" (select)="handleOpenAuthRequest(rx)" > <lucide-icon [img]="ShieldAlert" class="size-3.5" /> <span>Request MD Renewal</span> </ui-dropdown-menu-item> } @if (rx.packageTracking) { <ui-dropdown-menu-item class="gap-2 text-xs" (select)="handleOpenTracking(rx)"> <lucide-icon [img]="Truck" class="text-info size-3.5" /> <span>Track USPS Delivery</span> </ui-dropdown-menu-item> } <ui-dropdown-menu-item class="gap-2 text-xs" (select)="handleOpenTransfer()"> <lucide-icon [img]="ArrowLeftRight" class="size-3.5" /> <span>Transfer to Another Pharmacy</span> </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="gap-2 text-xs"> <lucide-icon [img]="FileText" class="text-muted-foreground size-3.5" /> <span>View Full Drug Monograph</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="gap-2 text-xs"> <lucide-icon [img]="Phone" class="text-muted-foreground size-3.5" /> <span>Call Prescriber ({{ rx.prescriber.phone }})</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </div> </td> </tr> } </tbody> </table> </div> <div ui-card-footer class="bg-muted/30 border-border text-muted-foreground flex flex-col justify-between gap-3 border-t p-4 text-xs sm:flex-row sm:items-center" > <div class="flex items-center gap-2"> <lucide-icon [img]="Info" class="text-primary size-3.5 shrink-0" /> <span>Refills requested before 3:00 PM are processed same-day by our clinical pharmacy team.</span> </div> <div class="flex items-center gap-3"> <span>Automatic Refill Program: <strong class="text-foreground font-medium">Enabled</strong></span> </div> </div> </div> <!-- Refill Request Dialog --> <ui-dialog [open]="isRefillOpen()" (openChange)="onRefillOpenChange($event)"> @if (selectedPrescription(); as rx) { <ui-dialog-content class="sm:max-w-xl"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="RefreshCw" class="size-4" /> </div> <div> <h2 ui-dialog-title class="text-lg font-bold">Request Prescription Refill</h2> <p ui-dialog-description class="text-xs"> Select your supply duration, delivery method, and confirm your copay. </p> </div> </div> </div> @if (orderSubmitted()) { <div class="space-y-4 py-4"> <div class="border-success/30 bg-success/10 text-success space-y-2 rounded-xl border p-5 text-center"> <div class="bg-success/20 text-success mx-auto flex size-12 items-center justify-center rounded-full"> <lucide-icon [img]="CheckCircle2" class="size-6" /> </div> <h3 class="text-base font-bold">Refill Order Successfully Placed!</h3> <p class="text-success/90 mx-auto max-w-md text-xs"> Your order for <strong>{{ rx.name }}</strong> ({{ supplyDuration() }}-day supply) has been sent to {{ activePharmacy().name }}. </p> </div> <div class="bg-muted/40 text-muted-foreground space-y-1.5 rounded-lg border p-3 text-xs"> <div class="flex justify-between"> <span>Fulfillment Method:</span> <strong class="text-foreground">{{ deliveryMethod() === 'delivery' ? 'Express Home Delivery (1-2 Days)' : 'In-Store Pickup' }}</strong> </div> <div class="flex justify-between"> <span>Estimated Copay:</span> <strong class="text-foreground font-mono tabular-nums">\${{ estimatedCopay().toFixed(2) }}</strong> </div> <div class="flex justify-between"> <span>Remaining Refills After This Order:</span> <strong class="text-foreground tabular-nums">{{ rx.refillsRemaining - 1 >= 0 ? rx.refillsRemaining - 1 : 0 }}</strong> </div> </div> <div ui-dialog-footer class="pt-2"> <button ui-button variant="default" class="w-full" (click)="isRefillOpen.set(false)"> Done & Return to Prescriptions </button> </div> </div> } @else { <div class="space-y-5 py-2"> <div class="bg-muted/40 border-border space-y-3 rounded-xl border p-4"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-0.5"> <p class="text-muted-foreground text-xs font-semibold">Selected Prescription</p> <h4 class="text-foreground text-sm font-bold">{{ rx.name }}</h4> <p class="text-muted-foreground text-xs">{{ rx.genericFor }}</p> </div> <div class="space-y-0.5 text-left sm:text-right"> <span class="bg-background rounded border px-2 py-0.5 font-mono text-xs font-medium"> {{ rx.rxNumber }} </span> <p class="text-success text-xs font-medium">{{ rx.refillsRemaining }} refills remaining</p> </div> </div> </div> <div class="space-y-2"> <span class="text-foreground text-xs font-semibold">Supply Duration</span> <div class="grid grid-cols-2 gap-2"> <button ui-button [variant]="supplyDuration() === '30' ? 'default' : 'outline'" size="sm" (click)="supplyDuration.set('30')" > 30-Day Supply · \${{ rx.copayEst }}.00 </button> <button ui-button [variant]="supplyDuration() === '90' ? 'default' : 'outline'" size="sm" (click)="supplyDuration.set('90')" > 90-Day Supply · $20.00 </button> </div> </div> <div class="space-y-2"> <span class="text-foreground text-xs font-semibold">Fulfillment Method</span> <div class="grid grid-cols-2 gap-2"> <button ui-button [variant]="deliveryMethod() === 'pickup' ? 'default' : 'outline'" size="sm" class="gap-1.5" (click)="deliveryMethod.set('pickup')" > <lucide-icon [img]="Store" class="size-3.5" /> In-Store Pickup </button> <button ui-button [variant]="deliveryMethod() === 'delivery' ? 'default' : 'outline'" size="sm" class="gap-1.5" (click)="deliveryMethod.set('delivery')" > <lucide-icon [img]="Truck" class="size-3.5" /> Home Delivery </button> </div> </div> <div ui-dialog-footer class="pt-2"> <button ui-button variant="outline" (click)="isRefillOpen.set(false)">Cancel</button> <button ui-button variant="default" class="gap-1.5 shadow-xs" (click)="submitRefillOrder()"> <lucide-icon [img]="RefreshCw" class="size-3.5" /> Place Refill Order </button> </div> </div> } </ui-dialog-content> } </ui-dialog> <!-- Tracking Dialog --> <ui-dialog [open]="isTrackingOpen()" (openChange)="isTrackingOpen.set($event)"> @if (trackingPrescription(); as rx) { <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Truck" class="size-4" /> </div> <div> <h2 ui-dialog-title class="text-lg font-bold">Delivery & Tracking Status</h2> <p ui-dialog-description class="text-xs">Tracking details for {{ rx.name }}</p> </div> </div> </div> @if (rx.packageTracking; as tracking) { <div class="space-y-4 py-2 text-xs"> <div class="bg-muted/40 border-border space-y-2 rounded-xl border p-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Carrier & Service:</span> <strong class="text-foreground">{{ tracking.carrier }}</strong> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Tracking Number:</span> <strong class="text-foreground font-mono">{{ tracking.trackingNumber }}</strong> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Status:</span> <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info text-xs font-semibold"> {{ tracking.status }} </span> </div> <div class="flex items-center justify-between border-t pt-1"> <span class="text-muted-foreground">Estimated Delivery:</span> <strong class="text-foreground font-bold">{{ tracking.estimatedArrival }}</strong> </div> </div> <div class="space-y-3 pl-2"> <p class="text-foreground text-xs font-semibold">Package History Timeline</p> <div class="border-border relative ml-2 space-y-4 border-l pl-4"> @for (step of tracking.steps; track step.title) { <div class="relative space-y-0.5"> <div [class]="trackingDotClass(step)"></div> <p [class]=" step.current ? 'text-info text-xs font-semibold' : 'text-foreground text-xs font-semibold' " > {{ step.title }} </p> <p class="text-muted-foreground text-xs tabular-nums">{{ step.time }}</p> </div> } </div> </div> </div> } <div ui-dialog-footer> <button ui-button variant="outline" class="w-full sm:w-auto" (click)="isTrackingOpen.set(false)"> Close Tracking </button> </div> </ui-dialog-content> } </ui-dialog> <!-- Transfer Prescription Dialog --> <ui-dialog [open]="isTransferOpen()" (openChange)="onTransferOpenChange($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="ArrowLeftRight" class="size-4" /> </div> <div> <h2 ui-dialog-title class="text-lg font-bold">Transfer Prescription</h2> <p ui-dialog-description class="text-xs"> Move an existing prescription from another pharmacy to {{ activePharmacy().name }}. </p> </div> </div> </div> @if (transferSubmitted()) { <div class="space-y-4 py-4"> <div class="border-success/30 bg-success/10 text-success space-y-2 rounded-xl border p-5 text-center"> <div class="bg-success/20 text-success mx-auto flex size-12 items-center justify-center rounded-full"> <lucide-icon [img]="CheckCircle2" class="size-6" /> </div> <h3 class="text-base font-bold">Transfer Request Submitted!</h3> <p class="text-success/90 mx-auto max-w-sm text-xs"> Our clinical pharmacy team will contact your previous pharmacy to transfer the prescription. This typically takes 24–48 hours. </p> </div> <div ui-dialog-footer> <button ui-button variant="default" class="w-full" (click)="isTransferOpen.set(false)">Done</button> </div> </div> } @else { <div class="space-y-3.5 py-2 text-xs"> <div class="space-y-1"> <label class="text-foreground text-xs font-medium">Current / Previous Pharmacy Name</label> <ui-input [value]="transferForm().pharmacyName" (valueChange)="patchTransfer({ pharmacyName: $event })" placeholder="e.g. Walgreens, Rite Aid, Walmart..." class="h-9 text-xs shadow-xs" /> </div> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1"> <label class="text-foreground text-xs font-medium">Pharmacy Phone Number</label> <ui-input [value]="transferForm().phone" (valueChange)="patchTransfer({ phone: $event })" type="tel" placeholder="(555) 000-0000" class="h-9 text-xs shadow-xs" /> </div> <div class="space-y-1"> <label class="text-foreground text-xs font-medium">Existing Rx Number (if known)</label> <ui-input [value]="transferForm().rxNumber" (valueChange)="patchTransfer({ rxNumber: $event })" placeholder="e.g. RX-123456" class="h-9 font-mono text-xs shadow-xs" /> </div> </div> <div class="space-y-1"> <label class="text-foreground text-xs font-medium">Medication Name & Strength</label> <ui-input [value]="transferForm().medicationName" (valueChange)="patchTransfer({ medicationName: $event })" placeholder="e.g. Lipitor 20mg or Levothyroxine 50mcg" class="h-9 text-xs shadow-xs" /> </div> <div class="space-y-1"> <label class="text-foreground text-xs font-medium" >Special Instructions / Prescriber Notes (Optional)</label > <ui-textarea [value]="transferForm().notes" (valueChange)="patchTransfer({ notes: $event })" [rows]="2" placeholder="Any additional notes for the receiving pharmacist..." class="text-xs shadow-xs" /> </div> <div ui-dialog-footer class="pt-2"> <button ui-button variant="outline" (click)="isTransferOpen.set(false)">Cancel</button> <button ui-button variant="default" class="gap-1.5 shadow-xs" (click)="submitTransfer()"> <lucide-icon [img]="ArrowLeftRight" class="size-3.5" /> Initiate Transfer </button> </div> </div> } </ui-dialog-content> </ui-dialog> <!-- Change Preferred Pharmacy Dialog --> <ui-dialog [open]="isChangePharmacyOpen()" (openChange)="isChangePharmacyOpen.set($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Building2" class="size-4" /> </div> <div> <h2 ui-dialog-title class="text-lg font-bold">Select Preferred Pharmacy</h2> <p ui-dialog-description class="text-xs"> Choose the location where your local orders and drive-thru pickups are routed. </p> </div> </div> </div> <div class="space-y-3 py-2 text-xs"> @for (pharmacy of availablePharmacies(); track pharmacy.id) { <div [class]="pharmacyOptionClass(pharmacy)" (click)="tempSelectedPharmacyId.set(pharmacy.id)"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-foreground font-bold">{{ pharmacy.name }}</span> @if (pharmacy.id === activePharmacy().id) { <span ui-badge variant="secondary" class="text-xs">Current</span> } </div> <p class="text-muted-foreground">{{ pharmacy.address }}</p> <div class="text-muted-foreground flex items-center gap-3 pt-1"> <span >Phone: <strong class="text-foreground font-medium">{{ pharmacy.phone }}</strong></span > <span>{{ pharmacy.hours }}</span> </div> </div> <div class="pt-0.5"> <div [class]="pharmacyRadioClass(pharmacy)"> @if (tempSelectedPharmacyId() === pharmacy.id) { <div class="bg-background size-1.5 rounded-full"></div> } </div> </div> </div> } </div> <div ui-dialog-footer class="pt-2"> <button ui-button variant="outline" (click)="isChangePharmacyOpen.set(false)">Cancel</button> <button ui-button variant="default" (click)="savePharmacyChange()">Save Preferred Pharmacy</button> </div> </ui-dialog-content> </ui-dialog> <!-- Request MD Authorization Dialog --> <ui-dialog [open]="isAuthRequestOpen()" (openChange)="isAuthRequestOpen.set($event)"> @if (authRequestedRx(); as rx) { <ui-dialog-content class="sm:max-w-md"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="ShieldAlert" class="size-4" /> </div> <div> <h2 ui-dialog-title class="text-lg font-bold">Request Prescriber Authorization</h2> <p ui-dialog-description class="text-xs">Renew zero-refill prescription with your physician.</p> </div> </div> </div> @if (authRequestSubmitted()) { <div class="space-y-3 py-4 text-center"> <div class="bg-success/20 text-success mx-auto flex size-12 items-center justify-center rounded-full"> <lucide-icon [img]="CheckCircle2" class="size-6" /> </div> <h3 class="text-foreground text-sm font-bold">Authorization Request Sent!</h3> <p class="text-muted-foreground text-xs"> A renewal request for <strong>{{ rx.name }}</strong> has been electronically transmitted to <strong>{{ rx.prescriber.name }}</strong >. </p> <div ui-dialog-footer class="pt-2"> <button ui-button variant="default" class="w-full" (click)="isAuthRequestOpen.set(false)"> Done </button> </div> </div> } @else { <div class="space-y-4 py-2 text-xs"> <div class="bg-muted/40 space-y-1.5 rounded-xl border p-3.5"> <p class="text-foreground font-bold">{{ rx.name }}</p> <p class="text-muted-foreground">{{ rx.genericFor }}</p> <p class="text-muted-foreground"> Prescribing Physician: <strong class="text-foreground">{{ rx.prescriber.name }}</strong> </p> <p class="text-muted-foreground">Clinic: {{ rx.prescriber.clinic }} ({{ rx.prescriber.phone }})</p> </div> <p class="text-muted-foreground leading-relaxed"> Because this prescription has 0 refills remaining, we will send an electronic request (e-Prescription Renewal) directly to your doctor's office. </p> <div ui-dialog-footer> <button ui-button variant="outline" (click)="isAuthRequestOpen.set(false)">Cancel</button> <button ui-button variant="default" class="gap-1.5" (click)="authRequestSubmitted.set(true)"> <lucide-icon [img]="Check" class="size-3.5" /> Send Renewal Request </button> </div> </div> } </ui-dialog-content> } </ui-dialog> </div> `, }) export class UiPrescriptionRefillManagerComponent { protected readonly AlertCircle = AlertCircle protected readonly ArrowLeftRight = ArrowLeftRight protected readonly Building2 = Building2 protected readonly Calendar = Calendar protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly FileText = FileText protected readonly Info = Info protected readonly MapPin = MapPin protected readonly MoreHorizontal = MoreHorizontal protected readonly Phone = Phone protected readonly Pill = Pill protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly Search = Search protected readonly ShieldAlert = ShieldAlert protected readonly Store = Store protected readonly Truck = Truck @Input('class') className?: string @Input() set initialPharmacyId(v: string | undefined) { const found = this.availablePharmacies().find((p) => p.id === v) if (found) this.activePharmacy.set(found) } readonly prescriptions = signal<Prescription[]>(defaultPrescriptions.map((p) => ({ ...p }))) readonly availablePharmacies = signal<Pharmacy[]>(defaultAvailablePharmacies.map((p) => ({ ...p }))) readonly activePharmacy = signal<Pharmacy>({ ...defaultAvailablePharmacies[0] }) readonly searchQuery = signal('') readonly statusFilter = signal<StatusFilter>('all') readonly isRefillOpen = signal(false) readonly selectedPrescriptionId = signal('rx-1') readonly supplyDuration = signal<'30' | '90'>('90') readonly deliveryMethod = signal<'pickup' | 'delivery'>('delivery') readonly orderSubmitted = signal(false) readonly isTrackingOpen = signal(false) readonly trackingPrescription = signal<Prescription | null>(null) readonly isTransferOpen = signal(false) readonly transferForm = signal<TransferFormState>({ pharmacyName: '', phone: '', rxNumber: '', medicationName: '', notes: '', }) readonly transferSubmitted = signal(false) readonly isChangePharmacyOpen = signal(false) readonly tempSelectedPharmacyId = signal(defaultAvailablePharmacies[0].id) readonly isAuthRequestOpen = signal(false) readonly authRequestedRx = signal<Prescription | null>(null) readonly authRequestSubmitted = signal(false) readonly filteredPrescriptions = computed(() => { const q = this.searchQuery().toLowerCase() const f = this.statusFilter() return this.prescriptions().filter((rx) => { const matchesSearch = rx.name.toLowerCase().includes(q) || rx.rxNumber.toLowerCase().includes(q) || rx.genericFor.toLowerCase().includes(q) || rx.prescriber.name.toLowerCase().includes(q) if (!matchesSearch) return false if (f === 'all') return true return rx.status === f }) }) readonly selectedPrescription = computed<Prescription>(() => { return this.prescriptions().find((rx) => rx.id === this.selectedPrescriptionId()) ?? this.prescriptions()[0] }) readonly estimatedCopay = computed(() => this.supplyDuration() === '90' ? 20.0 : this.selectedPrescription().copayEst, ) get rootClass(): string { return cn('bg-background text-foreground w-full space-y-6', this.className) } filterButtonClass(filter: StatusFilter): string { const active = this.statusFilter() === filter return cn( 'h-8 text-xs font-medium', active && 'bg-accent text-accent-foreground', active && filter === 'ready' && 'text-warning', active && filter === 'needs_auth' && 'text-destructive', ) } prescriptionRowClass(rx: Prescription): string { return cn('transition-colors', rx.status === 'ready' && 'bg-warning/[0.02] bg-warning/[0.04]') } dueDateClass(rx: Prescription): string { return cn( 'flex items-center gap-1.5 font-medium tabular-nums', rx.isDue ? 'text-warning font-semibold' : 'text-muted-foreground', ) } trackingDotClass(step: { done: boolean; current?: boolean }): string { return cn( 'ring-background absolute top-0.5 -left-[23px] flex size-3.5 items-center justify-center rounded-full ring-2', step.current ? 'bg-info ring-info animate-pulse' : step.done ? 'bg-primary text-primary-foreground' : 'bg-muted border', ) } pharmacyOptionClass(pharmacy: Pharmacy): string { return cn( 'flex cursor-pointer items-start justify-between rounded-xl border p-3.5 transition-colors', this.tempSelectedPharmacyId() === pharmacy.id ? 'border-primary bg-primary/5 ring-primary ring-1' : 'border-border bg-card hover:bg-muted/40', ) } pharmacyRadioClass(pharmacy: Pharmacy): string { return cn( 'flex size-4 items-center justify-center rounded-full border', this.tempSelectedPharmacyId() === pharmacy.id ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/40', ) } openRefillModal(prescriptionId?: string): void { if (prescriptionId) { this.selectedPrescriptionId.set(prescriptionId) } else { const readyMed = this.prescriptions().find((r) => r.status === 'ready') this.selectedPrescriptionId.set(readyMed ? readyMed.id : this.prescriptions()[0].id) } this.orderSubmitted.set(false) this.isRefillOpen.set(true) } onRefillOpenChange(open: boolean): void { this.isRefillOpen.set(open) if (!open) this.orderSubmitted.set(false) } submitRefillOrder(): void { this.orderSubmitted.set(true) this.prescriptions.update((prev) => prev.map((rx) => rx.id === this.selectedPrescriptionId() ? { ...rx, refillsRemaining: Math.max(0, rx.refillsRemaining - 1) } : rx, ), ) } handleOpenTracking(rx: Prescription): void { this.trackingPrescription.set(rx) this.isTrackingOpen.set(true) } handleOpenAuthRequest(rx: Prescription): void { this.authRequestedRx.set(rx) this.authRequestSubmitted.set(false) this.isAuthRequestOpen.set(true) } handleOpenTransfer(): void { this.transferForm.set({ pharmacyName: '', phone: '', rxNumber: '', medicationName: '', notes: '' }) this.transferSubmitted.set(false) this.isTransferOpen.set(true) } onTransferOpenChange(open: boolean): void { this.isTransferOpen.set(open) if (!open) this.transferSubmitted.set(false) } patchTransfer(patch: Partial<TransferFormState>): void { this.transferForm.update((prev) => ({ ...prev, ...patch })) } submitTransfer(): void { this.transferSubmitted.set(true) } handleChangePharmacy(): void { this.tempSelectedPharmacyId.set(this.activePharmacy().id) this.isChangePharmacyOpen.set(true) } savePharmacyChange(): void { const chosen = this.availablePharmacies().find((p) => p.id === this.tempSelectedPharmacyId()) if (chosen) this.activePharmacy.set(chosen) this.isChangePharmacyOpen.set(false) } }src/app/components/blocks/prescription-refill-manager/index.ts0.2 kB
export { UiPrescriptionRefillManagerComponent, type Pharmacy, type Prescription, type PrescriptionStatus, type TransferFormState, } from './prescription-refill-manager.component'
Raw manifest:https://uipkge.dev/r/angular/prescription-refill-manager.json