PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.1
Yatra – Travel Booking & Tour Operator Software v3.0.14.1
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.14.1, at templates/partials/booking-form-fields.php

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