# 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**: ```javascript // 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**: ```javascript page.intro_or_default_text // Customizable intro copy ``` **Model-Level Properties**: ```javascript 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) ```javascript 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` ```ruby 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` ```ruby 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` ```erb <%= render 'appointment_form', presenter: @presenter, existing_vehicles: @existing_vehicles %> ``` #### File: `app/views/appointments/_appointment_form.html.erb` ```erb

Schedule an Appointment

<%= form_with url: appointments_path, method: :post, id: 'appt_form', local: true, class: 'container' do |f| %>
  • Service(s)
  • Date/Time
  • Your Info
  • Confirm
<%= presenter.intro_text %>
<% if presenter.show_online_scheduling_promo? %>
<%= presenter.online_scheduling_promo_message %>
<% end %> <%= render 'step_1_services', presenter: presenter %> <%= render 'step_2_datetime', presenter: presenter %> <%= render 'step_3_customer_info', presenter: presenter, existing_vehicles: existing_vehicles %> <%= render 'step_4_confirmation', presenter: presenter %>

Requested Appointment Summary

<%= presenter.account_name %>
<%= presenter.account_address %>
<%= presenter.account_phone %>

Hours of Operation

    <%= raw presenter.store_hours_html %>

Change
<% if presenter.use_mobile_verification? %> <%= render 'phone_verification_modal' %> <% end %> <% end %>
``` #### File: `app/views/appointments/_step_1_services.html.erb` ```erb
<% presenter.visible_service_categories.each do |category| %>

<%= category.name %>

<% category.account_services.each do |account_service| %> <% end %>
<% end %>
``` #### File: `app/views/appointments/_step_2_datetime.html.erb` ```erb ``` #### File: `app/views/appointments/_step_3_customer_info.html.erb` ```erb ``` #### File: `app/views/appointments/_step_4_confirmation.html.erb` ```erb ``` ### Step 4: Extract JavaScript to External File File: `app/assets/javascripts/appointment_form.js` ```javascript 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