claude-os/docs/BLOCK_CASE_STUDY_TUFFY_APPOINTMENT_FORM.md
2025-10-27 17:41:44 -05:00

38 KiB

Block System Case Study: Tuffy Appointment Form (Block 122)

Migration Blueprint for Rails Views Refactor

Date: 2025-10-24 Block ID: 122 (Tuffy Appt Main) Account ID: 11187 (Account-specific block) Status: Real production block analyzed


Executive Summary

Block 122 is THE complex appointment scheduling form - representing the highest complexity in PISTN's Block system. It demonstrates:

  • ✅ 43KB of mixed HTML, Handlebars templating, and inline JavaScript
  • ✅ 4-step wizard workflow with client-side state management
  • ✅ Complex conditional rendering based on account settings
  • ✅ Inline JavaScript with jQuery and event handlers
  • ✅ Form validation with business logic
  • ✅ Multiple child block yields
  • ✅ Data binding between form fields and confirmation display
  • ✅ Phone verification modal workflow
  • ✅ Google Tag Manager event tracking
  • ✅ Existing vehicle lookup and selection

This block is a stress test for the migration strategy - if we can migrate Block 122 to Rails, we can migrate anything.


Part 1: Block 122 Architecture Analysis

Database Record

ID: 122
Name: Tuffy Appt Main
Tag: (empty)
Role: (empty)
Account ID: 11187 (Account-specific - not universal)
Parent ID: NULL (top-level block)
Content Length: 43,762 bytes (~44 KB)

Key Statistics

  • Total Lines of Code: ~1,150 lines (mixed HTML/JavaScript)
  • JavaScript Lines: ~600 lines (inline, not external file)
  • HTML Structure: Bootstrap grid-based 4-step wizard
  • Form Steps:
    1. Service selection with service categories
    2. Date/time picker with calendar UI
    3. Customer information with vehicle selection
    4. Confirmation review
  • Form Fields: 25+ input fields across all steps
  • Conditional Renders: 12+ places using {{ }} handlebars syntax
  • Event Handlers: 10+ JavaScript event listeners
  • API Endpoints: Calls to /verify_phone_number, /appointment (form submit)

Part 2: Detailed Structure Breakdown

Data Dependencies

Account-Level Properties Accessed:

// Via {{account.property}} syntax
account.id                                  // Account ID
account.name                                // Display name
account.address_str                         // Address
account.phone_number                        // Phone
account.online_scheduling_promo_message     // Promotional message
account.require_addr_for_appt               // Address field requirement
account.require_costco_membership_number?   // Costco membership requirement
account.show_towing                         // Towing field requirement
account.require_license_plate               // License plate requirement
account.use_mobile_verification             // Phone verification toggle
account.store_hours_options                 // Hour dropdowns (HTML options)
account.store_hours_html                    // Rendered hours display
account.render_new_appt_times_string        // Calendar filtering flag
account.appt_min_hours                      // Minimum hours before appointment

Page Object:

page.intro_or_default_text                  // Customizable intro copy

Model-Level Properties:

account.visible_service_categories          // yield: Service category loop
account.account_services                    // yield: Services within category

Form Structure

Appointment Form (id="appt_form")
├── Step 1: Service Selection
│   ├── {{yield account.visible_service_categories}}  → Loop through categories
│   ├── Wait/Drop-off dropdown
│   └── Submit: "Select date/time"
│
├── Step 2: Date & Time
│   ├── jQuery UI Datepicker calendar
│   ├── Phone Verification Section (conditional on use_mobile_verification)
│   ├── Preferred Time dropdown ({{account.store_hours_options}})
│   ├── Towing dropdown (conditional on account.show_towing)
│   └── Submit: "Enter your information"
│
├── Step 3: Customer Information
│   ├── Personal Info (first name, last name, email, phone)
│   ├── Costco Membership (conditional on require_costco_membership_number)
│   ├── {{yield Address Fields if account.require_addr_for_appt}}
│   ├── Vehicle Selection
│   │   ├── Existing Vehicles List (populated by JavaScript)
│   │   └── {{yield New Veh Form}} (for new vehicles)
│   ├── Car Issues textarea
│   └── Submit: "Confirm your Appointment Request"
│
├── Step 4: Confirmation
│   ├── Summary Review
│   │   ├── Customer Name & Vehicle
│   │   ├── Location ({{account.name}}, {{account.address_str}})
│   │   ├── Services
│   │   ├── Date/Time
│   │   └── Notes
│   ├── {{yield TuffySignUps}} (right sidebar)
│   ├── Recaptcha box
│   └── Submit: "Request Appointment"
│
└── Modal: Phone Verification
    ├── Input: Verification code
    ├── Actions: Verify Code, Resend Code
    └── Status messages

JavaScript Functions (Critical to Migration)

window.choose_step(stepNum)
  // Navigate between form steps
  // Updates UI, scrolls to top, shows/hides sidebars
  // Core: $('.form-step').hide(); $(`#form-step-${i}`).show();

window.validate_step(stepNum)
  // Complex validation logic including:
  // - Required field checking
  // - Special vehicle selection logic (existing vs new)
  // - Date validation (must be 24+ hours from now)
  // - Store hours validation
  // - Phone verification check (if enabled)
  // - Google Tag Manager event firing

window.set_confirm()
  // Updates confirmation display when fields change
  // Binds to: input[name="services[]"], input[name="customer[*]"]

window.updateConfirmationStep()
  // Updates step 4 confirmation with selected vehicle

window.verifyPhoneNumber(accountId, number)
  // AJAX call to /verify_phone_number
  // Shows modal with verification code input
  // Expiry: 5 minutes

window.updateTimeSlots(dateStr, accountId, wait, selectedServiceIds)
  // AJAX: Fetches available time slots
  // Called when date selected IF render_new_appt_times_string == true

window.change_location()
  // Reopens location selection

window.validate_store_hours()
  // Checks if appointment time is within store hours
  // Uses window.active_hours array

window.removeSelectError / removeError
  // Event handlers for error removal on user input

Child Block Yields

Block 122 references these child blocks via {{yield ...}}:

  1. {{yield account.visible_service_categories}}

    • Loop through service categories
    • Each category has checkboxes for services
    • Calls {{yield account.account_services}} for services in category
    • Example: yield Block 25342 (service selector)
  2. {{yield Address Fields if account.require_addr_for_appt}}

    • Conditional address input block
    • Only shown if account.require_addr_for_appt is true
  3. {{yield New Veh Form}}

    • New vehicle year/make/model selectors
    • Example: block 24794, 24793 (vehicle form blocks)
  4. {{yield TuffySignUps}}

    • Newsletter signup on confirmation step
    • Example: block 2418 (empty placeholder)

Part 3: Complexity Metrics

Why Block 122 is High Complexity

Factor Score Details
Size 9/10 43KB, ~1,150 lines
JavaScript 10/10 600+ lines of inline JS, event handlers, AJAX
Conditional Logic 7/10 12+ places with if conditions, feature flags
State Management 9/10 Multi-step wizard with complex validation
Data Binding 8/10 Form fields must sync with confirmation display
External Dependencies 8/10 jQuery UI Datepicker, reCAPTCHA, Google Tag Manager
API Calls 6/10 POST /appointment, GET /verify_phone_number, XHR for time slots
Testing Difficulty 9/10 Integration + E2E testing required for full coverage

Overall Complexity: 8.5/10 - Enterprise-grade appointment form with sophisticated UX


Part 4: Migration Strategy

Approach: Decompose into Rails Components

Instead of replacing Block 122 directly, decompose into:

  1. Views - Multi-step form partials
  2. Controller - Step navigation, validation
  3. Service - Business logic (validation, time slot fetching)
  4. Presenter - Data preparation for views
  5. JavaScript - Extract to external JS files

File Structure

app/views/appointments/
├── _appointment_form.html.erb          # Main form wrapper
├── _step_1_services.html.erb           # Service selection
├── _step_2_datetime.html.erb           # Date/time picker
├── _step_3_customer_info.html.erb      # Personal info & vehicle
├── _step_4_confirmation.html.erb       # Review & submit
└── _phone_verification_modal.html.erb  # Phone verification modal

app/controllers/
└── appointments_controller.rb           # Step navigation, submission

app/presenters/
└── appointment_form_presenter.rb        # Data preparation, conditionals

app/services/
└── validate_appointment_service.rb      # Complex validation logic
└── fetch_appointment_slots_service.rb   # Calendar/slot fetching

app/assets/javascripts/
└── appointment_form.js                  # Extract inline JS here

Step 1: Create Presenter

File: app/presenters/appointment_form_presenter.rb

class AppointmentFormPresenter
  def initialize(account, appointment = nil)
    @account = account
    @appointment = appointment || Appointment.new
  end

  # Account properties
  def account_id; @account.id; end
  def account_name; @account.name; end
  def account_address; @account.address_str; end
  def account_phone; @account.phone_number; end

  # Conditionals
  def show_online_scheduling_promo?
    @account.online_scheduling_promo_message.present?
  end

  def online_scheduling_promo_message
    @account.online_scheduling_promo_message
  end

  def require_address?
    @account.require_addr_for_appt?
  end

  def require_costco_membership?
    @account.require_costco_membership_number?
  end

  def show_towing?
    @account.show_towing?
  end

  def use_mobile_verification?
    @account.use_mobile_verification?
  end

  def minimum_appointment_hours
    @account.appt_min_hours || 24
  end

  def filter_time_slots?
    @account.render_new_appt_times_string == 'true'
  end

  # Service categories (for yield loop)
  def visible_service_categories
    @account.visible_service_categories
  end

  # Store hours
  def store_hours_html
    @account.store_hours_html
  end

  def store_hours_options
    @account.store_hours_options
  end

  def store_hours_json
    # For JavaScript time validation
    @account.store_hours_for_js.to_json
  end

  # Intro text
  def intro_text
    Page.intro_or_default_text || "Schedule your appointment with us."
  end
end

Step 2: Create Controller

File: app/controllers/appointments_controller.rb

class AppointmentsController < ApplicationController
  before_action :set_account
  before_action :set_presenter, only: [:new]

  # Show appointment form
  def new
    @appointment = Appointment.new
    @presenter = AppointmentFormPresenter.new(@account)

    # For existing vehicles (pre-fill if customer found)
    @existing_vehicles = @account.vehicles.where(
      phone_number: params[:phone]
    ) if params[:phone].present?
  end

  # Handle step navigation (AJAX)
  def validate_step
    step = params[:step].to_i
    data = JSON.parse(params[:form_data] || '{}')

    result = ValidateAppointmentService.perform(
      account: @account,
      step: step,
      data: data
    )

    if result[:valid]
      render json: { valid: true }
    else
      render json: { valid: false, errors: result[:errors] }, status: :unprocessable_entity
    end
  end

  # Fetch available time slots
  def available_slots
    date = params[:date]
    services = params[:service_ids] || []
    wait_type = params[:wait]

    slots = FetchAppointmentSlotsService.perform(
      account: @account,
      date: date,
      service_ids: services,
      wait_type: wait_type
    )

    render json: { slots: slots }
  end

  # Verify phone number for existing customer lookup
  def verify_phone
    phone = params[:phone]
    result = VerifyPhoneService.perform(phone: phone)

    if result[:valid]
      # Send verification code (SMS/email)
      render json: {
        valid: true,
        verification_code: result[:code],  # For testing only
        expires_at: 5.minutes.from_now
      }
    else
      render json: { valid: false }, status: :unprocessable_entity
    end
  end

  # Create appointment
  def create
    service = CreateAppointmentService.perform(
      account: @account,
      customer_params: customer_params,
      appointment_params: appointment_params
    )

    if service.is_a?(Appointment)
      # Send confirmation email, etc.
      render json: { success: true, appointment_id: service.id }
    else
      render json: { success: false, error: service }, status: :unprocessable_entity
    end
  end

  private

  def set_account
    @account = current_account
  end

  def set_presenter
    @presenter = AppointmentFormPresenter.new(@account)
  end

  def customer_params
    params.require(:customer).permit(
      :first_name, :last_name, :email, :phone,
      :costco_membership_number
    )
  end

  def appointment_params
    params.require(:appointment).permit(
      :appointment_date, :appointment_time, :wait,
      :towing, :car_issues, :car_year, :car_make, :car_model,
      :service_ids => []
    )
  end
end

Step 3: Create Views

File: app/views/appointments/new.html.erb

<%= render 'appointment_form', presenter: @presenter, existing_vehicles: @existing_vehicles %>

File: app/views/appointments/_appointment_form.html.erb

<article>
  <header class="brand-header condensed text-center pt-4 pb-4 pl-2 pr-2 pt-lg-5 pb-lg-4 pl-lg-4 pr-lg-4 d-flex flex-column align-items-center justify-content-center lazy lazyload"
          data-bg="/images/tuffy/header-quote.jpg">
    <h1 class="display-2 text-yellow text-uppercase">Schedule an Appointment</h1>
  </header>

  <section class="form-wrap pt-5 pb-5">
    <div id="apptLoadingModal" class="appt-modal-overlay" style="display:none;">
      <div class="appt-modal-content">
        <p>Fetching available times <span class="ellipsis"></span></p>
      </div>
    </div>

    <%= form_with url: appointments_path, method: :post, id: 'appt_form', local: true, class: 'container' do |f| %>
      <input type="hidden" id="account_id" value="<%= presenter.account_id %>">
      <input type="hidden" id="filterTimeSlots" value="<%= presenter.filter_time_slots? %>">
      <input type="hidden" id="customer_sent_message_id" name="customer[sent_message_id]">
      <input type="hidden" id="rwg_token" name="rwg_token">

      <noscript>JavaScript required for appointment scheduling</noscript>

      <div class="row">
        <!-- Form Steps Container -->
        <div class="col-12 col-lg-8">
          <!-- Step Progress Indicator -->
          <div class="form-steps d-none d-lg-block">
            <ul class="steps list-unstyled d-flex flex-row justify-content-between align-items-start mb-4 mt-2">
              <li class="h5 current text-uppercase"><span>Service(s)</span></li>
              <li class="h5 text-uppercase"><span>Date/Time</span></li>
              <li class="h5 text-uppercase"><span>Your Info</span></li>
              <li class="h5 text-uppercase"><span>Confirm</span></li>
            </ul>
          </div>

          <!-- Intro Text -->
          <div class="col-12 col-lg-8" style="text-align: justify;">
            <%= presenter.intro_text %>
          </div>

          <!-- Promo Message -->
          <% if presenter.show_online_scheduling_promo? %>
            <div class="col-12 col-lg-8" style="font-size:20px;font-weight:bold;text-align: justify;">
              <%= presenter.online_scheduling_promo_message %>
            </div>
          <% end %>

          <!-- Step 1: Service Selection -->
          <%= render 'step_1_services', presenter: presenter %>

          <!-- Step 2: Date & Time -->
          <%= render 'step_2_datetime', presenter: presenter %>

          <!-- Step 3: Customer Information -->
          <%= render 'step_3_customer_info', presenter: presenter, existing_vehicles: existing_vehicles %>

          <!-- Step 4: Confirmation -->
          <%= render 'step_4_confirmation', presenter: presenter %>
        </div>

        <!-- Right Sidebar (Signup) -->
        <div class="col-12 col-lg-5 h-100" id="sign-up-sidebar" style="display: none;">
          <div class="form-cta bg-yellow pl-3 pr-3 pt-4 pb-4 h-100 text-black">
            <!-- Signup section would go here -->
            <div id="recaptcha-box"></div>
            <%= submit_tag 'Request Appointment',
                          class: 'btn btn-lg btn-black text-yellow d-block w-100 text-center',
                          id: 'tuffyRequestApptBtn',
                          disabled: true %>
          </div>
        </div>

        <!-- Right Sidebar (Summary) -->
        <div class="col-12 col-lg-4 h-100" id="summary-sidebar">
          <div class="form-cta bg-black pl-3 pr-3 pt-4 pb-4 h-100">
            <h2 class="h3 text-uppercase text-yellow mb-4">Requested Appointment Summary</h2>
            <div class="form-group d-flex justify-content-between text-white pb-3 mb-3">
              <div class="content">
                <p class="mb-3 small">
                  <strong><%= presenter.account_name %></strong><br>
                  <%= presenter.account_address %><br>
                  <%= presenter.account_phone %>
                </p>
                <p class="m-0 small">
                  <strong>Hours of Operation</strong><br>
                  <ul style="list-style-type: none; padding-left: 0; font-size: 80%;" class="hours">
                    <%= raw presenter.store_hours_html %>
                  </ul>
                </p>
              </div>
              <a href="#" class="tiny text-yellow" onclick="window.change_location(); return false;">Change</a>
            </div>
          </div>
        </div>
      </div>

      <% if presenter.use_mobile_verification? %>
        <%= render 'phone_verification_modal' %>
      <% end %>
    <% end %>
  </section>
</article>

<!-- Initialize JavaScript -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    window.appointmentForm = new AppointmentForm({
      accountId: '<%= presenter.account_id %>',
      filterTimeSlots: <%= presenter.filter_time_slots?.to_s.titleize.downcase %>,
      usePhoneVerification: <%= presenter.use_mobile_verification?.to_s.titleize.downcase %>,
      minimumHours: <%= presenter.minimum_appointment_hours %>,
      storeHours: <%= raw presenter.store_hours_json %>
    });
  });
</script>

File: app/views/appointments/_step_1_services.html.erb

<div class="form-step col-12 col-lg-8 mb-5 mb-lg-0" id="form-step-1">
  <div class="form-flds-container bg-ltgray border">
    <!-- Service Categories Loop -->
    <% presenter.visible_service_categories.each do |category| %>
      <div class="service-category mb-4">
        <h3 class="h4 text-uppercase"><%= category.name %></h3>
        <div class="services-list">
          <% category.account_services.each do |account_service| %>
            <label class="d-flex align-items-center mb-2">
              <input type="checkbox"
                     name="appointment[service_ids][]"
                     id="service_<%= account_service.service.id %>"
                     value="<%= account_service.service.id %>"
                     class="service-checkbox mr-2">
              <span><%= account_service.service.name %></span>
            </label>
          <% end %>
        </div>
      </div>
    <% end %>

    <!-- Wait/Drop-off Selection -->
    <div class="form-flds-container bg-ltgray border mt-2">
      <label for="wait" class="h4 d-block text-uppercase p-2">
        Planning to wait or drop off? <span class="required">*</span>
      </label>
      <select id="wait" name="appointment[wait]" class="required w-100 border h4 p-2">
        <option value="">Select Wait/Drop Off</option>
        <option value="Wait">I will wait for my vehicle</option>
        <option value="Drop-Off">I will drop off my vehicle</option>
      </select>
    </div>
  </div>

  <div class="text-right pt-4">
    <button type="button"
            class="text-uppercase text-black btn btn-yellow btn-lg m-0 d-block w-100 pl-5 pr-5"
            id="apptSelectDateTimeBtn"
            onclick="window.appointmentForm.goToStep(2); return false;">
      Select date/time
    </button>
  </div>
</div>

File: app/views/appointments/_step_2_datetime.html.erb

<div class="form-step col-12 col-lg-8 mb-5 mb-lg-0" id="form-step-2" style="display: none;">
  <div class="form-flds-container bg-ltgray border pt-4 pb-4 pl-2 pr-2 p-lg-4">
    <div class="container full">
      <div class="row">
        <!-- Date Picker -->
        <div class="col-12 calendar-wrap-outer">
          <div class="calendar-wrap position-relative mt-2 mb-5 d-flex justify-content-center justify-lg-content-start align-items-center">
            <div id="date-select"></div>
            <input type="hidden" id="customer_appointment_date"
                   name="appointment[appointment_date]"
                   class="required">
          </div>
        </div>
      </div>

      <!-- Time Selection (conditional) -->
      <div class="row" id="selectHoursSection" style="display:none;">
        <div class="col-12 col-lg-4 mb-3 mb-lg-0">
          <label for="customer_appointment_time" class="h5 d-block text-uppercase">
            Preferred Time <span class="required">*</span>
          </label>
          <select id="customer_appointment_time"
                  name="appointment[appointment_time]"
                  class="w-100 border p-2 h4 required">
            <option>Select Time</option>
            <%= raw presenter.store_hours_options %>
          </select>
        </div>

        <% if presenter.show_towing? %>
          <div class="col-12 col-lg-4 mb-3 mb-lg-0">
            <label for="towing" class="h5 d-block text-uppercase">
              Will you need towing? <span class="required">*</span>
            </label>
            <select id="towing" name="appointment[towing]" class="w-100 border p-2 h4 required">
              <option value="">Select Towing</option>
              <option value="No">No</option>
              <option value="Yes">Yes</option>
            </select>
          </div>
        <% end %>
      </div>

      <!-- Phone Verification (conditional) -->
      <% if presenter.use_mobile_verification? %>
        <div class="row mt-4" id="phoneVerificationSection" style="display:none;">
          <div class="col-12">
            <div class="form-group">
              <label for="customer_phone_step2" class="h5 d-block text-uppercase">
                Phone Number <span class="required">*</span>
              </label>
              <div class="d-flex">
                <input type="text" id="customer_phone_step2"
                       name="customer[phone]"
                       class="w-100 border p-2 h4 required"
                       style="margin-right: 10px;"
                       placeholder="Enter your phone number">
                <button type="button" id="verifyPhoneBtn" class="btn btn-yellow text-black" style="white-space: nowrap;">
                  <span id="verifyBtnText">Verify Phone</span>
                </button>
              </div>
              <div id="phoneVerificationStatus" class="mt-2" style="display: none;">
                <small class="text-success">Phone number verified</small>
              </div>
              <div id="customerFoundStatus" class="mt-2" style="display: none;">
                <small class="text-info">Customer found! We'll pre-fill your information in the next step.</small>
              </div>
            </div>
          </div>
        </div>
      <% end %>
    </div>
  </div>

  <div class="text-right pt-4">
    <button type="button"
            class="text-uppercase text-black btn btn-yellow btn-lg m-0 d-block w-100 pl-5 pr-5"
            id="apptEnterInformationBtn"
            onclick="window.appointmentForm.goToStep(3); return false;">
      Enter your information
    </button>
  </div>
</div>

File: app/views/appointments/_step_3_customer_info.html.erb

<div class="form-step col-12 col-lg-8 mb-5 mb-lg-0" id="form-step-3" style="display: none;">
  <div class="form-flds-container bg-ltgray border pl-4 pr-4 pt-5 pb-3">
    <input type="hidden" name="customer[form_submit]" value="true">

    <!-- Personal Info -->
    <div class="form-group d-block d-lg-flex">
      <div class="form-fld w-100 pr-lg-2 mb-3 mb-lg-0">
        <label for="customer_first_name" class="h5 d-block text-uppercase">
          First name <span class="required">*</span>
        </label>
        <input type="text" id="customer_first_name"
               name="customer[first_name]"
               class="w-100 border h4 p-2 required">
      </div>
      <div class="form-fld w-100 pr-lg-2 mb-3 mb-lg-0">
        <label for="customer_last_name" class="h5 d-block text-uppercase">
          Last name <span class="required">*</span>
        </label>
        <input type="text" id="customer_last_name"
               name="customer[last_name]"
               class="w-100 border h4 p-2 required">
      </div>
    </div>

    <!-- Contact Info -->
    <div class="form-group d-block d-lg-flex">
      <div class="form-fld w-100 pr-lg-2 mb-3 mb-lg-0">
        <label for="customer_email" class="h5 d-block text-uppercase">
          Email address <span class="required">*</span>
        </label>
        <input type="email" id="customer_email"
               name="customer[email]"
               class="w-100 border h4 p-2 required">
      </div>
      <div class="form-fld w-100 pr-lg-2 mb-3 mb-lg-0">
        <label for="customer_phone" class="h5 d-block text-uppercase">
          Phone number <span class="required">*</span>
        </label>
        <input type="text" id="customer_phone"
               name="customer[phone]"
               class="w-100 border h4 p-2 required">
        <small id="phoneVerifiedMessage" class="text-muted" style="display: none;">
          Phone number verified in previous step
        </small>
      </div>
    </div>

    <!-- Costco Membership (conditional) -->
    <% if presenter.require_costco_membership? %>
      <div class="form-group d-block d-lg-flex">
        <div class="form-fld w-100 pr-lg-2 mb-3 mb-lg-0">
          <label for="customer_costco_membership_number" class="h5 d-block text-uppercase">
            Costco Membership Number (if a member)
          </label>
          <input type="text" id="customer_costco_membership_number"
                 name="customer[costco_membership_number]"
                 class="w-100 border h4 p-2">
        </div>
      </div>
    <% end %>

    <!-- Address (conditional via yield) -->
    <% if presenter.require_address? %>
      <%= render 'address_fields' %>
    <% end %>

    <!-- Vehicle Selection -->
    <div class="form-group d-flex">
      <div class="form-fld w-100">
        <label for="vehicle" class="h5 d-block text-uppercase mb-3">
          Your vehicle <span class="required">*</span>
        </label>
        <fieldset>
          <!-- Existing Vehicles (conditional) -->
          <div id="existingVehiclesSection" style="display: none;">
            <div class="mb-3">
              <label class="h6 d-block text-uppercase mb-2">Select an existing vehicle:</label>
              <div id="existingVehiclesList" class="mb-3">
                <% if existing_vehicles.present? %>
                  <% existing_vehicles.each do |vehicle| %>
                    <label class="d-flex align-items-center mb-2">
                      <input type="radio" name="vehicle_selection"
                             value="existing_<%= vehicle.id %>" class="mr-2">
                      <span><%= "#{vehicle.year} #{vehicle.make} #{vehicle.model}" %></span>
                    </label>
                  <% end %>
                <% end %>
              </div>
              <div class="text-center">
                <button type="button" id="addNewVehicleBtn" class="btn btn-yellow text-black btn-sm">
                  + Add a different vehicle
                </button>
              </div>
            </div>
            <hr class="my-3">
          </div>

          <!-- New Vehicle Form -->
          <div id="newVehicleSection">
            <%= render 'vehicle_form' %>
          </div>
        </fieldset>
      </div>
    </div>

    <!-- Problem Description -->
    <div class="form-group d-flex">
      <div class="form-fld w-100">
        <label for="customer_car_issues" class="h5 d-block text-uppercase">
          Tell us a bit about your vehicle problem(s) <span class="required">*</span>
        </label>
        <textarea id="customer_car_issues"
                  name="appointment[car_issues]"
                  class="w-100 border h4 p-2 required"
                  style="height:10rem"></textarea>
      </div>
    </div>
  </div>

  <div class="text-right pt-4">
    <button type="button"
            class="text-uppercase text-black btn btn-yellow btn-lg m-0 d-block w-100 pl-5 pr-5"
            id="apptConfirmAppointmentBtn"
            onclick="window.appointmentForm.goToStep(4); return false;">
      Confirm your Appointment Request
    </button>
  </div>
</div>

File: app/views/appointments/_step_4_confirmation.html.erb

<div class="form-step col-12 col-lg-7" id="form-step-4" style="display: none">
  <div class="form-flds-container bg-ltgray border pl-4 pr-4 pt-5 pb-3">
    <h2 class="h1 mb-5">Does this look right?</h2>

    <!-- Customer & Vehicle Summary -->
    <div class="form-group d-flex justify-content-between text-black pb-3 border-bottom mb-3">
      <div class="content">
        <h3 class="h3 text-uppercase text-black mb-2">
          <span id="confirm_first_name"></span> <span id="confirm_last_name"></span><br>
          <span id="confirm_car_year"></span> <span id="confirm_car_make"></span>
          <span id="confirm_car_model"></span> <span id="confirm_car_option"></span>
        </h3>
        <p class="m-0">
          <span id="confirm_email"></span><br>
          <span id="confirm_phone"></span>
        </p>
      </div>
      <a href="#" class="small" onclick="window.appointmentForm.goToStep(3); return false;">Change</a>
    </div>

    <!-- Location Summary -->
    <div class="form-group d-flex justify-content-between text-black pb-3 border-bottom mb-3">
      <div class="content">
        <h3 class="h3 text-uppercase text-black mb-2">Location</h3>
        <p class="m-0">
          <strong><%= presenter.account_name %></strong><br>
          <%= presenter.account_address %><br>
          <%= presenter.account_phone %>
        </p>
      </div>
      <a href="#" class="small" onclick="window.appointmentForm.change_location(); return false;">Change</a>
    </div>

    <!-- Services Summary -->
    <div class="form-group d-flex justify-content-between text-black pb-3 border-bottom mb-3">
      <div class="content">
        <h3 class="h3 text-uppercase text-black mb-2">Services</h3>
        <p class="m-0" id="confirm_services"></p>
      </div>
      <a href="#" class="small" onclick="window.appointmentForm.goToStep(1); return false;">Edit</a>
    </div>

    <!-- Date/Time Summary -->
    <div class="form-group d-flex justify-content-between text-black pb-3 border-bottom mb-3">
      <div class="content">
        <h3 class="h3 text-uppercase text-black mb-2">Date/Time</h3>
        <p class="m-0">
          <span id="confirm_appointment_date"></span> @ <span id="confirm_appointment_time"></span>
          <br><span id="confirm_wait"></span>
          <% if presenter.show_towing? %>
            <br><span id="confirm_towing"></span> towing required
          <% end %>
        </p>
      </div>
      <a href="#" class="small" onclick="window.appointmentForm.goToStep(2); return false;">Edit</a>
    </div>

    <!-- Notes Summary -->
    <div class="form-group d-flex justify-content-between text-black pb-3">
      <div class="content">
        <h3 class="h3 text-uppercase text-black mb-2">Your notes</h3>
        <p class="m-0" id="confirm_car_issues"></p>
      </div>
      <a href="#" class="small" onclick="window.appointmentForm.goToStep(3); return false;">Edit</a>
    </div>
  </div>
</div>

Step 4: Extract JavaScript to External File

File: app/assets/javascripts/appointment_form.js

class AppointmentForm {
  constructor(options = {}) {
    this.accountId = options.accountId;
    this.filterTimeSlots = options.filterTimeSlots;
    this.usePhoneVerification = options.usePhoneVerification;
    this.minimumHours = options.minimumHours || 24;
    this.storeHours = options.storeHours || {};

    this.currentStep = 1;
    this.phoneVerified = false;
    this.selectedExistingVehicle = null;

    this.init();
  }

  init() {
    this.setupEventListeners();
    this.initDatepicker();
    this.setupStepNavigation();
  }

  setupEventListeners() {
    // Service checkboxes
    document.querySelectorAll('.service-checkbox').forEach(checkbox => {
      checkbox.addEventListener('change', () => this.updateServicesSummary());
    });

    // Form field changes
    document.querySelectorAll('input[name^="customer["], select[name^="appointment["]').forEach(field => {
      field.addEventListener('change', () => this.updateConfirmationDisplay(field));
    });

    // Wait/Drop-off
    document.getElementById('wait').addEventListener('change', (e) => {
      document.getElementById('confirm_wait').textContent = e.target.value;
    });

    // Phone verification
    if (this.usePhoneVerification) {
      document.getElementById('verifyPhoneBtn').addEventListener('click', () => {
        this.verifyPhone();
      });
    }

    // Existing vehicles
    if (document.getElementById('existingVehiclesList').children.length > 0) {
      document.querySelectorAll('input[name="vehicle_selection"]').forEach(radio => {
        radio.addEventListener('change', (e) => {
          this.selectExistingVehicle(e.target.value);
        });
      });
      document.getElementById('addNewVehicleBtn').addEventListener('click', () => {
        this.switchToNewVehicle();
      });
    }
  }

  initDatepicker() {
    // Initialize jQuery UI datepicker
    // Implement date selection logic
  }

  setupStepNavigation() {
    // Setup step transitions
  }

  goToStep(step) {
    if (!this.validateStep(this.currentStep)) {
      return false;
    }

    this.currentStep = step;
    this.showStep(step);
    return false;
  }

  showStep(step) {
    // Hide all steps
    document.querySelectorAll('[id^="form-step-"]').forEach(el => {
      el.style.display = 'none';
    });

    // Show selected step
    document.getElementById(`form-step-${step}`).style.display = 'block';

    // Update progress indicator
    this.updateProgressIndicator(step);

    // Update sidebars
    if (step === 4) {
      document.getElementById('summary-sidebar').style.display = 'none';
      document.getElementById('sign-up-sidebar').style.display = 'block';
    } else {
      document.getElementById('sign-up-sidebar').style.display = 'none';
      document.getElementById('summary-sidebar').style.display = 'block';
    }

    // Scroll to top
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }

  validateStep(step) {
    // Complex validation logic
    // Returns true if valid, false otherwise
  }

  updateConfirmationDisplay(field) {
    // Update confirmation section as user types
  }

  updateServicesSummary() {
    // Update services display
  }

  verifyPhone() {
    // AJAX call to verify phone number
  }

  selectExistingVehicle(vehicleId) {
    // Handle existing vehicle selection
  }

  switchToNewVehicle() {
    // Switch to new vehicle form
  }

  change_location() {
    // Reopen location selection
  }
}

Part 5: Risks and Mitigation

High Risk Areas

Risk Severity Mitigation
Complex JavaScript Behavior CRITICAL Extract to external file, comprehensive testing, unit tests for validation logic
Form State Management HIGH Use Stimulus controllers or React component for state
AJAX Calls (time slots, phone verify) HIGH Mock endpoints in tests, use fetch API instead of jQuery
Date Validation HIGH Use Rails date validation + JavaScript, test edge cases
Existing Vehicle Lookup MEDIUM Create service for vehicle lookup, test with real data
Phone Verification MEDIUM Create SMS service abstraction, test verification flow
Google Tag Manager LOW Keep GTM code separate, can be added back easily
Accessibility MEDIUM Add ARIA labels, test with screen readers

Part 6: Implementation Timeline

Phase 1: Setup & Views (2 weeks)

  • Create presenter class
  • Create view partials
  • Create controller
  • Basic HTML structure

Phase 2: JavaScript & Validation (2 weeks)

  • Extract inline JavaScript to Stimulus controller
  • Implement step validation
  • Implement date picker integration
  • Implement form field synchronization

Phase 3: Services & API Integration (1.5 weeks)

  • Create validation services
  • Create time slot fetching service
  • Create phone verification service
  • Create appointment creation service

Phase 4: Testing & Polish (1.5 weeks)

  • Unit tests for services
  • Integration tests for form flow
  • E2E tests with Capybara
  • Performance testing
  • Browser compatibility testing

Phase 5: Rollout (1 week)

  • Feature flag configuration
  • Beta testing with test account
  • Gradual rollout to production
  • Monitor error rates
  • Collect user feedback

Total: 8 weeks for complete migration


Part 7: Conclusion

Block 122 represents the highest complexity in PISTN's Block system. Its migration path shows:

  1. Decomposition: Break monolithic Block into separate concerns (views, controller, services)
  2. Modernization: Replace inline JavaScript with standard Rails patterns (Stimulus, fetch API)
  3. Testability: Extract business logic into services that can be tested independently
  4. Maintainability: Standard Rails conventions make code easier to understand and modify

Success Criteria:

  • ✅ Feature parity with original Block 122
  • ✅ 100% form submission success rate
  • ✅ Phone verification flow works
  • ✅ Existing vehicle lookup functional
  • ✅ All validations pass
  • ✅ Performance within 10% of original
  • ✅ Accessibility meets WCAG 2.1 AA

If Block 122 can be migrated successfully, ALL blocks can be migrated.


Status: Architectural analysis complete - ready for implementation Next Steps:

  1. Create Stimulus controller for form state management
  2. Implement form partials with real data
  3. Build validation services
  4. Begin E2E testing framework