Menu

Change language

GitHub repo

Patient Intake Form

blockhealthcare

HIPAA-compliant new patient registration and medical history questionnaire in a SectionCard. Features a 4-step Stepper (Personal Information → Medical History → Insurance & Billing → Consent & Sign), demographic inputs with biological sex and emergency contact, chronic conditions checklist with allergy details and medication textareas, insurance carrier selection with card photo upload dropzones, regulatory HIPAA disclosure accordion with electronic signature pad, and a post-submission confirmation receipt with patient ID.

Installation

$npx uipkge-ng@latest add patient-intake-form
Manifest:https://uipkge.dev/r/angular/patient-intake-form.jsonInstalls to:src/app/components/blocks/patient-intake-form/

Variants

Loading interactive previews…

Schema

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

PatientIntakeData
interface PatientIntakeData {
  firstName: string
  middleName: string
  lastName: string
  preferredName: string
  dob: string
  biologicalSex: string
  maritalStatus: string
  phone: string
  email: string
  streetAddress: string
  city: string
  state: string
  zipCode: string
  emergencyName: string
  emergencyRelationship: string
  emergencyPhone: string
  chronicConditions: string[]
  hasAllergies: boolean
  allergyDetails: string
  currentMedications: string
  pastSurgeries: string
  primaryCarePhysician: string
  clinicPhone: string
  insuranceCarrier: string
  memberId: string
  groupNumber: string
  policyholderName: string
  policyholderRelationship: string
  frontCardUploaded: boolean
  backCardUploaded: boolean
  hipaaConsent: boolean
  treatmentConsent: boolean
  telehealthConsent: boolean
  signatureName: string
  signatureDate: string
  signerRole: string
}

Files installed (2)

  • src/app/components/blocks/patient-intake-form/patient-intake-form.component.ts49.2 kB
    // HIPAA-compliant new patient registration in a SectionCard: a 4-step Stepper (Personal
    // Information → Medical History → Insurance & Billing → Consent & Sign) with demographic
    // inputs, chronic-conditions checklist, allergy/medication textareas, carrier selection with
    // card-photo dropzones, HIPAA disclosures with an electronic signature stamp, and a
    // post-submission confirmation receipt. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'
    import {
      Activity,
      AlertTriangle,
      Building2,
      Calendar,
      Check,
      ChevronLeft,
      ChevronRight,
      CreditCard,
      Download,
      FileCheck,
      FileText,
      Lock,
      LucideAngularModule,
      Phone,
      RotateCcw,
      ShieldCheck,
      Stethoscope,
      User,
      UserCheck,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiLabelComponent } from '@/ui/label/label.component'
    import { UiSectionCardComponent } from '@/ui/section-card/section-card.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiStepperComponent, type StepperStep } from '@/ui/stepper/stepper.component'
    import { UiTextareaComponent } from '@/ui/textarea/textarea.component'
    
    export interface PatientIntakeData {
      firstName: string
      middleName: string
      lastName: string
      preferredName: string
      dob: string
      biologicalSex: string
      maritalStatus: string
      phone: string
      email: string
      streetAddress: string
      city: string
      state: string
      zipCode: string
      emergencyName: string
      emergencyRelationship: string
      emergencyPhone: string
      chronicConditions: string[]
      hasAllergies: boolean
      allergyDetails: string
      currentMedications: string
      pastSurgeries: string
      primaryCarePhysician: string
      clinicPhone: string
      insuranceCarrier: string
      memberId: string
      groupNumber: string
      policyholderName: string
      policyholderRelationship: string
      frontCardUploaded: boolean
      backCardUploaded: boolean
      hipaaConsent: boolean
      treatmentConsent: boolean
      telehealthConsent: boolean
      signatureName: string
      signatureDate: string
      signerRole: string
    }
    
    const steps: StepperStep[] = [
      { id: 1, title: 'Personal Information', description: 'Demographics & Contact' },
      { id: 2, title: 'Medical History', description: 'Conditions & Medications' },
      { id: 3, title: 'Insurance & Billing', description: 'Coverage & Card Photos' },
      { id: 4, title: 'Consent & Sign', description: 'HIPAA & Authorization' },
    ]
    
    const CHRONIC_CONDITIONS = [
      { id: 'hypertension', label: 'Hypertension (High Blood Pressure)' },
      { id: 'diabetes', label: 'Diabetes (Type 1 or Type 2)' },
      { id: 'asthma', label: 'Asthma / Respiratory Conditions' },
      { id: 'heart_disease', label: 'Coronary Artery / Heart Disease' },
      { id: 'arthritis', label: 'Arthritis / Chronic Joint Pain' },
      { id: 'cancer', label: 'Cancer / Remission History' },
      { id: 'thyroid', label: 'Thyroid / Endocrine Disorder' },
      { id: 'anxiety_depression', label: 'Depression / Clinical Anxiety' },
      { id: 'kidney_disease', label: 'Chronic Kidney Disease' },
      { id: 'gerd', label: 'Acid Reflux / GERD' },
    ]
    
    const INSURANCE_CARRIERS = [
      { id: 'bcbs', label: 'BlueCross BlueShield' },
      { id: 'aetna', label: 'Aetna Health' },
      { id: 'cigna', label: 'Cigna Healthcare' },
      { id: 'united', label: 'UnitedHealthcare' },
      { id: 'medicare', label: 'Medicare Part A / B' },
      { id: 'medicaid', label: 'Medicaid' },
      { id: 'humana', label: 'Humana' },
      { id: 'kaiser', label: 'Kaiser Permanente' },
      { id: 'self_pay', label: 'Self-Pay (Uninsured)' },
    ]
    
    const DEFAULT_FORM: PatientIntakeData = {
      firstName: 'Eleanor',
      middleName: 'Rose',
      lastName: 'Vance',
      preferredName: 'Ellie',
      dob: '1988-10-14',
      biologicalSex: 'female',
      maritalStatus: 'married',
      phone: '(555) 234-8921',
      email: '[email protected]',
      streetAddress: '742 Evergreen Terrace',
      city: 'Springfield',
      state: 'OR',
      zipCode: '97477',
      emergencyName: 'Marcus Vance',
      emergencyRelationship: 'spouse',
      emergencyPhone: '(555) 987-6543',
      chronicConditions: ['hypertension', 'asthma'],
      hasAllergies: true,
      allergyDetails: 'Penicillin (Anaphylaxis/hives), Sulfa antibiotics (severe skin rash)',
      currentMedications:
        'Lisinopril 10mg PO once daily in morning, Albuterol HFA 90mcg inhaler 1-2 puffs Q4H PRN for bronchospasm, Multivitamin daily',
      pastSurgeries: 'Appendectomy (Laparoscopic, 2016), Right knee arthroscopy / meniscus debridement (2021)',
      primaryCarePhysician: 'Dr. Robert Chen, MD',
      clinicPhone: '(555) 601-2290 — Northwest Health Partners',
      insuranceCarrier: 'bcbs',
      memberId: 'XEH-902814891',
      groupNumber: 'GRP-44028',
      policyholderName: 'Eleanor Vance',
      policyholderRelationship: 'self',
      frontCardUploaded: true,
      backCardUploaded: true,
      hipaaConsent: true,
      treatmentConsent: true,
      telehealthConsent: true,
      signatureName: 'Eleanor R. Vance',
      signatureDate: '2026-08-21',
      signerRole: 'patient',
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-patient-intake-form, [ui-patient-intake-form]',
      standalone: true,
      // React renders the SectionCard itself: the host stays out of layout and `class` goes to the card.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCheckboxComponent,
        UiInputComponent,
        UiLabelComponent,
        UiSectionCardComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSeparatorComponent,
        UiStepperComponent,
        UiTextareaComponent,
      ],
      template: `
        <ui-section-card
          data-slot="patient-intake-form"
          title="New Patient Registration"
          description="HIPAA-compliant digital intake questionnaire & clinical onboarding portal."
          [class]="cardClass"
        >
          <div slot="header-action" class="flex items-center gap-2">
            <span
              ui-badge
              wrap
              variant="outline"
              class="border-success/30 bg-success/10 text-success gap-1 py-0.5 text-xs font-medium"
            >
              <lucide-icon [img]="ShieldCheck" class="size-3.5" />
              HIPAA Protected
            </span>
            <span ui-badge wrap variant="secondary" class="hidden font-mono text-xs sm:inline-flex">
              Form #PHI-2026-894
            </span>
          </div>
    
          @if (step() <= 4) {
            <ui-stepper [steps]="steps" [value]="step()" (valueChange)="onStepperInput($event)" class="mb-6" />
          }
    
          <!-- STEP 1: Personal Information -->
          @if (step() === 1) {
            <div class="space-y-6">
              <div class="border-border/80 bg-muted/40 flex items-start gap-3 rounded-lg border p-3.5 text-xs">
                <lucide-icon [img]="ShieldCheck" class="text-primary mt-0.5 size-4 shrink-0" />
                <div class="space-y-0.5">
                  <p class="text-foreground font-semibold">Protected Health Information (PHI) Notice</p>
                  <p class="text-muted-foreground leading-relaxed">
                    All legal demographics and medical history submitted through this portal are encrypted in accordance
                    with federal HIPAA Omnibus rules and directly synchronized with the clinic's Electronic Health Record
                    (EHR).
                  </p>
                </div>
              </div>
    
              <div class="space-y-3">
                <h3 class="text-foreground flex items-center gap-2 text-xs font-semibold">
                  <lucide-icon [img]="User" class="text-primary size-3.5" />
                  Legal Patient Identification
                </h3>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="pi-first-name" class="text-xs">Legal First Name *</label>
                    <ui-input
                      id="pi-first-name"
                      [value]="form().firstName"
                      (valueChange)="setField('firstName', $event)"
                      placeholder="First name"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="pi-middle-name" class="text-xs">Middle Name</label>
                    <ui-input
                      id="pi-middle-name"
                      [value]="form().middleName"
                      (valueChange)="setField('middleName', $event)"
                      placeholder="Middle name"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="pi-last-name" class="text-xs">Legal Last Name *</label>
                    <ui-input
                      id="pi-last-name"
                      [value]="form().lastName"
                      (valueChange)="setField('lastName', $event)"
                      placeholder="Last name"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="pi-preferred-name" class="text-xs">Preferred Name</label>
                    <ui-input
                      id="pi-preferred-name"
                      [value]="form().preferredName"
                      (valueChange)="setField('preferredName', $event)"
                      placeholder="Nickname / Preferred"
                      size="small"
                    />
                  </div>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="space-y-3">
                <h3 class="text-foreground flex items-center gap-2 text-xs font-semibold">
                  <lucide-icon [img]="Calendar" class="text-primary size-3.5" />
                  Birth & Clinical Demographics
                </h3>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
                  <div class="space-y-1.5">
                    <label ui-label for="pi-dob" class="text-xs">Date of Birth *</label>
                    <ui-input
                      id="pi-dob"
                      [value]="form().dob"
                      (valueChange)="setField('dob', $event)"
                      type="date"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="pi-biological-sex" class="text-xs">Biological Sex Assigned at Birth *</label>
                    <ui-select [value]="form().biologicalSex" (valueChange)="setField('biologicalSex', $event)">
                      <button ui-select-trigger id="pi-biological-sex" size="sm" class="h-8 text-xs">
                        <ui-select-value placeholder="Select sex" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="female">Female</ui-select-item>
                        <ui-select-item value="male">Male</ui-select-item>
                        <ui-select-item value="intersex">Intersex</ui-select-item>
                        <ui-select-item value="undisclosed">Prefer not to disclose</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="pi-marital-status" class="text-xs">Marital Status</label>
                    <ui-select [value]="form().maritalStatus" (valueChange)="setField('maritalStatus', $event)">
                      <button ui-select-trigger id="pi-marital-status" size="sm" class="h-8 text-xs">
                        <ui-select-value placeholder="Select status" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="single">Single</ui-select-item>
                        <ui-select-item value="married">Married</ui-select-item>
                        <ui-select-item value="divorced">Divorced</ui-select-item>
                        <ui-select-item value="widowed">Widowed</ui-select-item>
                        <ui-select-item value="other">Other</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="space-y-3">
                <h3 class="text-foreground flex items-center gap-2 text-xs font-semibold">
                  <lucide-icon [img]="Phone" class="text-primary size-3.5" />
                  Contact & Residential Address
                </h3>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  <div class="space-y-1.5">
                    <label ui-label for="pi-phone" class="text-xs">Mobile Phone Number *</label>
                    <ui-input
                      id="pi-phone"
                      [value]="form().phone"
                      (valueChange)="setField('phone', $event)"
                      type="tel"
                      placeholder="(555) 234-8921"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="pi-email" class="text-xs">Email Address *</label>
                    <ui-input
                      id="pi-email"
                      [value]="form().email"
                      (valueChange)="setField('email', $event)"
                      type="email"
                      placeholder="[email protected]"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5 sm:col-span-2">
                    <label ui-label for="pi-address" class="text-xs">Street Address *</label>
                    <ui-input
                      id="pi-address"
                      [value]="form().streetAddress"
                      (valueChange)="setField('streetAddress', $event)"
                      placeholder="Street, Apt / Suite"
                      size="small"
                    />
                  </div>
                  <div class="grid grid-cols-1 gap-3 sm:col-span-2 sm:grid-cols-3">
                    <div class="col-span-1 space-y-1.5">
                      <label ui-label for="pi-city" class="text-xs">City</label>
                      <ui-input
                        id="pi-city"
                        [value]="form().city"
                        (valueChange)="setField('city', $event)"
                        placeholder="City"
                        size="small"
                      />
                    </div>
                    <div class="col-span-1 space-y-1.5">
                      <label ui-label for="pi-state" class="text-xs">State</label>
                      <ui-input
                        id="pi-state"
                        [value]="form().state"
                        (valueChange)="setField('state', $event)"
                        placeholder="State / Province"
                        size="small"
                      />
                    </div>
                    <div class="col-span-1 space-y-1.5">
                      <label ui-label for="pi-zip" class="text-xs">ZIP Code</label>
                      <ui-input
                        id="pi-zip"
                        [value]="form().zipCode"
                        (valueChange)="setField('zipCode', $event)"
                        placeholder="ZIP code"
                        size="small"
                      />
                    </div>
                  </div>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="border-border bg-muted/20 space-y-3 rounded-lg border p-4">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="UserCheck" class="text-primary size-4" />
                    <h4 class="text-foreground text-xs font-semibold">
                      Emergency Contact (Designated Healthcare Surrogate)
                    </h4>
                  </div>
                  <span ui-badge wrap variant="secondary" class="text-xs">Required</span>
                </div>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
                  <div class="space-y-1.5">
                    <label ui-label for="pi-emg-name" class="text-xs">Contact Full Name *</label>
                    <ui-input
                      id="pi-emg-name"
                      [value]="form().emergencyName"
                      (valueChange)="setField('emergencyName', $event)"
                      placeholder="Full legal name"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="pi-emg-rel" class="text-xs">Relationship to Patient *</label>
                    <ui-select
                      [value]="form().emergencyRelationship"
                      (valueChange)="setField('emergencyRelationship', $event)"
                    >
                      <button ui-select-trigger id="pi-emg-rel" size="sm" class="h-8 text-xs">
                        <ui-select-value placeholder="Relationship" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="spouse">Spouse</ui-select-item>
                        <ui-select-item value="parent">Parent / Guardian</ui-select-item>
                        <ui-select-item value="child">Adult Child</ui-select-item>
                        <ui-select-item value="sibling">Sibling</ui-select-item>
                        <ui-select-item value="friend">Friend / Relative</ui-select-item>
                        <ui-select-item value="other">Other Legal Representative</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="pi-emg-phone" class="text-xs">Emergency Phone *</label>
                    <ui-input
                      id="pi-emg-phone"
                      [value]="form().emergencyPhone"
                      (valueChange)="setField('emergencyPhone', $event)"
                      type="tel"
                      placeholder="(555) 987-6543"
                      size="small"
                    />
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- STEP 2: Medical History -->
          @if (step() === 2) {
            <div class="space-y-6">
              <div class="space-y-3">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="Activity" class="text-primary size-4" />
                    <h3 class="text-foreground text-xs font-semibold">Chronic & Pre-Existing Health Conditions</h3>
                  </div>
                  <span ui-badge wrap variant="secondary" class="font-mono text-xs tabular-nums">
                    {{ form().chronicConditions.length }} selected
                  </span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Select any conditions diagnosed or treated in the past 5 years. If none apply, leave unchecked.
                </p>
                <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
                  @for (cond of chronicConditions; track cond.id) {
                    <div
                      class="border-border hover:bg-muted/30 flex items-center gap-2.5 rounded-lg border p-2.5 transition-colors"
                    >
                      <ui-checkbox
                        [id]="'condition-' + cond.id"
                        [checked]="form().chronicConditions.includes(cond.id)"
                        (checkedChange)="toggleCondition(cond.id, $event)"
                      />
                      <label
                        ui-label
                        [for]="'condition-' + cond.id"
                        class="flex-1 cursor-pointer text-xs font-normal select-none"
                      >
                        {{ cond.label }}
                      </label>
                    </div>
                  }
                </div>
              </div>
    
              <ui-separator />
    
              <div class="border-border bg-muted/20 space-y-3 rounded-lg border p-4">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="AlertTriangle" class="text-warning size-4" />
                    <h4 class="text-foreground text-xs font-semibold">Known Drug, Latex, or Food Allergies</h4>
                  </div>
                  <div class="flex items-center gap-2">
                    <ui-checkbox
                      id="mh-has-allergies"
                      [checked]="form().hasAllergies"
                      (checkedChange)="setField('hasAllergies', !!$event)"
                      label="I have known allergies"
                    />
                  </div>
                </div>
                @if (form().hasAllergies) {
                  <div class="space-y-1.5 pt-1">
                    <label ui-label for="mh-allergy-details" class="text-xs">
                      List Specific Allergens & Adverse Reactions *
                    </label>
                    <ui-input
                      id="mh-allergy-details"
                      [value]="form().allergyDetails"
                      (valueChange)="setField('allergyDetails', $event)"
                      placeholder="e.g., Penicillin (Anaphylaxis), Sulfa (Severe Rash), Peanuts (Hives), Latex"
                      size="small"
                    />
                  </div>
                } @else {
                  <p class="text-muted-foreground text-xs italic">No known drug, latex, or food allergies reported.</p>
                }
              </div>
    
              <ui-separator />
    
              <div class="space-y-2">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="Stethoscope" class="text-primary size-4" />
                  <label ui-label for="mh-medications" class="text-xs font-semibold">
                    Current Prescription & OTC Medications
                  </label>
                </div>
                <ui-textarea
                  id="mh-medications"
                  [value]="form().currentMedications"
                  (valueChange)="setField('currentMedications', $event)"
                  [rows]="3"
                  placeholder="Include drug name, dosage, and schedule (e.g., Lisinopril 10mg once daily in AM, Metformin 500mg BID with meals)..."
                  class="text-xs"
                />
                <p class="text-muted-foreground text-xs">
                  Include all daily vitamins, herbal supplements, inhalers, and injections.
                </p>
              </div>
    
              <ui-separator />
    
              <div class="space-y-2">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="FileText" class="text-primary size-4" />
                  <label ui-label for="mh-surgeries" class="text-xs font-semibold">
                    Past Surgeries, Major Injuries & Hospitalizations
                  </label>
                </div>
                <ui-textarea
                  id="mh-surgeries"
                  [value]="form().pastSurgeries"
                  (valueChange)="setField('pastSurgeries', $event)"
                  [rows]="2"
                  placeholder="List approximate year and procedure (e.g., Appendectomy 2016, Knee Arthroscopy 2021)..."
                  class="text-xs"
                />
              </div>
    
              <ui-separator />
    
              <div class="space-y-3">
                <h4 class="text-foreground text-xs font-semibold">Primary Care Provider / Referring Clinic</h4>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  <div class="space-y-1.5">
                    <label ui-label for="mh-pcp-name" class="text-xs">Physician or Practice Name</label>
                    <ui-input
                      id="mh-pcp-name"
                      [value]="form().primaryCarePhysician"
                      (valueChange)="setField('primaryCarePhysician', $event)"
                      placeholder="Dr. Robert Chen, MD"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="mh-clinic-phone" class="text-xs">Clinic Contact / Phone</label>
                    <ui-input
                      id="mh-clinic-phone"
                      [value]="form().clinicPhone"
                      (valueChange)="setField('clinicPhone', $event)"
                      placeholder="(555) 601-2290"
                      size="small"
                    />
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- STEP 3: Insurance & Billing -->
          @if (step() === 3) {
            <div class="space-y-6">
              <div class="space-y-3">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="Building2" class="text-primary size-4" />
                    <h3 class="text-foreground text-xs font-semibold">Primary Medical Insurance Carrier</h3>
                  </div>
                  <span ui-badge wrap variant="outline" class="text-xs">Primary Plan</span>
                </div>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
                  <div class="space-y-1.5">
                    <label ui-label for="ins-carrier" class="text-xs">Insurance Carrier *</label>
                    <ui-select [value]="form().insuranceCarrier" (valueChange)="setField('insuranceCarrier', $event)">
                      <button ui-select-trigger id="ins-carrier" size="sm" class="h-8 text-xs">
                        <ui-select-value placeholder="Select carrier" />
                      </button>
                      <ui-select-content>
                        @for (carrier of insuranceCarriers; track carrier.id) {
                          <ui-select-item [value]="carrier.id">{{ carrier.label }}</ui-select-item>
                        }
                      </ui-select-content>
                    </ui-select>
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="ins-member-id" class="text-xs">Member / Subscriber ID *</label>
                    <ui-input
                      id="ins-member-id"
                      [value]="form().memberId"
                      (valueChange)="setField('memberId', $event)"
                      placeholder="e.g. XEH-902814891"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="ins-group-num" class="text-xs">Group Number</label>
                    <ui-input
                      id="ins-group-num"
                      [value]="form().groupNumber"
                      (valueChange)="setField('groupNumber', $event)"
                      placeholder="e.g. GRP-44028"
                      size="small"
                    />
                  </div>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="space-y-3">
                <h3 class="text-foreground flex items-center gap-2 text-xs font-semibold">
                  <lucide-icon [img]="User" class="text-primary size-3.5" />
                  Primary Policyholder Information
                </h3>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  <div class="space-y-1.5">
                    <label ui-label for="ins-holder-name" class="text-xs">Primary Insured Full Legal Name *</label>
                    <ui-input
                      id="ins-holder-name"
                      [value]="form().policyholderName"
                      (valueChange)="setField('policyholderName', $event)"
                      placeholder="Policyholder name"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5">
                    <label ui-label for="ins-holder-rel" class="text-xs">Relationship to Patient *</label>
                    <ui-select
                      [value]="form().policyholderRelationship"
                      (valueChange)="setField('policyholderRelationship', $event)"
                    >
                      <button ui-select-trigger id="ins-holder-rel" size="sm" class="h-8 text-xs">
                        <ui-select-value placeholder="Relationship" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="self">Self (Patient is Policyholder)</ui-select-item>
                        <ui-select-item value="spouse">Spouse</ui-select-item>
                        <ui-select-item value="child">Child / Dependent</ui-select-item>
                        <ui-select-item value="other">Other Legal Custodian</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="space-y-3">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="CreditCard" class="text-primary size-4" />
                    <h3 class="text-foreground text-xs font-semibold">Insurance Card Photo Verification</h3>
                  </div>
                  <span class="text-muted-foreground text-xs">PNG, JPG, PDF up to 10MB</span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Provide high-resolution photos of both front and back sides of your insurance card for optical character
                  recognition (OCR) and benefit eligibility verification.
                </p>
    
                <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
                  <div
                    class="border-border bg-card hover:bg-muted/20 flex flex-col justify-between rounded-xl border p-4 text-center transition-colors"
                  >
                    <div class="space-y-2 py-3">
                      <div class="bg-primary/10 text-primary mx-auto flex size-10 items-center justify-center rounded-full">
                        <lucide-icon [img]="CreditCard" class="size-5" />
                      </div>
                      <div class="space-y-0.5">
                        <p class="text-foreground text-xs font-semibold">Front of Insurance Card</p>
                        <p class="text-muted-foreground text-xs">Subscriber ID & carrier logo visible</p>
                      </div>
                    </div>
                    <div
                      class="border-border/60 bg-muted/40 flex flex-wrap items-center justify-between rounded-md border px-3 py-2 text-xs"
                    >
                      @if (form().frontCardUploaded) {
                        <span class="text-success flex items-center gap-1.5 font-medium">
                          <lucide-icon [img]="Check" class="size-3.5" />
                          card_front_scan.jpg (2.4 MB)
                        </span>
                      } @else {
                        <span class="text-muted-foreground">No file selected</span>
                      }
                      <button
                        ui-button
                        type="button"
                        variant="outline"
                        size="sm"
                        class="h-7 text-xs font-medium"
                        (click)="toggleUpload('frontCardUploaded')"
                      >
                        {{ form().frontCardUploaded ? 'Replace' : 'Upload Front' }}
                      </button>
                    </div>
                  </div>
    
                  <div
                    class="border-border bg-card hover:bg-muted/20 flex flex-col justify-between rounded-xl border p-4 text-center transition-colors"
                  >
                    <div class="space-y-2 py-3">
                      <div class="bg-primary/10 text-primary mx-auto flex size-10 items-center justify-center rounded-full">
                        <lucide-icon [img]="CreditCard" class="size-5" />
                      </div>
                      <div class="space-y-0.5">
                        <p class="text-foreground text-xs font-semibold">Back of Insurance Card</p>
                        <p class="text-muted-foreground text-xs">Claims address & Rx BIN visible</p>
                      </div>
                    </div>
                    <div
                      class="border-border/60 bg-muted/40 flex flex-wrap items-center justify-between rounded-md border px-3 py-2 text-xs"
                    >
                      @if (form().backCardUploaded) {
                        <span class="text-success flex items-center gap-1.5 font-medium">
                          <lucide-icon [img]="Check" class="size-3.5" />
                          card_back_scan.jpg (1.9 MB)
                        </span>
                      } @else {
                        <span class="text-muted-foreground">No file selected</span>
                      }
                      <button
                        ui-button
                        type="button"
                        variant="outline"
                        size="sm"
                        class="h-7 text-xs font-medium"
                        (click)="toggleUpload('backCardUploaded')"
                      >
                        {{ form().backCardUploaded ? 'Replace' : 'Upload Back' }}
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- STEP 4: Consent & Sign -->
          @if (step() === 4) {
            <div class="space-y-6">
              <div class="space-y-2">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="FileText" class="text-primary size-4" />
                    <h3 class="text-foreground text-xs font-semibold">Regulatory Disclosures & Legal Notice</h3>
                  </div>
                  <span ui-badge wrap variant="secondary" class="text-xs">Mandatory Acknowledgement</span>
                </div>
                <div
                  class="border-border bg-muted/20 text-muted-foreground max-h-40 space-y-3 overflow-y-auto rounded-lg border p-3.5 text-xs leading-relaxed"
                >
                  <div>
                    <span class="text-foreground font-semibold">1. HIPAA Notice of Privacy Practices:</span>
                    I acknowledge receipt and review of the Notice of Privacy Practices, which describes how my Protected
                    Health Information (PHI) may be used and disclosed for clinical treatment, payment processing, and
                    healthcare operations in strict compliance with the HIPAA Omnibus Rule (45 CFR § 164.520).
                  </div>
                  <div>
                    <span class="text-foreground font-semibold">2. Informed Consent for Outpatient Treatment:</span>
                    I voluntarily authorize the clinical physicians and allied healthcare team to perform diagnostic
                    evaluations, blood tests, physical examinations, and outpatient clinical care deemed medically
                    necessary.
                  </div>
                  <div>
                    <span class="text-foreground font-semibold">3. Assignment of Benefits & Financial Agreement:</span>
                    I assign all medical insurance benefits directly to the provider. I acknowledge that I remain
                    financially responsible for copayments, deductibles, and non-covered services not adjudicated by my
                    carrier.
                  </div>
                </div>
              </div>
    
              <div class="divide-border divide-y rounded-lg border">
                <div class="flex items-start gap-3 p-3.5">
                  <ui-checkbox
                    id="cs-hipaa-consent"
                    [checked]="form().hipaaConsent"
                    (checkedChange)="setField('hipaaConsent', !!$event)"
                    class="mt-0.5"
                  />
                  <div class="space-y-0.5">
                    <label ui-label for="cs-hipaa-consent" class="cursor-pointer text-xs font-medium">
                      HIPAA Privacy Practices Acknowledgement *
                    </label>
                    <p class="text-muted-foreground text-xs">
                      I have read, understood, and accept the HIPAA Notice of Privacy Practices and consent to electronic
                      medical record management.
                    </p>
                  </div>
                </div>
    
                <div class="flex items-start gap-3 p-3.5">
                  <ui-checkbox
                    id="cs-treatment-consent"
                    [checked]="form().treatmentConsent"
                    (checkedChange)="setField('treatmentConsent', !!$event)"
                    class="mt-0.5"
                  />
                  <div class="space-y-0.5">
                    <label ui-label for="cs-treatment-consent" class="cursor-pointer text-xs font-medium">
                      Informed Treatment Consent & Benefit Assignment *
                    </label>
                    <p class="text-muted-foreground text-xs">
                      I authorize medical evaluation and agree to the assignment of insurance benefits to the clinical care
                      provider.
                    </p>
                  </div>
                </div>
    
                <div class="flex items-start gap-3 p-3.5">
                  <ui-checkbox
                    id="cs-telehealth-consent"
                    [checked]="form().telehealthConsent"
                    (checkedChange)="setField('telehealthConsent', !!$event)"
                    class="mt-0.5"
                  />
                  <div class="space-y-0.5">
                    <label ui-label for="cs-telehealth-consent" class="cursor-pointer text-xs font-medium">
                      Telehealth & Encrypted Digital Communications (Optional)
                    </label>
                    <p class="text-muted-foreground text-xs">
                      I consent to virtual video consultations and automated SMS/email appointment notifications and lab
                      report alerts.
                    </p>
                  </div>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="space-y-4">
                <div class="flex flex-wrap items-center justify-between">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="Lock" class="text-primary size-4" />
                    <h3 class="text-foreground text-xs font-semibold">Electronic Signature & Digital Certification</h3>
                  </div>
                  <span ui-badge wrap variant="outline" class="border-primary/30 text-primary font-mono text-xs">
                    256-bit Certified
                  </span>
                </div>
    
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="cs-signer-name" class="text-xs">Type Full Legal Name as Signature *</label>
                    <ui-input
                      id="cs-signer-name"
                      [value]="form().signatureName"
                      (valueChange)="setField('signatureName', $event)"
                      placeholder="e.g. Eleanor R. Vance"
                      size="small"
                    />
                  </div>
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="cs-signer-role" class="text-xs">Signer Capacity / Role *</label>
                    <ui-select [value]="form().signerRole" (valueChange)="setField('signerRole', $event)">
                      <button ui-select-trigger id="cs-signer-role" size="sm" class="h-8 text-xs">
                        <ui-select-value placeholder="Select role" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="patient">Patient (Self)</ui-select-item>
                        <ui-select-item value="parent_guardian">Parent / Legal Guardian</ui-select-item>
                        <ui-select-item value="poa">Medical Power of Attorney</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                  <div class="space-y-1.5 sm:col-span-1">
                    <label ui-label for="cs-sig-date" class="text-xs">Signature Date *</label>
                    <ui-input
                      id="cs-sig-date"
                      [value]="form().signatureDate"
                      (valueChange)="setField('signatureDate', $event)"
                      type="date"
                      size="small"
                    />
                  </div>
                </div>
    
                <div class="border-primary/30 bg-primary/5 space-y-3 rounded-xl border p-4">
                  <div class="flex flex-wrap items-center justify-between">
                    <span class="text-muted-foreground text-xs font-medium"> Legal Digital Signature Stamp </span>
                    <span class="text-primary flex items-center gap-1 font-mono text-xs">
                      <lucide-icon [img]="ShieldCheck" class="size-3.5" />
                      Verified & Timestamped
                    </span>
                  </div>
    
                  <div class="border-primary/20 border-b pt-1 pb-3">
                    <p class="text-primary text-2xl font-medium tracking-wide italic">
                      {{ form().signatureName || 'Your Signature' }}
                    </p>
                  </div>
    
                  <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 font-mono text-xs">
                    <span>Signer: {{ form().signatureName || 'Eleanor Vance' }} ({{ form().signerRole }})</span>
                    <span>Date: {{ form().signatureDate }} · Ref #SIG-49102-HIPAA</span>
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- STEP 5: Success / Confirmation Receipt -->
          @if (step() === 5) {
            <div class="space-y-6 py-4 text-center">
              <div
                class="border-success/30 bg-success/10 text-success mx-auto flex size-16 items-center justify-center rounded-full border shadow-xs"
              >
                <lucide-icon [img]="Check" class="size-8" />
              </div>
    
              <div class="space-y-1">
                <h3 class="text-foreground text-xl font-bold tracking-tight">Patient Intake Submitted Successfully</h3>
                <p class="text-muted-foreground mx-auto max-w-md text-xs sm:text-sm">
                  Your new patient registration and encrypted medical disclosures have been received and queued for clinical
                  triage.
                </p>
              </div>
    
              <div class="border-border inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs">
                <span class="text-muted-foreground">Confirmation ID:</span>
                <span class="text-foreground font-mono font-bold">{{ intakeId() }}</span>
              </div>
    
              <div class="border-border bg-muted/30 mx-auto max-w-xl space-y-3 rounded-xl border p-4 text-left text-xs">
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  <div>
                    <span class="text-muted-foreground block">Patient Name:</span>
                    <span class="text-foreground font-semibold"
                      >{{ form().firstName }} {{ form().middleName }} {{ form().lastName }}</span
                    >
                  </div>
                  <div>
                    <span class="text-muted-foreground block">Date of Birth:</span>
                    <span class="text-foreground font-medium tabular-nums"
                      >{{ form().dob }} ({{ form().biologicalSex }})</span
                    >
                  </div>
                  <div>
                    <span class="text-muted-foreground block">Primary Insurance:</span>
                    <span class="text-foreground font-medium"
                      >{{ selectedCarrierLabel() }} (ID: {{ form().memberId }})</span
                    >
                  </div>
                  <div>
                    <span class="text-muted-foreground block">Emergency Contact:</span>
                    <span class="text-foreground font-medium"
                      >{{ form().emergencyName }} ({{ form().emergencyPhone }})</span
                    >
                  </div>
                  <div
                    class="border-border/60 col-span-1 flex flex-wrap items-center justify-between border-t pt-2 sm:col-span-2"
                  >
                    <span class="text-muted-foreground">Legal Signature:</span>
                    <span class="text-primary font-medium tracking-wide italic"
                      >{{ form().signatureName }} ({{ form().signatureDate }})</span
                    >
                  </div>
                </div>
              </div>
    
              <div class="flex flex-wrap items-center justify-center gap-3 pt-2">
                <button
                  ui-button
                  aria-label="Download attachment"
                  variant="outline"
                  size="sm"
                  class="gap-1.5 text-xs font-medium"
                >
                  <lucide-icon [img]="Download" class="size-3.5" />
                  Download Intake PDF
                </button>
                <button ui-button size="sm" class="gap-1.5 text-xs font-medium" (click)="resetForm()">
                  <lucide-icon [img]="RotateCcw" class="size-3.5" />
                  Register Another Patient
                </button>
              </div>
            </div>
          }
    
          <div slot="footer" class="border-border bg-muted/20 flex w-full items-center justify-between border-t px-6 py-4">
            @if (step() > 1 && step() <= 4) {
              <button ui-button variant="ghost" size="sm" class="gap-1.5 text-xs font-medium" (click)="goBack()">
                <lucide-icon [img]="ChevronLeft" class="size-4" />
                Back
              </button>
            } @else {
              <span aria-hidden="true"></span>
            }
    
            @if (step() <= 4) {
              <span class="text-muted-foreground text-xs font-medium"> Step {{ step() }} of 4 — {{ stepTitle() }} </span>
            } @else {
              <span class="text-muted-foreground text-xs font-medium">Registration Complete</span>
            }
    
            @if (step() < 4) {
              <button
                ui-button
                size="sm"
                class="gap-1.5 text-xs font-medium"
                [disabled]="continueDisabled()"
                (click)="goNext()"
              >
                Continue
                <lucide-icon [img]="ChevronRight" class="size-4" />
              </button>
            } @else if (step() === 4) {
              <button
                ui-button
                size="sm"
                class="bg-primary text-primary-foreground gap-1.5 text-xs font-medium"
                [disabled]="submitDisabled()"
                (click)="handleSubmit()"
              >
                <lucide-icon [img]="FileCheck" class="size-4" />
                Submit Registration
              </button>
            } @else {
              <span aria-hidden="true"></span>
            }
          </div>
        </ui-section-card>
      `,
    })
    export class UiPatientIntakeFormComponent {
      protected readonly Activity = Activity
      protected readonly AlertTriangle = AlertTriangle
      protected readonly Building2 = Building2
      protected readonly Calendar = Calendar
      protected readonly Check = Check
      protected readonly ChevronLeft = ChevronLeft
      protected readonly ChevronRight = ChevronRight
      protected readonly CreditCard = CreditCard
      protected readonly Download = Download
      protected readonly FileCheck = FileCheck
      protected readonly FileText = FileText
      protected readonly Lock = Lock
      protected readonly Phone = Phone
      protected readonly RotateCcw = RotateCcw
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Stethoscope = Stethoscope
      protected readonly User = User
      protected readonly UserCheck = UserCheck
    
      protected readonly steps = steps
      protected readonly chronicConditions = CHRONIC_CONDITIONS
      protected readonly insuranceCarriers = INSURANCE_CARRIERS
    
      @Input('class') className?: string
    
      readonly step = signal(1)
      readonly submitted = signal(false)
      readonly intakeId = signal('INTAKE-2026-8942')
      readonly form = signal<PatientIntakeData>({
        ...DEFAULT_FORM,
        chronicConditions: [...DEFAULT_FORM.chronicConditions],
      })
    
      @Input('initialStep') set initialStepAttr(v: number | undefined) {
        if (v !== undefined) this.step.set(v)
      }
      @Input('initialData') set initialDataAttr(v: Partial<PatientIntakeData> | undefined) {
        if (v !== undefined) {
          this.form.update((prev) => ({
            ...prev,
            ...v,
            chronicConditions: v.chronicConditions ? [...v.chronicConditions] : [...prev.chronicConditions],
          }))
        }
      }
    
      @Output() readonly submit = new EventEmitter<PatientIntakeData>()
      @Output() readonly stepChange = new EventEmitter<number>()
    
      readonly selectedCarrierLabel = computed(() => {
        const found = INSURANCE_CARRIERS.find((c) => c.id === this.form().insuranceCarrier)
        return found ? found.label : this.form().insuranceCarrier
      })
    
      readonly stepTitle = computed(() => steps[this.step() - 1]?.title ?? '')
    
      readonly continueDisabled = computed(
        () => this.step() === 1 && (this.form().firstName.trim() === '' || this.form().lastName.trim() === ''),
      )
    
      readonly submitDisabled = computed(
        () => !this.form().hipaaConsent || !this.form().treatmentConsent || this.form().signatureName.trim() === '',
      )
    
      get cardClass(): string {
        return cn('max-w-4xl', this.className)
      }
    
      setField<K extends keyof PatientIntakeData>(key: K, value: PatientIntakeData[K]): void {
        this.form.update((prev) => ({ ...prev, [key]: value }))
      }
    
      toggleCondition(conditionId: string, checked: boolean | 'indeterminate'): void {
        this.form.update((prev) => {
          if (checked === true) {
            if (prev.chronicConditions.includes(conditionId)) return prev
            return { ...prev, chronicConditions: [...prev.chronicConditions, conditionId] }
          }
          return { ...prev, chronicConditions: prev.chronicConditions.filter((id) => id !== conditionId) }
        })
      }
    
      toggleUpload(key: 'frontCardUploaded' | 'backCardUploaded'): void {
        this.form.update((prev) => ({ ...prev, [key]: !prev[key] }))
      }
    
      onStepperInput(value: number): void {
        // Backward navigation through the stepper is free; forward goes through Continue.
        if (value < this.step() && this.step() !== 5) {
          this.step.set(value)
          this.stepChange.emit(value)
        }
      }
    
      goNext(): void {
        if (this.step() < 4) {
          const next = this.step() + 1
          this.step.set(next)
          this.stepChange.emit(next)
        }
      }
    
      goBack(): void {
        if (this.step() > 1 && this.step() <= 4) {
          const prev = this.step() - 1
          this.step.set(prev)
          this.stepChange.emit(prev)
        }
      }
    
      handleSubmit(): void {
        this.submitted.set(true)
        this.step.set(5)
        const form = this.form()
        this.submit.emit({ ...form, chronicConditions: [...form.chronicConditions] })
        this.stepChange.emit(5)
      }
    
      resetForm(): void {
        this.step.set(1)
        this.submitted.set(false)
        this.stepChange.emit(1)
      }
    }
    
  • src/app/components/blocks/patient-intake-form/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/patient-intake-form.json