PageSourceSearch

https://www.puntacanatransfer.com.do/assets/controllers/booking_step_controller-c599d22e.js

js puntacanatransfer.com.do collected 2026-09-30 12:48:30 UTC 24,472 bytes, 677 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import { requestPreview } from "lib/booking_preview_client"
3import { track, trackSecondary } from "lib/analytics"
4
5// Handles step 2 interactions: segments, extras, passenger data and preview totals.
6export default class extends Controller {
7  static targets = [
8    "segment",
9    "extraToggle",
10    "baseTotal",
11    "extrasTotal",
12    "grandTotal",
13    "summaryNote",
14    "paymentMethod",
15    "payment",
16    "cardTotal",
17    "cardNote"
18  ]
19
20  static values = {
21    previewEndpoint: String
22  }
23
24  connect() {
25    this.abortController = null
26    this.debouncedRefresh = this.debounce(() => this.refreshPreview(), 250)
27
28    this.sentCheckoutDetailsStepView = false
29    
30    this.boundChangeHandler = (event) => {
31      // Keep date constraints fresh on any change
32      this.adjustDateMinsToLocalToday()
33      this.enforceRoundTripDateOrdering()
34      // Toggle return date section visibility based on service type
35      this.toggleReturnDateSection()
36      this.trackDateSelected(event)
37      this.debouncedRefresh()
38      // Reflect passenger count in the URL so shared links preserve the selection
39      try {
40        this.updateUrlWithPassengers()
41      } catch (e) {
42        // no-op
43      }
44    }
45    this.element.addEventListener("input", this.boundChangeHandler)
46    this.element.addEventListener("change", this.boundChangeHandler)
47    // Listen for Flowbite datepicker change events
48    this.element.addEventListener("changeDate", this.boundChangeHandler)
49    this.boundStepChangeHandler = (e) => {
50      this.toggleReturnDateSection()
51      const step = Number(e?.detail?.step)
52      if (!Number.isNaN(step)) this.maybeTrackDetailsStepView(step)
53    }
54    this.element.addEventListener("transfer-form:step-changed", this.boundStepChangeHandler)
55    // Validate round-trip return date before form submission
56    const form = this.element.closest("form")
57    if (form) {
58      this.boundSubmitHandler = (e) => this.validateBeforeSubmit(e)
59      form.addEventListener("submit", this.boundSubmitHandler)
60      // Also listen for Turbo submit events in case Turbo intercepts the submit
61      document.addEventListener("turbo:submit-start", this.boundSubmitHandler)
62    } else {
63    }
64    // Ensure date pickers allow selecting the user's local "today" (override server-side time zone skew)
65    this.adjustDateMinsToLocalToday()
66    // Apply round-trip constraint: inbound date >= last outbound date
67    this.enforceRoundTripDateOrdering()
68    // Toggle return date section visibility based on service type
69    this.toggleReturnDateSection()
70
71    // If we land directly on step 2 (details), emit the view event.
72    const currentStepInput = form?.querySelector("input[name='booking[current_step]']")
73    const currentStep = Number(currentStepInput?.value)
74    if (!Number.isNaN(currentStep)) this.maybeTrackDetailsStepView(currentStep)
75
76    this.refreshPreview()
77  }
78
79  updateUrlWithPassengers() {
80    const root = this.element.closest("form")
81    if (!root || typeof window === 'undefined') return
82    const passengerInputs = Array.from(root.querySelectorAll("[name='booking[passengers]']"))
83    const passengers = passengerInputs.reduce((memo, input) => {
84      const val = parseInt(input.value || "0", 10)
85      return (!Number.isNaN(val) && val > 0) ? val : memo
86    }, 0)
87
88    try {
89      const url = new URL(window.location.href)
90      const params = url.searchParams
91      if (passengers && passengers > 0) {
92        params.set('booking[passengers]', String(passengers))
93      } else {
94        params.delete('booking[passengers]')
95      }
96      // Replace state to avoid polluting history while keeping shareable URL
97      const newUrl = url.pathname + (params.toString() ? `?${params.toString()}` : '') + (url.hash || '')
98      window.history.replaceState({}, document.title, newUrl)
99    } catch (e) {
100      // If URL constructor isn't available or other error, ignore
101    }
102  }
103
104  maybeTrackDetailsStepView(currentStep) {
105    if (this.sentCheckoutDetailsStepView) return
106    // Transfer form UI step 2 corresponds to the GA4 checkout step 3 (details)
107    if (Number(currentStep) !== 2) return
108
109    track(
110      "checkout_step_view",
111      {
112        step: 3,
113        step_name: "details"
114      },
115      { dedupeTtlMs: 30_000 }
116    )
117    this.sentCheckoutDetailsStepView = true
118  }
119
120  disconnect() {
121    this.element.removeEventListener("input", this.boundChangeHandler)
122    this.element.removeEventListener("change", this.boundChangeHandler)
123    this.element.removeEventListener("changeDate", this.boundChangeHandler)
124    const form = this.element.closest("form")
125    if (form && this.boundSubmitHandler) {
126      form.removeEventListener("submit", this.boundSubmitHandler)
127      document.removeEventListener("turbo:submit-start", this.boundSubmitHandler)
128    }
129    if (this.boundStepChangeHandler) {
130      this.element.removeEventListener("transfer-form:step-changed", this.boundStepChangeHandler)
131    }
132    if (this.abortController) this.abortController.abort()
133  }
134
135  validateBeforeSubmit(e) {
136    // Get the form - could be from submit event or Turbo event
137    let form = e.target
138    if (e.type === "turbo:submit-start") {
139      form = e.detail.formSubmission.form
140    }
141    if (!form) return
142
143    trackSecondary(
144      "booking_form_submit",
145      {
146        checkout_step: 2,
147        step_name: "details",
148      },
149      { dedupeTtlMs: 2_000, dataLayerEvent: true }
150    )
151
152    // Log all segment date inputs before submission
153    const outboundDateInput = form.querySelector("input[name='booking[segments][0][date]']")
154    const returnDateInput = form.querySelector("input[name='booking[segments][1][date]']")
155    if (!outboundDateInput?.value || !returnDateInput?.value) {
156      // Don't block submission - let server validation handle it
157      // This allows us to see the actual data being sent
158    }
159  }
160
161  trackDateSelected(event) {
162    const target = event?.target
163    if (!target || !target.matches?.("input[name*='[date]']")) return
164    if (!target.value) return
165
166    trackSecondary(
167      "date_selected",
168      {
169        field_name: target.name,
170        date: target.value,
171      },
172      { dedupeTtlMs: 2_000, dataLayerEvent: true }
173    )
174  }
175
176  async refreshPreview() {
177    const endpoint = this.previewEndpointValue || "/api/v1/bookings/preview"
178    const payload = this.buildPayload()
179    if (!payload) return
180
181    if (this.abortController) this.abortController.abort()
182    this.abortController = new AbortController()
183
184    try {
185      const result = await requestPreview(endpoint, payload, this.abortController.signal)
186      this.renderTotals(result)
187      this.element.dispatchEvent(new CustomEvent("booking-step:preview-updated", { detail: result, bubbles: true }))
188    } catch (error) {
189      if (error?.name === "AbortError") return
190      console.error("[BookingStep] refreshPreview error", error)
191      this.renderTotals(null)
192      this.element.dispatchEvent(new CustomEvent("booking-step:preview-error", { detail: error, bubbles: true }))
193    }
194  }
195
196  debounce(fn, wait = 200) {
197    let timeout
198    return (...args) => {
199      clearTimeout(timeout)
200      timeout = setTimeout(() => {
201        Promise.resolve(fn.apply(this, args)).catch((error) => {
202          console.error("[BookingStep] debounce error", error)
203          this.element.dispatchEvent(new CustomEvent("booking-step:preview-error", { detail: error, bubbles: true }))
204        })
205      }, wait)
206    }
207  }
208
209  buildPayload() {
210    const root = this.element.closest("form")
211    if (!root) return null
212
213    const passengerInputs = Array.from(root.querySelectorAll("[name='booking[passengers]']"))
214    const passengers = passengerInputs.reduce((memo, input) => {
215      const val = parseInt(input.value || "0", 10)
216      return (!Number.isNaN(val) && val > 0) ? val : memo
217    }, 0)
218    if (!passengers || Number.isNaN(passengers)) return null
219
220    const serviceType = this.resolveServiceType(root)
221    // Prefer hidden Step 2 input so we don't rely on potentially blank selects
222    const vehicleInput = root.querySelector("input[type='hidden'][name='booking[vehicle_id]']")
223      || this.element.querySelector("[name='booking[vehicle_id]']")
224      || root.querySelector("[name='booking[vehicle_id]']")
225    const originInput = root.querySelector("[name='booking[origin]']")
226    const destinationInput = root.querySelector("[name='booking[destination]']")
227
228    const origin = originInput ? originInput.dataset.zoneId : null
229    const destination = destinationInput ? destinationInput.dataset.zoneId : null
230
231    const segmentContainers = serviceType === "round_trip" ? this.segmentTargets : this.segmentTargets.slice(0, 1)
232
233    const segments = segmentContainers.map((container, index) => {
234      const seg = this.serializeSegment(container)
235      seg.sequence = index + 1
236
237      // Force directions based on service type and position
238      seg.direction = serviceType === "round_trip" ? (index === 0 ? "outbound" : "inbound") : "outbound"
239
240      // Assign zone ids, swapping for inbound legs when round trip
241      const isInbound = serviceType === "round_trip" && index === 1
242      const originZone = isInbound ? destination : origin
243      const destinationZone = isInbound ? origin : destination
244      seg.origin_zone_id = originZone
245      seg.destination_zone_id = destinationZone
246      return seg
247    })
248    if (segments.length === 0) return null
249    // Require zone ids and date for each segment before calling preview to avoid 400s
250    const hasRequiredSegmentFields = segments.every((s) => {
251      const oz = (s.origin_zone_id || "").toString().trim()
252      const dz = (s.destination_zone_id || "").toString().trim()
253      return oz && dz
254    })
255    if (!hasRequiredSegmentFields) return null
256
257    const extras = []
258    const optOutCodes = []
259    this.extraToggleTargets.forEach((toggle) => {
260      const code = toggle.dataset.code
261      if (!code) return
262      const selected = toggle.checked
263      if (!selected) optOutCodes.push(code)
264      const amountField = this.findSiblingInput(toggle, `booking[extras]`, "amount_cents")
265      const amount = amountField ? parseInt(amountField.value || "0", 10) : null
266      extras.push({ code, selected, amount_cents: amount })
267    })
268
269    const payload = {
270      passengers,
271      service_type: serviceType,
272      segments,
273      opt_out_codes: optOutCodes,
274      extras
275    }
276
277    if (vehicleInput && vehicleInput.value) {
278      payload.vehicle_id = vehicleInput.value
279    }
280
281    return payload
282  }
283
284  serializeSegment(container) {
285    const data = {}
286    const dateInput = container.querySelector("input[name*='[date]']")
287    
288    container.querySelectorAll("input:not([disabled]), select:not([disabled])").forEach((field) => {
289      const name = field.getAttribute("name")
290      if (!name) return
291      const match = name.match(/booking\[segments\]\[(\d+)\]\[(.+)\]/)
292      if (!match) return
293      const key = match[2]
294      const value = field.value
295      data[key] = value
296    })
297    return data
298  }
299
300  findSiblingInput(toggle, scope, attribute) {
301    const regex = new RegExp(`${scope}\\\\[(\\\\d+)\\\\]\\\\[${attribute}\\\\]`)
302    const siblings = toggle.parentElement?.querySelectorAll(`input[name*='${scope}']`) || []
303    return Array.from(siblings).find((input) => regex.test(input.name)) || null
304  }
305
306  renderTotals(result) {
307    if (!result) {
308      this.updateTotalTarget(this.baseTotalTarget, null)
309      this.updateTotalTarget(this.extrasTotalTarget, null)
310      this.updateTotalTarget(this.grandTotalTarget, null)
311      this.updateCardTotal(null)
312      if (this.hasSummaryNoteTarget) {
313        this.summaryNoteTarget.textContent = this.summaryPlaceholderText()
314        this.summaryNoteTarget.classList.remove("hidden")
315      }
316      return
317    }
318
319    const formatter = this.currencyFormatter(result.currency || "USD")
320    this.updateTotalTarget(this.baseTotalTarget, formatter(result.base_total_cents || 0))
321    this.updateTotalTarget(this.extrasTotalTarget, formatter(result.extras_total_cents || 0))
322    this.updateTotalTarget(this.grandTotalTarget, formatter(result.grand_total_cents || 0))
323    this.updateCardTotal(result)
324    if (this.hasSummaryNoteTarget) {
325      this.summaryNoteTarget.textContent = ""
326      this.summaryNoteTarget.classList.add("hidden")
327    }
328  }
329
330  updateTotalTarget(target, value) {
331    if (!target) return
332    const displayValue = value == null ? "—" : value
333    target.textContent = displayValue
334  }
335
336  updateCardTotal(result) {
337    if (!this.hasCardTotalTarget) return
338
339    if (!result) {
340      this.cardTotalTarget.textContent = "—"
341      // Still show the trip type even if no result
342      if (this.hasCardNoteTarget) {
343        const serviceTypeInput = this.element.querySelector("input[name='booking[service_type]']:checked")
344        const serviceType = serviceTypeInput?.value || 'one_way'
345        if (serviceType === 'round_trip') {
346          this.cardNoteTarget.textContent = this.roundTripNoteText()
347        } else {
348          this.cardNoteTarget.textContent = this.oneWayNoteText()
349        }
350        this.cardNoteTarget.classList.remove("hidden")
351      }
352      return
353    }
354
355    const formatter = this.currencyFormatter(result.currency || "USD")
356    this.cardTotalTarget.textContent = formatter(result.base_total_cents || result.grand_total_cents || 0)
357
358    if (this.hasCardNoteTarget) {
359      if ((result.service_type || "").toString() === "round_trip") {
360        this.cardNoteTarget.textContent = this.roundTripNoteText()
361        this.cardNoteTarget.classList.remove("hidden")
362      } else {
363        this.cardNoteTarget.textContent = this.oneWayNoteText()
364        this.cardNoteTarget.classList.remove("hidden")
365      }
366    }
367  }
368
369  roundTripNoteText() {
370    try {
371      const noteTarget = this.hasCardNoteTarget ? this.cardNoteTarget : null
372      const text = noteTarget?.dataset?.roundTripNote
373      return text || "round trip"
374    } catch (e) {
375      return "round trip"
376    }
377  }
378
379  oneWayNoteText() {
380    try {
381      const noteTarget = this.hasCardNoteTarget ? this.cardNoteTarget : null
382      const text = noteTarget?.dataset?.oneWayNote
383      return text || "×1 (one-way)"
384    } catch (e) {
385      return "×1 (one-way)"
386    }
387  }
388
389  currencyFormatter(currency) {
390    let formatter
391    try {
392      formatter = new Intl.NumberFormat(undefined, { style: "currency", currency })
393    } catch (e) {
394      formatter = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD" })
395    }
396    return (cents) => formatter.format((cents || 0) / 100)
397  }
398
399  debounce(fn, wait = 200) {
400    let timeout
401    return (...args) => {
402      clearTimeout(timeout)
403      timeout = setTimeout(() => fn.apply(this, args), wait)
404    }
405  }
406
407  // Force all date inputs within this step to have min=today (local), so same-day booking is selectable
408  adjustDateMinsToLocalToday() {
409    try {
410      const now = new Date()
411      const yyyy = now.getFullYear()
412      const mm = String(now.getMonth() + 1).padStart(2, "0")
413      const dd = String(now.getDate()).padStart(2, "0")
414      const todayStr = `${yyyy}-${mm}-${dd}`
415      this.element.querySelectorAll("input[name*='[date]']").forEach((input) => {
416        this.applyDatepickerMin(input, todayStr)
417      })
418    } catch (e) {
419      // no-op: if the browser doesn't support date inputs or something goes wrong, ignore
420    }
421  }
422
423  // Ensure departure/return date is not before arrival/outbound for round trips
424  enforceRoundTripDateOrdering() {
425    try {
426      const root = this.element.closest("form")
427      if (!root) return
428      const serviceType = this.resolveServiceType(root)
429      if (serviceType !== "round_trip") return
430
431      // Compute local today string
432      const now = new Date()
433      const yyyy = now.getFullYear()
434      const mm = String(now.getMonth() + 1).padStart(2, "0")
435      const dd = String(now.getDate()).padStart(2, "0")
436      const todayStr = `${yyyy}-${mm}-${dd}`
437
438      // Collect outbound dates and find max
439      let outboundDates = []
440      this.segmentTargets.forEach((container, index) => {
441        const dateInput = container.querySelector("input[name*='[date]']")
442        if (!dateInput) return
443        const direction = this.segmentDirection(container, index)
444        const value = (dateInput.value || "").trim()
445        if (direction === "outbound" && value.match(/^\d{4}-\d{2}-\d{2}$/)) {
446          outboundDates.push(value)
447        }
448      })
449      const outboundMax = outboundDates.sort().slice(-1)[0] || todayStr
450      const minForInbound = outboundMax > todayStr ? outboundMax : todayStr
451
452      // Apply min to inbound date fields
453      this.segmentTargets.forEach((container, index) => {
454        const dateInput = container.querySelector("input[name*='[date]']")
455        if (!dateInput) return
456        const direction = this.segmentDirection(container, index)
457        if (direction === "inbound") {
458          this.applyDatepickerMin(dateInput, minForInbound)
459          // If current value is before min, clear it to force re-selection
460          const v = (dateInput.value || "").trim()
461          if (v && v < minForInbound) {
462            dateInput.value = ""
463            if (dateInput.datepicker && typeof dateInput.datepicker.setDate === "function") {
464              dateInput.datepicker.setDate({ clear: true })
465            }
466          }
467        }
468      })
469    } catch (e) {
470      // ignore; non-fatal UI enhancement
471    }
472  }
473
474  summaryPlaceholderText() {
475    try {
476      const el = document.querySelector("#step-2")
477      const text = el?.dataset?.summaryPlaceholder
478      if (text) return text
479    } catch (e) { }
480    return "Select a vehicle to see totals"
481  }
482
483  applyDatepickerMin(input, minValue) {
484    if (!input || !minValue) return
485    const normalized = this.normalizeDateString(minValue)
486    if (!normalized) return
487    input.setAttribute("min", normalized)
488    input.setAttribute("data-min-date", normalized)
489    if (input.hasAttribute("datepicker")) {
490      input.setAttribute("datepicker-min-date", normalized)
491      const picker = this.findFlowbiteDatepicker(input)
492      if (picker && typeof picker.setOptions === "function") {
493        const minDateObj = this.castDatepickerValue(normalized)
494        picker.setOptions({ minDate: minDateObj || normalized })
495        if (picker.config && minDateObj) {
496          picker.config.minDate = minDateObj
497        }
498      }
499    }
500  }
501
502  resolveServiceType(root = null) {
503    const scope = root || this.element.closest("form")
504    if (!scope) return "one_way"
505    const input = scope.querySelector("input[name='booking[service_type]']:checked")
506      || scope.querySelector("input[name='booking[service_type]'][type='hidden']")
507      || scope.querySelector("[name='booking[service_type]']")
508    return input && input.value ? input.value : "one_way"
509  }
510
511  segmentDirection(container, index) {
512    const dirInput = container ? container.querySelector("input[name*='[direction]']") : null
513    let datasetDirection = ""
514    if (container && container.dataset && container.dataset.direction) {
515      datasetDirection = container.dataset.direction
516    }
517    const directionValue = (dirInput && dirInput.value ? dirInput.value : datasetDirection || "").toLowerCase()
518    if (directionValue) return directionValue
519    return index === 0 ? "outbound" : "inbound"
520  }
521
522  findFlowbiteDatepicker(input) {
523    if (!input) return null
524    if (input.datepicker) return input.datepicker
525    const id = input.getAttribute("id")
526    if (!id) return null
527    const instances = window.FlowbiteInstances
528    if (instances && typeof instances.getInstance === "function") {
529      const wrapper = instances.getInstance("Datepicker", id)
530      if (wrapper && typeof wrapper.getDatepickerInstance === "function") {
531        return wrapper.getDatepickerInstance()
532      }
533    }
534    return null
535  }
536
537  normalizeDateString(value) {
538    if (!value) return null
539    if (value instanceof Date) {
540      const yyyy = value.getFullYear()
541      const mm = String(value.getMonth() + 1).padStart(2, "0")
542      const dd = String(value.getDate()).padStart(2, "0")
543      return `${yyyy}-${mm}-${dd}`
544    }
545    if (typeof value === "string") {
546      const trimmed = value.trim()
547      if (!trimmed) return null
548      if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed
549      const parsed = new Date(trimmed)
550      if (!Number.isNaN(parsed.getTime())) {
551        return this.normalizeDateString(parsed)
552      }
553    }
554    return null
555  }
556
557  castDatepickerValue(value) {
558    if (!value) return null
559    if (value instanceof Date) return value
560    const normalized = this.normalizeDateString(value)
561    if (!normalized) return null
562    const [yyyy, mm, dd] = normalized.split("-").map((part) => parseInt(part, 10))
563    if (!yyyy || !mm || !dd) return null
564    const date = new Date(yyyy, mm - 1, dd)
565    return Number.isNaN(date.getTime()) ? null : date
566  }
567
568  handleOpenTimePicker(event) {
569    const trigger = event.currentTarget
570    if (!trigger) return
571    const inputId = trigger.dataset.inputId
572    if (!inputId) return
573
574    const field = document.getElementById(inputId)
575    if (!field) return
576
577    if (typeof field.showPicker === "function") {
578      field.showPicker()
579      return
580    }
581
582    field.focus()
583    try {
584      field.click()
585    } catch (e) {
586      // no-op: some browsers do not allow programmatic clicks here.
587    }
588  }
589
590  logFullFormData(prefix = 'Full Form Data') {
591    const form = this.element.closest('form')
592    if (!form) {
593      return
594    }
595    const formData = new FormData(form)
596    const data = {}
597    for (let [key, value] of formData.entries()) {
598      if (key.startsWith('booking[')) {
599        data[key] = value
600      }
601    }
602  }
603
604  paymentMethodChanged(event) {
605    const selectedValue = event.target.value
606    
607    // Toggle Stripe wrapper visibility
608    this.toggleStripeWrapper(selectedValue === 'stripe')
609    
610    this.debouncedRefresh()
611  }
612
613  toggleStripeWrapper(shouldShow) {
614    const wrapper = document.getElementById('stripe-card-wrapper')
615    if (!wrapper) return
616    
617    const currentlyHidden = wrapper.classList.contains('hidden')
618    if (shouldShow && currentlyHidden) {
619      wrapper.classList.remove('hidden')
620      // Attempt to mount Stripe elements if controller exists
621      const stripeController = this.findStripeCardController()
622      if (stripeController && !stripeController.mounted) {
623        stripeController.ensureStripeLoaded()
624      }
625    } else if (!shouldShow && !currentlyHidden) {
626      wrapper.classList.add('hidden')
627    }
628  }
629
630  findStripeCardController() {
631    const wrapper = document.getElementById('stripe-card-wrapper')
632    if (!wrapper) return null
633    return this.application.getControllerForElementAndIdentifier(wrapper, 'stripe-card')
634  }
635
636  toggleReturnDateSection() {
637    const form = this.element.closest("form")
638    if (!form) return
639
640    const currentStepInput = form.querySelector("input[name='booking[current_step]']")
641    const currentStep = currentStepInput?.value || ""
642    const isStepTwo = currentStep === "2"
643
644    // Find the service type value
645    const serviceTypeInput = form.querySelector("input[name='booking[service_type]']:checked")
646      || form.querySelector("input[name='booking[service_type]'][type='hidden']")
647    const isRoundTrip = serviceTypeInput && (serviceTypeInput.value === "round_trip" || serviceTypeInput.dataset.serviceType === "round_trip")
648
649    // Find the return date section
650    const returnDateSection = document.getElementById("return-date-section")
651    if (returnDateSection) {
652      returnDateSection.style.display = isRoundTrip ? "block" : "none"
653    }
654
655    // Disable all return inputs until Step 2 is visible (clarifies auto-submit behavior)
656    const stepTwoSection = document.getElementById("step-2")
657    const stepTwoVisible = stepTwoSection && !stepTwoSection.classList.contains("hidden")
658    const returnDateInputs = form.querySelectorAll("#return-date-section input, #return-date-section select")
659    returnDateInputs.forEach((input) => {
660      if (isRoundTrip && isStepTwo) {
661        input.removeAttribute("disabled")
662      } else {
663        input.setAttribute("disabled", "disabled")
664      }
665    })
666
667    // Update the required attribute only while Step 2 is active and round-trip
668    const returnDateInput = form.querySelector("input[name='booking[segments][1][date]']")
669    if (returnDateInput) {
670      if (isRoundTrip && isStepTwo) {
671        returnDateInput.setAttribute("required", "required")
672      } else {
673        returnDateInput.removeAttribute("required")
674      }
675    }
676  }
677}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.