PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / templates / partials / booking-form-fields.php

booking-form-fields.php in Yatra – Travel Booking & Tour Operator Software 3.0.9, at templates/partials/booking-form-fields.php

991 lines 46.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking Form Fields Partial
4 *
5 * Renders form fields based on configuration from Settings -> Booking Form
6 * Dynamic form customization requires the Dynamic Form Field module (Pro).
7 *
8 * @package Yatra
9 *
10 * Expected variables:
11 * - $trip_id
12 * - $trip_slug (optional)
13 * - $total_travelers
14 * - $deposit_required
15 * - $deposit_percentage
16 * - $partial_payment
17 * - $partial_payment_percentage
18 * - $enabled_gateways
19 *
20 * Form Configuration Structure:
21 * - contact_form: Lead traveler contact information (prefix: contact_)
22 * - emergency_contact_form: Emergency contact details (prefix: emergency_)
23 * - traveler_form: Individual traveler details (array format: travelers[1][field_id])
24 */
25
26 if (!defined('ABSPATH')) {
27 exit;
28 }
29
30 // Check if Dynamic Form Field module is enabled (Pro feature)
31 $is_dynamic_form_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
32
33 // Get form configuration - returns default config if module disabled, custom config if enabled
34 $form_config = yatra_get_booking_form_config();
35
36
37 // Country list — pulls from the canonical FormatHelper source so every
38 // dropdown (admin, public booking, Pro modules) shows the same full
39 // ISO-3166-1 list. Operators that want a "popular countries" subset
40 // or custom ordering apply the `yatra_countries_list` filter.
41 $countries = \Yatra\Helpers\FormatHelper::getCountries();
42
43 /**
44 * Render a single form field based on configuration
45 *
46 * @param array $field Field configuration
47 * @param string $prefix Field name prefix (e.g., 'contact_', 'emergency_')
48 * @param array $countries Country list for country fields
49 * @param string $custom_name Optional custom field name (for array-style names like travelers[1][field_id])
50 * @param string $custom_id Optional custom field ID
51 */
52 if (!function_exists('yatra_render_form_field')) {
53 function yatra_render_form_field($field, $prefix = '', $countries = [], $custom_name = null, $custom_id = null) {
54 if (empty($field['enabled'])) {
55 return;
56 }
57
58 // Display-only text block: render admin-authored content (safe HTML) at this
59 // position between fields. It is NOT an input — no label, name, value or
60 // required handling — so it submits nothing and is never validated.
61 if (($field['type'] ?? '') === 'text_block') {
62 $tb_width = 'yatra-field-full';
63 if (($field['width'] ?? '') === 'half') {
64 $tb_width = 'yatra-field-half';
65 } elseif (($field['width'] ?? '') === 'third') {
66 $tb_width = 'yatra-field-third';
67 }
68 ?>
69 <div class="yatra-form-group yatra-form-text-block <?php echo esc_attr($tb_width); ?>" data-applies-to="<?php echo esc_attr($field['applies_to'] ?? 'all'); ?>">
70 <?php echo wpautop(wp_kses_post(yatra_translate_form_string($field['content'] ?? ''))); ?>
71 </div>
72 <?php
73 return;
74 }
75
76 $field_id = $custom_id ? esc_attr($custom_id) : esc_attr($prefix . $field['id']);
77 $field_name = $custom_name ? esc_attr($custom_name) : esc_attr($prefix . $field['id']);
78 $required = !empty($field['required']);
79 $required_attr = $required ? 'required' : '';
80 $required_star = $required ? '<span class="required">*</span>' : '';
81
82 $prefill_value = '';
83 if ($custom_name === null && $prefix === 'contact_') {
84 global $booking;
85 if (isset($booking) && is_object($booking)) {
86 $prop = 'contact_' . ($field['id'] ?? '');
87 if (!empty($prop) && isset($booking->{$prop}) && $booking->{$prop} !== null && $booking->{$prop} !== '') {
88 $prefill_value = (string) $booking->{$prop};
89 }
90 }
91 }
92
93 $width_class = '';
94 if (!empty($field['width'])) {
95 switch ($field['width']) {
96 case 'half':
97 $width_class = 'yatra-field-half';
98 break;
99 case 'third':
100 $width_class = 'yatra-field-third';
101 break;
102 default:
103 $width_class = 'yatra-field-full';
104 }
105 }
106 $field_type = $field['type'] ?? 'text';
107 ?>
108 <div class="yatra-form-group <?php echo esc_attr($width_class); ?> <?php echo $field_type === 'checkbox' ? 'yatra-form-group--checkbox' : ''; ?>" data-applies-to="<?php echo esc_attr($field['applies_to'] ?? 'all'); ?>">
109 <?php if ($field_type !== 'checkbox') : ?>
110 <label for="<?php echo $field_id; ?>">
111 <?php echo esc_html(yatra_translate_form_string($field['label'])); ?> <?php echo $required_star; ?>
112 </label>
113 <?php endif; ?>
114 <?php
115 switch ($field['type']) {
116 case 'checkbox':
117 $is_checked = ($prefill_value === '1' || $prefill_value === 'on' || $prefill_value === 'true');
118 ?>
119 <label for="<?php echo $field_id; ?>" class="yatra-inline-checkbox-label">
120 <input
121 type="checkbox"
122 id="<?php echo $field_id; ?>"
123 name="<?php echo $field_name; ?>"
124 value="1"
125 <?php checked($is_checked, true, false); ?>
126 <?php echo $required_attr; ?>
127 />
128 <span><?php echo esc_html(yatra_translate_form_string($field['label'])); ?><?php echo $required_star; ?></span>
129 </label>
130 <?php
131 break;
132 case 'select':
133 ?>
134 <select id="<?php echo $field_id; ?>" name="<?php echo $field_name; ?>" <?php echo $required_attr; ?>>
135 <option value=""><?php echo esc_html(yatra_translate_form_string($field['placeholder'] ?? '') ?: __('Select...', 'yatra')); ?></option>
136 <?php if (!empty($field['options'])) : ?>
137 <?php foreach ($field['options'] as $option) : ?>
138 <option value="<?php echo esc_attr($option['value']); ?>" <?php echo selected($prefill_value, (string) ($option['value'] ?? ''), false); ?>>
139 <?php echo esc_html(yatra_translate_form_string($option['label'] ?? '')); ?>
140 </option>
141 <?php endforeach; ?>
142 <?php endif; ?>
143 </select>
144 <?php
145 break;
146
147 case 'country':
148 ?>
149 <select id="<?php echo $field_id; ?>" name="<?php echo $field_name; ?>" <?php echo $required_attr; ?>>
150 <option value=""><?php echo esc_html(yatra_translate_form_string($field['placeholder'] ?? '') ?: __('Select Country', 'yatra')); ?></option>
151 <?php foreach ($countries as $code => $name) : ?>
152 <option value="<?php echo esc_attr($code); ?>" <?php echo selected($prefill_value, (string) $code, false); ?>>
153 <?php echo esc_html($name); ?>
154 </option>
155 <?php endforeach; ?>
156 </select>
157 <?php
158 break;
159
160 case 'textarea':
161 ?>
162 <textarea
163 id="<?php echo $field_id; ?>"
164 name="<?php echo $field_name; ?>"
165 placeholder="<?php echo esc_attr(yatra_translate_form_string($field['placeholder'] ?? '')); ?>"
166 <?php echo $required_attr; ?>
167 rows="3"
168 ><?php echo esc_textarea($prefill_value); ?></textarea>
169 <?php
170 break;
171
172 case 'date':
173 $min_attr = '';
174 // Date-of-birth-style fields are bounded to today and flagged so
175 // booking.js can upgrade them to a picker with fast year jumping
176 // (a native date popup steps one month at a time). The class is
177 // the JS hook; if JS/flatpickr is unavailable the input stays a
178 // working native <input type="date">.
179 $is_dob = (stripos($field_id, 'birth') !== false || stripos($field_id, 'dob') !== false
180 || stripos($field_name, 'birth') !== false || stripos($field_name, 'dob') !== false);
181 $dob_attr = $is_dob ? ' data-yatra-dob="1" max="' . esc_attr(date('Y-m-d')) . '"' : '';
182 // Native date inputs ignore the placeholder, but once booking.js
183 // upgrades the field to a flatpickr text input the placeholder is
184 // what keeps it visually consistent with the other text fields
185 // (otherwise it renders blank). Use the field's own placeholder
186 // when set, else a format hint matching flatpickr's Y-m-d output.
187 $date_placeholder = !empty($field['placeholder'])
188 ? yatra_translate_form_string($field['placeholder'])
189 : 'YYYY-MM-DD';
190 ?>
191 <input
192 type="date"
193 class="yatra-js-date"
194 id="<?php echo $field_id; ?>"
195 name="<?php echo $field_name; ?>"
196 placeholder="<?php echo esc_attr($date_placeholder); ?>"
197 <?php echo $min_attr; ?>
198 <?php echo $dob_attr; ?>
199 <?php echo $required_attr; ?>
200 value="<?php echo esc_attr($prefill_value); ?>"
201 >
202 <?php
203 break;
204
205 default: // text, email, tel, number
206 // Phone (tel) fields get the international country-code widget
207 // UNLESS it has been explicitly disabled in the (Pro) form
208 // builder. A missing key means ON, so every existing form and
209 // customized field keeps the widget without needing a re-save.
210 $is_phone_widget = (($field['type'] ?? '') === 'tel')
211 && (!array_key_exists('show_country_code', $field) || !empty($field['show_country_code']));
212
213 if ($is_phone_widget) {
214 // Split the stored value for display: an international value
215 // ("+9779806015400") is parsed into country + national part;
216 // a legacy bare number is left in the number box under the
217 // default country (never rewritten on display).
218 $pv = (string) $prefill_value;
219 $detected = \Yatra\Helpers\FormatHelper::detectPhoneCountry($pv);
220 if ($detected !== null) {
221 $p_iso = $detected['iso'];
222 $p_dial = $detected['dial'];
223 $p_national = $detected['national'];
224 } else {
225 $p_iso = \Yatra\Helpers\FormatHelper::getDefaultPhoneCountry();
226 $p_dial = \Yatra\Helpers\FormatHelper::getDialingCode($p_iso);
227 $p_national = $pv;
228 }
229
230 // Companion hidden field holding the chosen ISO. Array-style
231 // names (travelers[0][phone]) become travelers[0][phone_country];
232 // the server combines dial code + national number on submit.
233 if (preg_match('/^(.*)\[([^\]]+)\]$/', $field_name, $mm)) {
234 $companion_name = $mm[1] . '[' . $mm[2] . '_country]';
235 } else {
236 $companion_name = $field_name . '_country';
237 }
238
239 $flag_base = \defined('YATRA_PLUGIN_URL') ? YATRA_PLUGIN_URL . 'assets/img/flags/' : '';
240 ?>
241 <div class="yatra-phone-field" data-yatra-phone
242 data-default-iso="<?php echo esc_attr($p_iso); ?>"
243 data-flag-base="<?php echo esc_url($flag_base); ?>">
244 <button type="button" class="yatra-phone-country" aria-haspopup="listbox" aria-expanded="false"
245 aria-label="<?php esc_attr_e('Select country dialing code', 'yatra'); ?>">
246 <img class="yatra-phone-flag" src="<?php echo esc_url($flag_base . strtolower($p_iso) . '.svg'); ?>"
247 alt="" width="22" height="16" loading="lazy">
248 <span class="yatra-phone-dial">+<?php echo esc_html($p_dial); ?></span>
249 <span class="yatra-phone-caret" aria-hidden="true"></span>
250 </button>
251 <input
252 type="tel"
253 class="yatra-phone-number"
254 id="<?php echo $field_id; ?>"
255 name="<?php echo $field_name; ?>"
256 value="<?php echo esc_attr($p_national); ?>"
257 placeholder="<?php echo esc_attr(yatra_translate_form_string($field['placeholder'] ?? '')); ?>"
258 inputmode="tel"
259 autocomplete="tel-national"
260 <?php echo $required_attr; ?>
261 >
262 <input type="hidden" class="yatra-phone-iso"
263 name="<?php echo esc_attr($companion_name); ?>"
264 value="<?php echo esc_attr($p_iso); ?>">
265 </div>
266 <?php
267 } else {
268 ?>
269 <input
270 type="<?php echo esc_attr($field['type']); ?>"
271 id="<?php echo $field_id; ?>"
272 name="<?php echo $field_name; ?>"
273 placeholder="<?php echo esc_attr(yatra_translate_form_string($field['placeholder'] ?? '')); ?>"
274 <?php echo $required_attr; ?>
275 value="<?php echo esc_attr($prefill_value); ?>"
276 >
277 <?php
278 }
279 break;
280 }
281 ?>
282 </div>
283 <?php
284 }
285 } // End function_exists check for yatra_render_form_field
286
287 /**
288 * Render a form section with all its fields
289 */
290 if (!function_exists('yatra_render_form_section')) {
291 function yatra_render_form_section($section_config, $prefix = '', $countries = []) {
292 if (isset($section_config['enabled']) && !$section_config['enabled']) {
293 return;
294 }
295
296 $fields = $section_config['fields'] ?? [];
297
298 // Sort fields by order
299 usort($fields, function($a, $b) {
300 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
301 });
302
303 // Group fields by section (for subsections like dietary)
304 $grouped_fields = [];
305 $current_section = null;
306
307 foreach ($fields as $field) {
308 if (!empty($field['enabled'])) {
309 $section = $field['section'] ?? 'main';
310 if (!isset($grouped_fields[$section])) {
311 $grouped_fields[$section] = [];
312 }
313 $grouped_fields[$section][] = $field;
314 }
315 }
316 ?>
317 <div class="yatra-booking-section">
318 <h2 class="yatra-section-title"><?php echo esc_html(yatra_translate_form_string($section_config['title'] ?? '')); ?></h2>
319 <?php if (!empty($section_config['description'])) : ?>
320 <p class="yatra-section-description"><?php echo esc_html(yatra_translate_form_string($section_config['description'])); ?></p>
321 <?php endif; ?>
322
323 <?php foreach ($grouped_fields as $section_key => $section_fields) : ?>
324 <?php if ($section_key !== 'main') : ?>
325 <div class="yatra-traveler-subsection">
326 <h4 class="yatra-subsection-title">
327 <?php
328 switch ($section_key) {
329 case 'passport':
330 esc_html_e('Additional details', 'yatra');
331 break;
332 case 'dietary_medical':
333 esc_html_e('Dietary & Medical Requirements', 'yatra');
334 break;
335 default:
336 echo esc_html(ucwords(str_replace('_', ' ', $section_key)));
337 }
338 ?>
339 </h4>
340 <?php endif; ?>
341
342 <div class="yatra-form-row">
343 <?php foreach ($section_fields as $field) : ?>
344 <?php yatra_render_form_field($field, $prefix, $countries); ?>
345 <?php endforeach; ?>
346 </div>
347
348 <?php if ($section_key !== 'main') : ?>
349 </div>
350 <?php endif; ?>
351 <?php endforeach; ?>
352 </div>
353 <?php
354 }
355 } // End function_exists check for yatra_render_form_section
356 ?>
357
358 <!-- Hidden Fields -->
359 <input type="hidden" name="trip_id" value="<?php echo esc_attr($trip_id); ?>">
360 <?php if (!empty($trip_slug)) : ?>
361 <input type="hidden" name="trip_slug" value="<?php echo esc_attr($trip_slug); ?>">
362 <?php endif; ?>
363
364 <?php if ($is_remaining_payment) : ?>
365 <input type="hidden" name="is_remaining_payment" value="1">
366 <?php if (!empty($existing_booking_id)) : ?>
367 <input type="hidden" name="existing_booking_id" value="<?php echo esc_attr($existing_booking_id); ?>">
368 <?php endif; ?>
369 <?php if (!empty($booking_reference)) : ?>
370 <input type="hidden" name="booking_reference" value="<?php echo esc_attr($booking_reference); ?>">
371 <?php endif; ?>
372 <?php if ($remaining_amount !== null) : ?>
373 <input type="hidden" name="remaining_amount" value="<?php echo esc_attr($remaining_amount); ?>">
374 <?php endif; ?>
375 <?php if (!empty($booking->amount_paid)) : ?>
376 <input type="hidden" name="amount_paid" value="<?php echo esc_attr($booking->amount_paid); ?>">
377 <?php endif; ?>
378 <?php if (!empty($booking->total_amount)) : ?>
379 <input type="hidden" name="total_amount" value="<?php echo esc_attr($booking->total_amount); ?>">
380 <?php endif; ?>
381 <?php endif; ?>
382
383 <?php if (!$is_remaining_payment) : ?>
384 <!-- Contact Form Section -->
385 <?php
386 $contact_config = $form_config['contact_form'] ?? [];
387 if (!empty($contact_config)) {
388 yatra_render_form_section($contact_config, 'contact_', $countries);
389 }
390 ?>
391
392 <!-- Emergency Contact Section -->
393 <?php
394 $emergency_config = $form_config['emergency_contact_form'] ?? [];
395 if (!empty($emergency_config) && (!isset($emergency_config['enabled']) || $emergency_config['enabled'])) {
396 yatra_render_form_section($emergency_config, 'emergency_', $countries);
397 }
398 ?>
399
400 <!-- Traveler Information Section -->
401 <?php
402 $traveler_config = $form_config['traveler_form'] ?? [];
403 ?>
404 <?php endif; ?>
405
406 <?php
407 $traveler_config = $form_config['traveler_form'] ?? [];
408 $traveler_count = isset($total_travelers) ? max(1, (int)$total_travelers) : 1;
409
410 // Check if we have traveler-based pricing info
411 $pricing_type = isset($pricing_type) ? $pricing_type : 'regular';
412 $price_types = isset($price_types) ? $price_types : [];
413 $traveler_counts = isset($traveler_counts) ? $traveler_counts : [];
414
415 /**
416 * Coerce JSON / serialized / object values to a list for foreach-safe iteration.
417 *
418 * @param mixed $value
419 * @return array<int|string, mixed>
420 */
421 $yatra_normalize_booking_arrayish = static function ($value): array {
422 if ($value === null || $value === false || $value === '') {
423 return [];
424 }
425 if (is_array($value)) {
426 return $value;
427 }
428 if ($value instanceof \stdClass) {
429 return (array) $value;
430 }
431 if (is_string($value)) {
432 $trim = trim($value);
433 if ($trim === '') {
434 return [];
435 }
436 $decoded = json_decode($value, true);
437 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
438 return $decoded;
439 }
440 if (function_exists('maybe_unserialize')) {
441 $un = maybe_unserialize($value);
442 if (is_array($un)) {
443 return $un;
444 }
445 }
446 }
447
448 return [];
449 };
450
451 $price_types = $yatra_normalize_booking_arrayish($price_types);
452 $traveler_counts = $yatra_normalize_booking_arrayish($traveler_counts);
453
454 // Build traveler-to-category mapping for traveler-based pricing
455 $traveler_category_map = [];
456 if ($pricing_type === 'traveler_based' && !empty($price_types) && !empty($traveler_counts)) {
457 $traveler_index = 1;
458 foreach ($price_types as $index => $pt) {
459 $pt = (object) $pt;
460 $category_id = $pt->category_id ?? $index;
461 $category_label = $pt->category_label ?? __('Traveler', 'yatra');
462 $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0;
463
464 for ($c = 1; $c <= $count; $c++) {
465 $traveler_category_map[$traveler_index] = [
466 'category_id' => $category_id,
467 'category_label' => $category_label,
468 'category_index' => $c,
469 ];
470 $traveler_index++;
471 }
472 }
473 }
474
475 $price_candidates = [
476 isset($trip->price) ? (float) $trip->price : 0,
477 isset($booking->session_price) ? (float) $booking->session_price : 0,
478 isset($trip->sale_price) ? (float) $trip->sale_price : 0,
479 isset($trip->original_price) ? (float) $trip->original_price : 0,
480 ];
481
482 $effective_trip_price = 0;
483 foreach ($price_candidates as $candidate) {
484 if ($candidate > 0) {
485 $effective_trip_price = $candidate;
486 break;
487 }
488 }
489
490 $traveler_price_rows = [];
491 $calculated_total = 0;
492
493 if ($pricing_type === 'traveler_based' && !empty($price_types)) {
494 foreach ($price_types as $index => $pt) {
495 $pt = (object) $pt;
496 $category_id = $pt->category_id ?? $index;
497 $category_label = $pt->category_label ?? __('Traveler', 'yatra');
498 $category_price = isset($pt->effective_price) ? (float) $pt->effective_price : ($pt->sale_price ?? $pt->discounted_price ?? $pt->original_price ?? 0);
499 $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : ($index === 0 ? 1 : 0);
500 // Single source of truth for the line amount (per-person × count, flat
501 // per-group, or per-block group pricing). Defaults to the prior flat/
502 // multiply behaviour when group_overflow is absent.
503 $pt_pricing_mode = $pt->pricing_mode ?? 'per_person';
504 $subtotal = \Yatra\Services\TripPricingService::categoryLineSubtotal($pt, $count, $category_price);
505
506 $age_info = '';
507 if (isset($pt->age_min) || isset($pt->age_max)) {
508 if (isset($pt->age_min) && isset($pt->age_max)) {
509 /* translators: 1: minimum age, 2: maximum age. */
510 $age_info = sprintf(__('(Age %1$d-%2$d)', 'yatra'), $pt->age_min, $pt->age_max);
511 } elseif (isset($pt->age_min)) {
512 /* translators: %d: minimum age. */
513 $age_info = sprintf(__('(Age %d+)', 'yatra'), $pt->age_min);
514 } else {
515 /* translators: %d: maximum age. */
516 $age_info = sprintf(__('(Up to age %d)', 'yatra'), $pt->age_max);
517 }
518 }
519
520 $traveler_price_rows[] = [
521 'category_id' => $category_id,
522 'category_label' => $category_label,
523 'category_price' => $category_price,
524 'count' => $count,
525 'subtotal' => $subtotal,
526 'pricing_mode' => $pt_pricing_mode,
527 'age_info' => $age_info,
528 ];
529
530 $calculated_total += $subtotal;
531
532 if ($effective_trip_price <= 0 && $category_price > 0) {
533 $effective_trip_price = $category_price;
534 }
535 }
536 }
537
538 if ($effective_trip_price <= 0) {
539 $effective_trip_price = 0;
540 }
541
542 $initial_total_amount = $calculated_total > 0
543 ? $calculated_total
544 : ($effective_trip_price * max(1, (int) $total_travelers));
545
546 if ($initial_total_amount <= 0 && $effective_trip_price > 0) {
547 $initial_total_amount = $effective_trip_price * max(1, (int) $total_travelers);
548 }
549
550 $initial_due_amount = $initial_total_amount;
551
552 ?>
553
554 <?php
555 // Honour the Traveler section's enable flag (Pro Dynamic Form module), mirroring
556 // the Contact/Emergency sections. The default config has no `enabled` key, so an
557 // un-customised/Free site treats the section as enabled and renders as before.
558 $traveler_section_enabled = !isset($traveler_config['enabled']) || (bool) $traveler_config['enabled'];
559 ?>
560 <?php if (!empty($traveler_config) && $traveler_section_enabled && !$is_remaining_payment) : ?>
561 <div class="yatra-booking-section">
562 <h2 class="yatra-section-title"><?php echo esc_html(yatra_translate_form_string($traveler_config['title'] ?? '') ?: __('Traveler Information', 'yatra')); ?></h2>
563 <?php if (!empty($traveler_config['description'])) : ?>
564 <p class="yatra-section-description"><?php echo esc_html(yatra_translate_form_string($traveler_config['description'])); ?></p>
565 <?php endif; ?>
566
567 <div id="yatra-travelers-container">
568 <?php
569 $traveler_fields = $traveler_config['fields'] ?? [];
570 usort($traveler_fields, function($a, $b) {
571 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
572 });
573
574 // Group fields by section
575 $grouped_traveler_fields = [];
576 foreach ($traveler_fields as $field) {
577 if (!empty($field['enabled'])) {
578 $section = $field['section'] ?? 'main';
579 if (!isset($grouped_traveler_fields[$section])) {
580 $grouped_traveler_fields[$section] = [];
581 }
582 $grouped_traveler_fields[$section][] = $field;
583 }
584 }
585
586 for ($i = 1; $i <= $traveler_count; $i++) :
587 // Determine traveler label based on category if traveler-based pricing
588 if (!empty($traveler_category_map[$i])) {
589 $category_info = $traveler_category_map[$i];
590 $category_label = $category_info['category_label'];
591 $category_index = $category_info['category_index'];
592 /* translators: 1: traveler category label (e.g. Adult, Child), 2: sequential index within that category. */
593 $traveler_label = sprintf(__('%1$s %2$d', 'yatra'), $category_label, $category_index);
594 if ($i === 1) {
595 $traveler_label .= ' (' . __('Lead Traveler', 'yatra') . ')';
596 }
597 } else {
598 /* translators: %d: traveler sequence number. */
599 $traveler_label = ($i === 1) ? __('Traveler 1 (Lead Traveler)', 'yatra') : sprintf(__('Traveler %d', 'yatra'), $i);
600 }
601
602 // Fields targeted at the lead traveler (applies_to === 'lead') render
603 // only for Traveler 1; everything else ('all' / unset) renders for
604 // every traveler. Empty subsections are dropped so additional
605 // travelers don't get an orphan heading.
606 $traveler_grouped_fields = [];
607 foreach ($grouped_traveler_fields as $sk => $sf) {
608 $applicable = array_values(array_filter($sf, function ($f) use ($i) {
609 return ($f['applies_to'] ?? 'all') !== 'lead' || $i === 1;
610 }));
611 if (!empty($applicable)) {
612 $traveler_grouped_fields[$sk] = $applicable;
613 }
614 }
615 ?>
616 <div class="yatra-traveler-form" data-traveler-index="<?php echo esc_attr($i); ?>" <?php if (!empty($traveler_category_map[$i])): ?>data-category-id="<?php echo esc_attr($traveler_category_map[$i]['category_id']); ?>" data-category-label="<?php echo esc_attr($traveler_category_map[$i]['category_label']); ?>"<?php endif; ?>>
617 <div class="yatra-traveler-header">
618 <h3 class="yatra-traveler-title"><?php echo esc_html($traveler_label); ?></h3>
619 <?php if ($i > 1) : ?>
620 <span class="yatra-traveler-note"><?php esc_html_e('Additional traveler', 'yatra'); ?></span>
621 <?php endif; ?>
622 </div>
623
624 <?php foreach ($traveler_grouped_fields as $section_key => $section_fields) : ?>
625 <?php if ($section_key !== 'main') : ?>
626 <div class="yatra-traveler-subsection">
627 <h4 class="yatra-subsection-title">
628 <?php
629 switch ($section_key) {
630 case 'passport':
631 esc_html_e('Additional details', 'yatra');
632 break;
633 case 'dietary_medical':
634 esc_html_e('Dietary & Medical Requirements', 'yatra');
635 break;
636 default:
637 echo esc_html(ucwords(str_replace('_', ' ', $section_key)));
638 }
639 ?>
640 </h4>
641 <?php endif; ?>
642
643 <div class="yatra-form-row">
644 <?php foreach ($section_fields as $field) :
645 $field_id = "traveler-{$i}-" . esc_attr($field['id']);
646 $field_name = "travelers[{$i}][" . esc_attr($field['id']) . "]";
647 yatra_render_form_field($field, '', $countries, $field_name, $field_id);
648 endforeach; ?>
649 </div>
650
651 <?php if ($section_key !== 'main') : ?>
652 </div>
653 <?php endif; ?>
654 <?php endforeach; ?>
655 </div>
656 <?php endfor; ?>
657 </div>
658 </div>
659 <?php endif; ?>
660
661 <!-- Payment Method Section -->
662 <?php
663 /**
664 * Filter to check if flexible payments module is enabled (Pro feature)
665 * When enabled, deposit and partial payment options will be available
666 */
667 $flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
668
669 /**
670 * Filter to get payment method options.
671 *
672 * Pro module (FlexiblePayments) can add deposit/partial payment options here.
673 * We pass `trip_id` so the Pro module can look up per-trip overrides
674 * (trip.deposit_amount / trip.deposit_percentage) and expose the deposit
675 * option even when the site-wide "deposit_required" flag is off — i.e. a
676 * single trip can opt in to a custom deposit without enabling deposits for
677 * the whole catalogue.
678 */
679 $payment_method_options = apply_filters('yatra_payment_method_options', [], [
680 'trip_id' => isset($trip_id) ? (int) $trip_id : 0,
681 'deposit_required' => $deposit_required,
682 'deposit_percentage' => $deposit_percentage,
683 'partial_payment' => $partial_payment,
684 'partial_payment_percentage' => $partial_payment_percentage,
685 ]);
686
687 $has_flexible_options = $flexible_payments_enabled && !empty($payment_method_options);
688 ?>
689 <?php if (!$is_remaining_payment && $has_flexible_options) : ?>
690 <div class="yatra-booking-section">
691 <h2 class="yatra-section-title"><?php esc_html_e('Payment Method', 'yatra'); ?></h2>
692
693 <div class="yatra-payment-methods">
694 <label class="yatra-payment-option">
695 <input type="radio" name="payment_method" value="full" checked>
696 <span class="yatra-payment-label">
697 <strong><?php esc_html_e('Pay in Full', 'yatra'); ?></strong>
698 <span><?php esc_html_e('Pay the total amount now', 'yatra'); ?></span>
699 </span>
700 </label>
701
702 <?php foreach ($payment_method_options as $option_key => $option) : ?>
703 <label class="yatra-payment-option">
704 <input type="radio" name="payment_method" value="<?php echo esc_attr($option['value']); ?>">
705 <span class="yatra-payment-label">
706 <strong><?php echo esc_html($option['label']); ?></strong>
707 <span><?php echo esc_html($option['description']); ?></span>
708 </span>
709 </label>
710 <?php endforeach; ?>
711 </div>
712 </div>
713 <?php endif; ?>
714
715 <!-- Payment Gateway Section -->
716 <?php
717 // Always use the live registry list (same as BookingPageHandler). Do not intersect with $enabled_gateways
718 // from older templates/sessions — that caused only stale gateways (e.g. Pay Later) to show.
719 $display_gateways = [];
720 if (class_exists('Yatra\PaymentGateways\PaymentGatewayRegistry')) {
721 $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance();
722 foreach ($registry->getForCheckout() as $gateway) {
723 if (!empty($gateway['id'])) {
724 $display_gateways[$gateway['id']] = $gateway;
725 }
726 }
727 }
728 ?>
729 <?php if (!empty($display_gateways)) : ?>
730 <div class="yatra-booking-section">
731 <h2 class="yatra-section-title"><?php esc_html_e('Select Payment Gateway', 'yatra'); ?></h2>
732
733 <div class="yatra-gateway-options">
734 <?php
735 $first = true;
736 foreach ($display_gateways as $gateway_id => $gateway) :
737 $icon = !empty($gateway['icon']) ? $gateway['icon'] : plugins_url('assets/images/payment-placeholder.png', dirname(__DIR__));
738 ?>
739 <div class="yatra-gateway-option-wrapper">
740 <label class="yatra-gateway-option">
741 <input type="radio" name="payment_gateway" value="<?php echo esc_attr($gateway_id); ?>" <?php checked($first); ?>>
742 <span class="yatra-gateway-icon-wrap">
743 <img src="<?php echo esc_url($icon ?? ''); ?>" alt="<?php echo esc_attr($gateway['title'] ?? ''); ?>" class="yatra-gateway-icon">
744 </span>
745 <span class="yatra-gateway-content">
746 <strong class="yatra-gateway-title"><?php echo esc_html($gateway['title']); ?></strong>
747 <span class="yatra-gateway-desc"><?php echo esc_html($gateway['description']); ?></span>
748 <?php if (empty($gateway['is_configured'])) : ?>
749 <span class="yatra-gateway-desc yatra-gateway-setup-required"><?php esc_html_e('Setup incomplete — finish configuration under Yatra → Settings → Payment, or choose another method.', 'yatra'); ?></span>
750 <?php endif; ?>
751 </span>
752 </label>
753 <div class="yatra-gateway-extra" id="yatra-gateway-extra-<?php echo esc_attr($gateway_id); ?>" data-gateway="<?php echo esc_attr($gateway_id); ?>"></div>
754 </div>
755 <?php
756 $first = false;
757 endforeach;
758 ?>
759 </div>
760 </div>
761 <?php endif; ?>
762
763 <?php if (!$is_remaining_payment) : ?>
764 <!-- Special Requests -->
765 <div class="yatra-booking-section">
766 <h2 class="yatra-section-title"><?php esc_html_e('Special Requests', 'yatra'); ?></h2>
767 <p class="yatra-section-description"><?php esc_html_e('Any special requests or notes for your trip (optional)', 'yatra'); ?></p>
768
769 <div class="yatra-form-group yatra-field-full">
770 <textarea
771 id="special-requests"
772 name="special_requests"
773 rows="4"
774 placeholder="<?php esc_attr_e('E.g., dietary requirements, accessibility needs, celebration requests...', 'yatra'); ?>"
775 ></textarea>
776 </div>
777 </div>
778 <?php endif; ?>
779
780 <!-- Account Creation Section (only for guests) -->
781 <?php
782 $allow_guest_checkout = \Yatra\Services\SettingsService::isEnabled('allow_guest_checkout');
783 $require_login = \Yatra\Services\SettingsService::isEnabled('require_login');
784 // Account CREATION is gated by the same setting the backend enforces
785 // (AuthController rejects registration when this is off). When registration is
786 // disabled we never offer account-creation UI.
787 $registration_enabled = \Yatra\Services\SettingsService::isEnabled('customer_registration');
788
789 // Show the account section only when it has something to render: login is
790 // required, guest checkout is off (must authenticate), or registration is on
791 // (optional/required account creation). When registration is OFF and guests are
792 // allowed, there is nothing to show — proceed as a pure guest.
793 if (!is_user_logged_in() && !$is_remaining_payment
794 && ($require_login || !$allow_guest_checkout || $registration_enabled)) :
795 ?>
796 <div class="yatra-booking-section yatra-account-section">
797 <h2 class="yatra-section-title"><?php esc_html_e('Account', 'yatra'); ?></h2>
798
799 <?php if ($require_login || (!$allow_guest_checkout && !$registration_enabled)) : ?>
800 <!-- Login Required Message (login required, OR guests off with registration disabled) -->
801 <div class="yatra-login-required-notice" style="background: #fef3c7; border: 1px solid #f59e0b; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
802 <div style="display: flex; align-items: flex-start; gap: 12px;">
803 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2" style="flex-shrink: 0;">
804 <circle cx="12" cy="12" r="10"></circle>
805 <line x1="12" y1="8" x2="12" y2="12"></line>
806 <line x1="12" y1="16" x2="12.01" y2="16"></line>
807 </svg>
808 <div>
809 <p style="font-weight: 600; color: #92400e; margin: 0 0 8px 0;"><?php esc_html_e('Login Required', 'yatra'); ?></p>
810 <p style="color: #a16207; margin: 0 0 12px 0; font-size: 14px;">
811 <?php echo $registration_enabled
812 ? esc_html__('You must be logged in to complete this booking. Please log in or create an account.', 'yatra')
813 : esc_html__('You must be logged in to complete this booking. Please log in to continue.', 'yatra'); ?>
814 </p>
815 <a href="<?php echo esc_url(wp_login_url(get_permalink())); ?>" class="yatra-login-btn" style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 14px;">
816 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
817 <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
818 <polyline points="10 17 15 12 10 7"></polyline>
819 <line x1="15" y1="12" x2="3" y2="12"></line>
820 </svg>
821 <?php esc_html_e('Log In', 'yatra'); ?>
822 </a>
823 <?php if ($registration_enabled) : ?>
824 <span style="margin: 0 12px; color: #a16207;"><?php esc_html_e('or', 'yatra'); ?></span>
825 <a href="<?php echo esc_url(wp_registration_url()); ?>" style="color: #3b82f6; text-decoration: none; font-weight: 500;">
826 <?php esc_html_e('Create an Account', 'yatra'); ?>
827 </a>
828 <?php endif; ?>
829 </div>
830 </div>
831 </div>
832 <input type="hidden" name="login_required" value="1">
833
834 <?php elseif (!$allow_guest_checkout && $registration_enabled) : ?>
835 <!-- Account Required - Must Create Account (registration enabled) -->
836 <p class="yatra-section-description" style="margin-bottom: 16px;">
837 <?php esc_html_e('Please create an account to complete your booking. This allows you to manage your bookings and receive important updates.', 'yatra'); ?>
838 </p>
839
840 <div class="yatra-form-row">
841 <div class="yatra-form-group yatra-field-half">
842 <label for="account_password">
843 <?php esc_html_e('Password', 'yatra'); ?> <span class="required">*</span>
844 </label>
845 <input type="password" id="account_password" name="account_password" required minlength="8"
846 placeholder="<?php esc_attr_e('Minimum 8 characters', 'yatra'); ?>">
847 </div>
848 <div class="yatra-form-group yatra-field-half">
849 <label for="account_password_confirm">
850 <?php esc_html_e('Confirm Password', 'yatra'); ?> <span class="required">*</span>
851 </label>
852 <input type="password" id="account_password_confirm" name="account_password_confirm" required minlength="8"
853 placeholder="<?php esc_attr_e('Re-enter password', 'yatra'); ?>">
854 </div>
855 </div>
856 <p class="yatra-field-hint" style="font-size: 13px; color: #6b7280; margin-top: 8px;">
857 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle; margin-right: 4px;">
858 <circle cx="12" cy="12" r="10"></circle>
859 <path d="M12 16v-4"></path>
860 <path d="M12 8h.01"></path>
861 </svg>
862 <?php esc_html_e('Your email address will be used as your username.', 'yatra'); ?>
863 </p>
864 <input type="hidden" name="create_account" value="1">
865
866 <?php elseif ($registration_enabled) : ?>
867 <!-- Guest Checkout Allowed - Optional Account Creation (registration enabled) -->
868 <p class="yatra-section-description" style="margin-bottom: 16px;">
869 <?php esc_html_e('Create an account to easily manage your bookings and receive travel updates.', 'yatra'); ?>
870 </p>
871
872 <label class="yatra-checkbox-label" style="margin-bottom: 16px;">
873 <input type="checkbox" name="create_account" id="create-account" value="1">
874 <span><?php esc_html_e('Create an account for easier booking management', 'yatra'); ?></span>
875 </label>
876
877 <div class="yatra-account-fields" id="yatra-account-fields" style="display: none;">
878 <div class="yatra-form-row">
879 <div class="yatra-form-group yatra-field-half">
880 <label for="account_password">
881 <?php esc_html_e('Password', 'yatra'); ?> <span class="required">*</span>
882 </label>
883 <input type="password" id="account_password" name="account_password" minlength="8"
884 placeholder="<?php esc_attr_e('Minimum 8 characters', 'yatra'); ?>">
885 </div>
886 <div class="yatra-form-group yatra-field-half">
887 <label for="account_password_confirm">
888 <?php esc_html_e('Confirm Password', 'yatra'); ?> <span class="required">*</span>
889 </label>
890 <input type="password" id="account_password_confirm" name="account_password_confirm" minlength="8"
891 placeholder="<?php esc_attr_e('Re-enter password', 'yatra'); ?>">
892 </div>
893 </div>
894 <p class="yatra-field-hint" style="font-size: 13px; color: #6b7280; margin-top: 8px;">
895 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle; margin-right: 4px;">
896 <circle cx="12" cy="12" r="10"></circle>
897 <path d="M12 16v-4"></path>
898 <path d="M12 8h.01"></path>
899 </svg>
900 <?php esc_html_e('Your email address will be used as your username.', 'yatra'); ?>
901 </p>
902 </div>
903 <?php endif; ?>
904 </div>
905 <?php elseif (is_user_logged_in()) : ?>
906 <!-- User is logged in -->
907 <div class="yatra-booking-section yatra-logged-in-notice">
908 <div style="display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #ecfdf5; border: 1px solid #10b981; border-radius: 8px;">
909 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2">
910 <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
911 <polyline points="22 4 12 14.01 9 11.01"></polyline>
912 </svg>
913 <div>
914 <span style="font-weight: 500; color: #065f46;">
915 <?php
916 $current_user = wp_get_current_user();
917 printf(
918 /* translators: %s: User display name */
919 esc_html__('Logged in as %s', 'yatra'),
920 '<strong>' . esc_html($current_user->display_name) . '</strong>'
921 );
922 ?>
923 </span>
924 <a href="<?php echo esc_url(wp_logout_url(get_permalink())); ?>" style="margin-left: 12px; color: #059669; font-size: 13px;">
925 <?php esc_html_e('Log out', 'yatra'); ?>
926 </a>
927 </div>
928 </div>
929 </div>
930 <?php endif; ?>
931
932 <!-- Terms & Conditions -->
933 <div class="yatra-booking-section">
934 <div class="yatra-terms-container">
935 <?php
936 $termsPageId = 0;
937 $privacyPageId = 0;
938 if (class_exists('\\Yatra\\Services\\SettingsService')) {
939 $termsPageId = \Yatra\Services\SettingsService::getInt('terms_page_id', 0);
940 $privacyPageId = \Yatra\Services\SettingsService::getInt('privacy_policy_page_id', 0);
941 } else {
942 $termsPageId = (int) get_option('yatra_terms_page_id', 0);
943 $privacyPageId = (int) get_option('yatra_privacy_policy_page_id', 0);
944 }
945
946 $termsUrl = $termsPageId > 0 ? get_permalink($termsPageId) : '';
947 $privacyUrl = $privacyPageId > 0 ? get_permalink($privacyPageId) : '';
948 if ($privacyUrl === '' && function_exists('get_privacy_policy_url')) {
949 $privacyUrl = (string) get_privacy_policy_url();
950 }
951 ?>
952
953 <label class="yatra-checkbox-label">
954 <input type="checkbox" name="accept_terms" id="accept-terms" required>
955 <span>
956 <?php
957 printf(
958 /* translators: %s: Terms and Conditions link */
959 esc_html__('I have read and agree to the %s', 'yatra'),
960 $termsUrl
961 ? '<a href="' . esc_url($termsUrl) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Terms and Conditions', 'yatra') . '</a>'
962 : '<span>' . esc_html__('Terms and Conditions', 'yatra') . '</span>'
963 );
964 ?>
965 <span class="required">*</span>
966 </span>
967 </label>
968
969 <label class="yatra-checkbox-label">
970 <input type="checkbox" name="accept_privacy" id="accept-privacy" required>
971 <span>
972 <?php
973 printf(
974 /* translators: %s: Privacy Policy link */
975 esc_html__('I agree to the %s and consent to my data being processed', 'yatra'),
976 $privacyUrl
977 ? '<a href="' . esc_url($privacyUrl) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Privacy Policy', 'yatra') . '</a>'
978 : '<span>' . esc_html__('Privacy Policy', 'yatra') . '</span>'
979 );
980 ?>
981 <span class="required">*</span>
982 </span>
983 </label>
984
985 <label class="yatra-checkbox-label">
986 <input type="checkbox" name="subscribe_newsletter" id="subscribe-newsletter">
987 <span><?php esc_html_e('Subscribe to our newsletter for travel tips and exclusive offers', 'yatra'); ?></span>
988 </label>
989 </div>
990 </div>
991