PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 All 35 releases
vikbooking / admin / controllers / bookings.php

bookings.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/controllers/bookings.php

1,293 lines 47.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * VikBooking bookings controller.
16 *
17 * @since 1.16.0 (J) - 1.6.0 (WP)
18 */
19 class VikBookingControllerBookings extends JControllerAdmin
20 {
21 /**
22 * AJAX endpoint to search for an extra service name.
23 *
24 * @return void
25 */
26 public function search_service()
27 {
28 if (!JSession::checkToken()) {
29 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
30 }
31
32 $dbo = JFactory::getDbo();
33
34 $service_name = VikRequest::getString('service_name', '', 'request');
35 $max_results = VikRequest::getInt('max_results', 10, 'request');
36
37 $sql_term = $dbo->quote("%{$service_name}%");
38 $sql_clause = !empty($service_name) ? 'LIKE ' . $sql_term : 'IS NOT NULL';
39
40 $q = "SELECT `or`.`idorder`, `or`.`idroom`, `or`.`adults`, `or`.`children`, `or`.`extracosts`, `o`.`days` AS `nights`, `o`.`ts`, `r`.`name` AS `room_name`
41 FROM `#__vikbooking_ordersrooms` AS `or`
42 LEFT JOIN `#__vikbooking_orders` AS `o` ON `or`.`idorder`=`o`.`id`
43 LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id`
44 WHERE `or`.`extracosts` {$sql_clause}
45 ORDER BY `or`.`idorder` DESC";
46 $dbo->setQuery($q, 0, $max_results);
47 $dbo->execute();
48 if (!$dbo->getNumRows()) {
49 // no results
50 VBOHttpDocument::getInstance()->json([]);
51 }
52
53 $results = $dbo->loadAssocList();
54
55 $matching_services = [];
56
57 foreach ($results as $k => $result) {
58 $extra_services = json_decode($result['extracosts'], true);
59 if (empty($extra_services)) {
60 continue;
61 }
62 foreach ($extra_services as $extra_service) {
63 if (empty($service_name) || stristr($extra_service['name'], $service_name) !== false || stristr($service_name, $extra_service['name']) !== false) {
64 // matching service found
65 $matching_service = $result;
66 unset($matching_service['extracosts']);
67 $matching_service['service'] = $extra_service;
68 $matching_service['service']['format_cost'] = VikBooking::getCurrencySymb() . ' ' . VikBooking::numberFormat($extra_service['cost']);
69 $matching_service['format_dt'] = VikBooking::formatDateTs($result['ts']);
70 // push result
71 $matching_services[] = $matching_service;
72 if (count($matching_services) >= $max_results) {
73 break 2;
74 }
75 }
76 }
77 }
78
79 // output the JSON encoded list of matching results found
80 VBOHttpDocument::getInstance()->json($matching_services);
81 }
82
83 /**
84 * AJAX endpoint to count the number of uses for various coupon codes.
85 *
86 * @return void
87 */
88 public function coupons_use_count()
89 {
90 if (!JSession::checkToken()) {
91 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
92 }
93
94 $dbo = JFactory::getDbo();
95
96 $coupon_codes = VikRequest::getVar('coupon_codes', array());
97
98 $use_counts = [];
99
100 foreach ($coupon_codes as $coupon_code) {
101 $q = "SELECT COUNT(*) FROM `#__vikbooking_orders` WHERE `coupon` LIKE " . $dbo->quote("%;{$coupon_code}");
102 $dbo->setQuery($q);
103 $dbo->execute();
104 $use_counts[] = [
105 'code' => $coupon_code,
106 'count' => (int)$dbo->loadResult(),
107 ];
108 }
109
110 // output the JSON encoded list of coupon use counts
111 VBOHttpDocument::getInstance()->json($use_counts);
112 }
113
114 /**
115 * AJAX endpoint to dynamically search for customers. Compatible with select2.
116 *
117 * @return void
118 */
119 public function customers_search()
120 {
121 if (!JSession::checkToken()) {
122 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
123 }
124
125 $dbo = JFactory::getDbo();
126
127 $term = VikRequest::getString('term', '', 'request');
128
129 $response = [
130 'results' => [],
131 'pagination' => [
132 'more' => false,
133 ],
134 ];
135
136 if (empty($term)) {
137 // output the JSON object with no results
138 VBOHttpDocument::getInstance()->json($response);
139 }
140
141 $sql_term = $dbo->quote("%{$term}%");
142
143 $q = "SELECT `c`.`id`, `c`.`first_name`, `c`.`last_name`, `c`.`country`,
144 (SELECT COUNT(*) FROM `#__vikbooking_customers_orders` AS `co` WHERE `co`.`idcustomer`=`c`.`id`) AS `tot_bookings`
145 FROM `#__vikbooking_customers` AS `c`
146 WHERE CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) LIKE {$sql_term}
147 OR `email` LIKE {$sql_term}
148 ORDER BY `c`.`first_name` ASC, `c`.`last_name` ASC;";
149 $dbo->setQuery($q);
150 $customers = $dbo->loadAssocList();
151
152 if ($customers) {
153 foreach ($customers as $k => $customer) {
154 $customers[$k]['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']) . ' (' . $customer['tot_bookings'] . ')';
155 }
156 // push results found
157 $response['results'] = $customers;
158 }
159
160 // output the JSON encoded object with results found
161 VBOHttpDocument::getInstance()->json($response);
162 }
163
164 /**
165 * AJAX endpoint to dynamically search for rooms. Compatible with select2.
166 *
167 * @return void
168 *
169 * @since 1.16.10 (J) - 1.6.10 (WP)
170 */
171 public function rooms_search()
172 {
173 if (!JSession::checkToken()) {
174 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
175 }
176
177 $dbo = JFactory::getDbo();
178 $app = JFactory::getApplication();
179
180 $term = $app->input->getString('term', '');
181
182 $response = [
183 'results' => [],
184 'pagination' => [
185 'more' => false,
186 ],
187 ];
188
189 if (empty($term)) {
190 // output the JSON object with no results
191 VBOHttpDocument::getInstance($app)->json($response);
192 }
193
194 $dbo->setQuery(
195 $dbo->getQuery(true)
196 ->select([
197 $dbo->qn('id'),
198 $dbo->qn('name', 'text'),
199 $dbo->qn('img'),
200 ])
201 ->from($dbo->qn('#__vikbooking_rooms'))
202 ->where($dbo->qn('name') . ' LIKE ' . $dbo->q("%{$term}%"))
203 ->order($dbo->qn('avail') . ' DESC')
204 ->order($dbo->qn('name') . ' ASC')
205 );
206
207 // set results found
208 $response['results'] = $dbo->loadAssocList();
209
210 // load and map mini thumbnails
211 $mini_thumbnails = VBORoomHelper::getInstance()->loadMiniThumbnails($response['results']);
212 $response['results'] = array_map(function($room) use ($mini_thumbnails) {
213 if ($mini_thumbnails[$room['id']] ?? '') {
214 // set mini thumbnail URL
215 $room['img'] = $mini_thumbnails[$room['id']];
216 } else {
217 unset($room['img']);
218 }
219 return $room;
220 }, $response['results']);
221
222 // output the JSON encoded object with results found
223 VBOHttpDocument::getInstance()->json($response);
224 }
225
226 /**
227 * AJAX endpoint to dynamically search for bookings. Compatible with select2.
228 *
229 * @return void
230 *
231 * @since 1.18.0 (J) - 1.8.0 (WP)
232 */
233 public function bookings_search()
234 {
235 if (!JSession::checkToken()) {
236 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
237 }
238
239 $dbo = JFactory::getDbo();
240 $app = JFactory::getApplication();
241
242 $booking_key = trim($app->input->getString('term', ''));
243 $booking_status = array_values(array_filter((array) $app->input->get('status', [], 'array')));
244
245 $response = [
246 'results' => [],
247 'pagination' => [
248 'more' => false,
249 ],
250 ];
251
252 if (empty($booking_key)) {
253 // output the JSON object with no results
254 VBOHttpDocument::getInstance($app)->json($response);
255 }
256
257 // attempt to detect a booking ID
258 $booking_id = 0;
259 if (preg_match("/^[0-9]+$/", $booking_key)) {
260 // only numbers should be a booking ID
261 $booking_id = $booking_key;
262 } elseif (preg_match('/^(?=.*?\d)(?=.*?[A-Z])[A-Z\d]+$/', $booking_key)) {
263 /**
264 * Matched both numbers and upper-case letters, so it has to be an OTA booking ID, not a customer name.
265 * Regex breakdown:
266 * beginning of string
267 * lookahead for at least one digit
268 * lookahead for at least one upper-case letter
269 * match one or more upper-case letters or digits
270 * end of string
271 */
272 $booking_id = $booking_key;
273 }
274
275 // start the query
276 $q = $dbo->getQuery(true)
277 ->select([
278 $dbo->qn('o.id'),
279 $dbo->qn('o.custdata'),
280 $dbo->qn('o.days'),
281 $dbo->qn('o.status'),
282 $dbo->qn('o.checkin'),
283 $dbo->qn('o.checkout'),
284 $dbo->qn('o.idorderota'),
285 $dbo->qn('o.channel'),
286 $dbo->qn('c.first_name'),
287 $dbo->qn('c.last_name'),
288 $dbo->qn('c.pic'),
289 ])
290 ->from($dbo->qn('#__vikbooking_orders', 'o'))
291 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
292 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
293 ->where($dbo->qn('o.closure') . ' = 0');
294
295 if ($booking_status) {
296 // filter by booking status
297 if (count($booking_status) === 1) {
298 // single booking status
299 $q->where($dbo->qn('o.status') . ' = ' . $dbo->q($booking_status[0]));
300 } else {
301 // multiple booking statuses
302 $q->where($dbo->qn('o.status') . ' IN (' . implode(', ', array_map([$dbo, 'q'], $booking_status)) . ')');
303 }
304 }
305
306 if (!empty($booking_id)) {
307 // search by booking ID or OTA booking ID only
308 if (preg_match("/^[0-9]+$/", (string) $booking_id)) {
309 // only numbers could be both website and OTA
310 $q->andWhere([
311 $dbo->qn('o.id') . ' = ' . (int) $booking_id,
312 $dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_id),
313 ], $glue = 'OR');
314 } else {
315 // alphanumeric IDs can only belong to an OTA reservation
316 $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_id));
317 }
318 } else {
319 // search by different values
320 if (stripos($booking_key, 'id:') === 0) {
321 // search by ID or OTA ID
322 $seek_parts = explode('id:', $booking_key);
323 $seek_value = trim($seek_parts[1]);
324 $q->andWhere([
325 $dbo->qn('o.id') . ' = ' . $dbo->q($seek_value),
326 $dbo->qn('o.idorderota') . ' = ' . $dbo->q($seek_value),
327 ], $glue = 'OR');
328 } elseif (stripos($booking_key, 'otaid:') === 0) {
329 // search by OTA Booking ID
330 $seek_parts = explode('otaid:', $booking_key);
331 $seek_value = trim($seek_parts[1]);
332 $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($seek_value));
333 } elseif (stripos($booking_key, 'coupon:') === 0) {
334 // search by coupon code
335 $seek_parts = explode('coupon:', $booking_key);
336 $seek_value = trim($seek_parts[1]);
337 $q->where($dbo->qn('o.coupon') . ' LIKE ' . $dbo->q("%{$seek_value}%"));
338 } elseif (stripos($booking_key, 'name:') === 0) {
339 // search by customer nominative
340 $seek_parts = explode('name:', $booking_key);
341 $seek_value = trim($seek_parts[1]);
342 $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$seek_value}%"));
343 } elseif (strpos($booking_key, '@') !== false) {
344 // search by customer email
345 $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($booking_key));
346 } elseif (strpos($booking_key, '+') === 0) {
347 // search by customer phone
348 $q->where($dbo->qn('o.phone') . ' = ' . $dbo->q($booking_key));
349 } else {
350 // seek for various values
351 if (preg_match("/^[a-z\s]+$/i", (string) $booking_key)) {
352 // when only letters (or spaces) look only for the customer name
353 $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$booking_key}%"));
354 } else {
355 // look for both customer name and booking ID
356 $q->andWhere([
357 'CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$booking_key}%"),
358 $dbo->qn('o.id') . ' = ' . $dbo->q($booking_key),
359 $dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_key),
360 ], $glue = 'OR');
361 }
362 }
363 }
364
365 // order by most recent bookings
366 $q->order($dbo->qn('id') . ' DESC');
367
368 $dbo->setQuery($q);
369
370 // set results found
371 $response['results'] = $dbo->loadAssocList();
372
373 // default icon for website reservations
374 $source_def_icon_cls = VikBookingIcons::i('hotel');
375
376 // map the results with the required properties
377 $response['results'] = array_map(function($booking) use ($source_def_icon_cls) {
378 // build "text" property
379 $text = $booking['id'];
380 if (!empty($booking['first_name'])) {
381 // use customer nominative when available
382 $text = trim($booking['first_name'] . ' ' . $booking['last_name']);
383 } elseif (!empty($booking['custdata'])) {
384 $text = VikBooking::getFirstCustDataField($booking['custdata']);
385 }
386 $booking['text'] = $text;
387
388 // build "img" property
389 if (!empty($booking['pic'])) {
390 // use guest profile picture
391 $booking['img'] = strpos($booking['pic'], 'http') === 0 ? $booking['pic'] : VBO_SITE_URI . 'resources/uploads/' . $booking['pic'];
392 } elseif (!empty($booking['channel'])) {
393 // use channel logo
394 $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true);
395 $booking['img'] = is_object($ch_logo_obj) ? $ch_logo_obj->getTinyLogoURL() : '';
396 }
397
398 if (empty($booking['img'])) {
399 // always set an empty string
400 $booking['img'] = '';
401 // set the default icon class
402 $booking['icon_class'] = $source_def_icon_cls;
403 }
404
405 // return the mapped booking element
406 return $booking;
407 }, $response['results']);
408
409 // output the JSON encoded object with results found
410 VBOHttpDocument::getInstance()->json($response);
411 }
412
413 /**
414 * AJAX endpoint to dynamically search for customers and build elements. Compatible with select2.
415 *
416 * @return void
417 *
418 * @since 1.18.0 (J) - 1.8.0 (WP)
419 */
420 public function customer_elements_search()
421 {
422 if (!JSession::checkToken()) {
423 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
424 }
425
426 $dbo = JFactory::getDbo();
427 $app = JFactory::getApplication();
428
429 $search_key = trim($app->input->getString('term', ''));
430
431 $response = [
432 'results' => [],
433 'pagination' => [
434 'more' => false,
435 ],
436 ];
437
438 if (empty($search_key)) {
439 // output the JSON object with no results
440 VBOHttpDocument::getInstance($app)->json($response);
441 }
442
443 // start the query
444 $q = $dbo->getQuery(true)
445 ->select('*')
446 ->from($dbo->qn('#__vikbooking_customers'))
447 ->where(1);
448
449 if (preg_match('/^[a-z0-9\.\-\_]+\@[a-z0-9\.\-\_]+\.[a-z0-9\.\-\_]+$/i', $search_key)) {
450 // full email address detected
451 $q->where($dbo->qn('email') . ' = ' . $dbo->q($search_key));
452 } else {
453 // search by different values
454 $seek_clauses = [];
455
456 // search by nominative
457 $seek_clauses[] = 'CONCAT_WS(" ", ' . $dbo->qn('first_name') . ', ' . $dbo->qn('last_name') . ') LIKE ' . $dbo->q('%' . $search_key . '%');
458
459 // search by company name
460 $seek_clauses[] = $dbo->qn('company') . ' LIKE ' . $dbo->q('%' . $search_key . '%');
461
462 if (preg_match('/^\+?[0-9\s]+$/i', $search_key)) {
463 // search by phone number
464 $seek_clauses[] = $dbo->qn('phone') . ' = ' . $dbo->q($search_key);
465 }
466
467 if (strpos($search_key, '@') !== false) {
468 // search by email address
469 $seek_clauses[] = $dbo->qn('email') . ' LIKE ' . $dbo->q('%' . $search_key . '%');
470 }
471
472 if (preg_match('/[0-9]+/', $search_key)) {
473 // search by company VAT number
474 $seek_clauses[] = $dbo->qn('vat') . ' = ' . $dbo->q($search_key);
475
476 // search by PIN code
477 $seek_clauses[] = $dbo->qn('pin') . ' = ' . $dbo->q($search_key);
478 }
479
480 // set multiple search clauses
481 $q->andWhere($seek_clauses, 'OR');
482 }
483
484 // order by customer nominative
485 $q->order($dbo->qn('first_name') . ' ASC');
486 $q->order($dbo->qn('last_name') . ' ASC');
487
488 $dbo->setQuery($q);
489
490 // set results found
491 $response['results'] = $dbo->loadAssocList();
492
493 // default icon for customers
494 $source_def_icon_cls = VikBookingIcons::i('user');
495
496 // map the results with the required properties
497 $response['results'] = array_map(function($customer) use ($source_def_icon_cls) {
498 // build "text" property
499 $customer['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']);
500
501 // build "img" property
502 if (!empty($customer['pic'])) {
503 // use customer profile picture
504 $customer['img'] = strpos($customer['pic'], 'http') === 0 ? $customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $customer['pic'];
505 } elseif (!empty($customer['country']) && is_file(implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'countries', $customer['country'] . '.png']))) {
506 // use customer country flag
507 $customer['img'] = VBO_ADMIN_URI . 'resources/countries/' . $customer['country'] . '.png';
508 $customer['img_title'] = $customer['country'];
509 }
510
511 if (empty($customer['img'])) {
512 // always set an empty string
513 $customer['img'] = '';
514 // set the default icon class
515 $customer['icon_class'] = $source_def_icon_cls;
516 }
517
518 // handle custom fields
519 if (!empty($customer['cfields'])) {
520 $custom_fields = (array) json_decode($customer['cfields'], true);
521 if ($custom_fields) {
522 $customer['cfields'] = $custom_fields;
523 }
524 }
525 if (!is_array($customer['cfields']) || !$customer['cfields']) {
526 // ensure this is a null value
527 $customer['cfields'] = null;
528 }
529
530 // return the mapped customer element
531 return $customer;
532 }, $response['results']);
533
534 // output the JSON encoded object with results found
535 VBOHttpDocument::getInstance()->json($response);
536 }
537
538 /**
539 * Regular task to update the status of a cancelled booking to pending (stand-by).
540 *
541 * @return void
542 */
543 public function set_to_pending()
544 {
545 $dbo = JFactory::getDbo();
546 $app = JFactory::getApplication();
547
548 $bid = $app->input->getInt('bid', 0);
549
550 if (!JSession::checkToken() && !JSession::checkToken('get')) {
551 $app->enqueueMessage(JText::translate('JINVALID_TOKEN'), 'error');
552 $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $bid);
553 $app->close();
554 }
555
556 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid;
557 $dbo->setQuery($q, 0, 1);
558 $dbo->execute();
559 if (!$dbo->getNumRows()) {
560 $app->enqueueMessage('Booking not found', 'error');
561 $app->redirect('index.php?option=com_vikbooking&task=orders');
562 $app->close();
563 }
564
565 $booking = $dbo->loadAssoc();
566 if ($booking['status'] != 'cancelled') {
567 $app->enqueueMessage('Booking status must be -Cancelled-', 'error');
568 $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']);
569 $app->close();
570 }
571
572 $q = "UPDATE `#__vikbooking_orders` SET `status`='standby' WHERE `id`=" . $booking['id'];
573 $dbo->setQuery($q);
574 $dbo->execute();
575
576 $app->enqueueMessage(JText::translate('JLIB_APPLICATION_SAVE_SUCCESS'));
577 $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']);
578 $app->close();
579 }
580
581 /**
582 * AJAX endpoint to assign a room index to a room booking record.
583 *
584 * @return void
585 */
586 public function set_room_booking_subunit()
587 {
588 if (!JSession::checkToken()) {
589 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
590 }
591
592 $dbo = JFactory::getDbo();
593 $app = JFactory::getApplication();
594
595 $bid = $app->input->getInt('bid', 0);
596 $rid = $app->input->getInt('rid', 0);
597 $orkey = $app->input->getInt('orkey', 0);
598 $rindex = $app->input->getInt('rindex', 0);
599
600 if (empty($bid) || empty($rid)) {
601 VBOHttpDocument::getInstance()->close(500, 'Missing request values');
602 }
603
604 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid;
605 $dbo->setQuery($q, 0, 1);
606 $booking = $dbo->loadAssoc();
607 if (!$booking) {
608 VBOHttpDocument::getInstance()->close(404, 'Booking not found');
609 }
610
611 $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
612 if (!$booking_rooms) {
613 VBOHttpDocument::getInstance()->close(500, 'No rooms booking found');
614 }
615
616 if (!isset($booking_rooms[$orkey]) || $booking_rooms[$orkey]['idroom'] != $rid) {
617 VBOHttpDocument::getInstance()->close(500, 'Invalid room booking record');
618 }
619
620 // update room record
621 $room_record = new stdClass;
622 $room_record->id = $booking_rooms[$orkey]['id'];
623 $room_record->roomindex = $rindex;
624
625 $dbo->updateObject('#__vikbooking_ordersrooms', $room_record, 'id');
626
627 // build list of affected nights
628 $nights_list_ymd = [];
629 $from_checkin_info = getdate($booking['checkin']);
630 for ($n = 0; $n < $booking['days']; $n++) {
631 // push affected night
632 $nights_list_ymd[] = date('Y-m-d', mktime(0, 0, 0, $from_checkin_info['mon'], ($from_checkin_info['mday'] + $n), $from_checkin_info['year']));
633 }
634
635 // build return values
636 $response = [
637 'bid' => $booking['id'],
638 'rid' => $booking_rooms[$orkey]['idroom'],
639 'rindex' => $rindex,
640 'from' => date('Y-m-d', $booking['checkin']),
641 'to' => date('Y-m-d', $booking['checkout']),
642 'nights' => $nights_list_ymd,
643 ];
644
645 // output the JSON encoded object
646 VBOHttpDocument::getInstance()->json($response);
647 }
648
649 /**
650 * AJAX endpoint to swap one sub-unit index with another for the same room ID and dates.
651 *
652 * @return void
653 *
654 * @since 1.16.2 (J) - 1.6.2 (WP)
655 */
656 public function swap_room_subunits()
657 {
658 if (!JSession::checkToken()) {
659 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
660 }
661
662 $dbo = JFactory::getDbo();
663 $app = JFactory::getApplication();
664
665 $bid_one = $app->input->getInt('bid_one', 0);
666 $bid_two = $app->input->getInt('bid_two', 0);
667 $rid = $app->input->getInt('rid', 0);
668 $index_one = $app->input->getInt('index_one', 0);
669 $index_two = $app->input->getInt('index_two', 0);
670 $checkin = $app->input->getString('checkin', '');
671
672 if (!$bid_one || !$bid_two || !$rid || !$index_one || !$index_two || $index_one < 0 || $index_two < 0) {
673 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values');
674 }
675
676 // collect the booking information
677 $booking_one = VikBooking::getBookingInfoFromID($bid_one);
678 $booking_two = VikBooking::getBookingInfoFromID($bid_two);
679 if (!$booking_one || !$booking_two) {
680 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservations');
681 }
682
683 // get room reservation records
684 $rooms_one = VikBooking::loadOrdersRoomsData($bid_one);
685 $rooms_two = VikBooking::loadOrdersRoomsData($bid_two);
686 if (!$rooms_one || !$rooms_two) {
687 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved room reservation records');
688 }
689
690 // clone room reservation records
691 $current_rooms_one = $rooms_one;
692 $current_rooms_two = $rooms_two;
693
694 // find the record IDs involved and room name
695 $update_id_one = null;
696 $update_id_two = null;
697 $room_name = '';
698
699 foreach ($rooms_one as $k => $room_one) {
700 if ($room_one['idroom'] == $rid && $room_one['roomindex'] == $index_one) {
701 $update_id_one = $room_one['id'];
702 $room_name = $room_one['room_name'];
703 $current_rooms_one[$k]['roomindex'] = $index_two;
704 break;
705 }
706 }
707
708 foreach ($rooms_two as $k => $room_two) {
709 if ($room_two['idroom'] == $rid && $room_two['roomindex'] == $index_two) {
710 $update_id_two = $room_two['id'];
711 $room_name = $room_two['room_name'];
712 $current_rooms_two[$k]['roomindex'] = $index_one;
713 break;
714 }
715 }
716
717 if (!$update_id_one || !$update_id_two) {
718 VBOHttpDocument::getInstance()->close(500, 'Could not find the involved room reservation record IDs');
719 }
720
721 // swap first room record
722 $q = $dbo->getQuery(true);
723
724 $q->update($dbo->qn('#__vikbooking_ordersrooms'))
725 ->set($dbo->qn('roomindex') . ' = ' . $index_two)
726 ->where($dbo->qn('id') . ' = ' . (int)$update_id_one);
727
728 $dbo->setQuery($q);
729 $dbo->execute();
730
731 $result = (bool)$dbo->getAffectedRows();
732
733 // swap second room record
734 $q = $dbo->getQuery(true);
735
736 $q->update($dbo->qn('#__vikbooking_ordersrooms'))
737 ->set($dbo->qn('roomindex') . ' = ' . $index_one)
738 ->where($dbo->qn('id') . ' = ' . (int)$update_id_two);
739
740 $dbo->setQuery($q);
741 $dbo->execute();
742
743 $result = $result || (bool)$dbo->getAffectedRows();
744
745 if (!$result) {
746 VBOHttpDocument::getInstance()->close(500, 'No records were updated for the involved room reservation IDs');
747 }
748
749 // update history record(s)
750 $user = JFactory::getUser();
751 VikBooking::getBookingHistoryInstance($booking_one['id'])
752 ->setPrevBooking(array_merge($booking_one, ['rooms_info' => $rooms_one]))
753 ->setBookingData($booking_one, $current_rooms_one)
754 ->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_one, $index_two) . " ({$user->name})");
755 if ($booking_one['id'] != $booking_two['id']) {
756 // update history for the second booking involved
757 VikBooking::getBookingHistoryInstance($booking_two['id'])
758 ->setPrevBooking(array_merge($booking_two, ['rooms_info' => $rooms_two]))
759 ->setBookingData($booking_two, $current_rooms_two)
760 ->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_two, $index_one) . " ({$user->name})");
761 }
762
763 // output the JSON encoded response object
764 VBOHttpDocument::getInstance()->json([
765 'swap_from' => $index_one,
766 'swap_to' => $index_two,
767 ]);
768 }
769
770 /**
771 * AJAX endpoint to remove the type flag (i.e. "overbooking") from a booking ID.
772 *
773 * @return void
774 */
775 public function delete_type_flag()
776 {
777 if (!JSession::checkToken()) {
778 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
779 }
780
781 $dbo = JFactory::getDbo();
782 $app = JFactory::getApplication();
783
784 $bid = $app->input->getUInt('bid', 0);
785 $flag = $app->input->getString('flag', '');
786
787 if (!$bid) {
788 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
789 }
790
791 $q = $dbo->getQuery(true);
792
793 $q->update($dbo->qn('#__vikbooking_orders'))
794 ->set($dbo->qn('type') . ' = ' . $dbo->q(''))
795 ->where($dbo->qn('id') . ' = ' . $bid);
796
797 $dbo->setQuery($q);
798 $dbo->execute();
799
800 if (!(bool)$dbo->getAffectedRows()) {
801 VBOHttpDocument::getInstance()->close(500, 'Could not update the booking record');
802 }
803
804 if (!strcasecmp($flag, 'overbooking')) {
805 // update history records
806 $user = JFactory::getUser();
807 VikBooking::getBookingHistoryInstance($bid)->store('OB', JText::translate('VBO_OVERBOOKING_FLAG_REMOVED') . " ({$user->name})");
808 }
809
810 VBOHttpDocument::getInstance()->json([$bid => 'ok']);
811 }
812
813 /**
814 * AJAX endpoint to set the AI options related to automatic guest review for a booking.
815 *
816 * @return void
817 *
818 * @since 1.16.10 (J) - 1.6.10 (WP)
819 */
820 public function set_ai_auto_guest_review_opt()
821 {
822 if (!JSession::checkToken()) {
823 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
824 }
825
826 $dbo = JFactory::getDbo();
827 $app = JFactory::getApplication();
828
829 $bid = $app->input->getInt('bid', 0);
830 $opt = $app->input->get('opt', [], 'array');
831
832 if (!$bid || !$opt) {
833 VBOHttpDocument::getInstance()->close(500, 'Missing request values');
834 }
835
836 // ensure the booking exists
837 $booking = VikBooking::getBookingInfoFromID($bid);
838 if (!$booking) {
839 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation');
840 }
841
842 // get AI options for this booking
843 $booking_ai_opts = (array) VBOFactory::getConfig()->getArray('ai_auto_guest_review_opt_' . $booking['id'], []);
844
845 // update the requested options
846 foreach ($opt as $param => $val) {
847 if (is_bool($val) || is_numeric($val)) {
848 $val = (int) $val;
849 }
850 // set new option value
851 $booking_ai_opts[$param] = $val;
852 }
853
854 // update AI options for this booking
855 VBOFactory::getConfig()->set('ai_auto_guest_review_opt_' . $booking['id'], $booking_ai_opts);
856
857 // return the new preferences
858 VBOHttpDocument::getInstance()->json($booking_ai_opts);
859 }
860
861 /**
862 * AJAX endpoint to register a new taking (payment).
863 *
864 * @return void
865 *
866 * @since 1.16.10 (J) - 1.6.10 (WP)
867 */
868 public function add_taking()
869 {
870 if (!JSession::checkToken()) {
871 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
872 }
873
874 $dbo = JFactory::getDbo();
875 $app = JFactory::getApplication();
876
877 $bid = $app->input->getInt('bid', 0);
878 $amount = $app->input->getFloat('amount', 0);
879 $payid = $app->input->getInt('payid', 0);
880 $descr = $app->input->getString('descr', '');
881
882 if (!$bid || !$amount || $amount < 0) {
883 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.');
884 }
885
886 // ensure the booking exists
887 $booking = VikBooking::getBookingInfoFromID($bid);
888 if (!$booking) {
889 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.');
890 }
891
892 $new_tot_paid = $booking['totpaid'] + $amount;
893
894 // update booking record
895 $dbo->setQuery(
896 $dbo->getQuery(true)
897 ->update($dbo->qn('#__vikbooking_orders'))
898 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
899 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
900 );
901 $dbo->execute();
902
903 // update booking history
904 $extra_data = new stdClass;
905 $extra_data->register_new = 1;
906 $extra_data->amount_paid = $amount;
907 $extra_data->payment_method = $descr;
908 if (!empty($payid)) {
909 $pay_info = VikBooking::getPayment($payid);
910 if ($pay_info) {
911 $extra_data->payment_method = $pay_info['name'];
912 }
913 }
914 VikBooking::getBookingHistoryInstance($booking['id'])
915 ->setExtraData($extra_data)
916 ->store(
917 'PU',
918 JText::sprintf('VBOPREVAMOUNTPAID', VikBooking::numberFormat($booking['totpaid']) . (!empty($extra_data->payment_method) ? ' (' . $extra_data->payment_method . ')' : ''))
919 );
920
921 // process completed
922 VBOHttpDocument::getInstance()->json([
923 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false),
924 ]);
925 }
926
927 /**
928 * AJAX endpoint to update a taking (payment) and related history record.
929 *
930 * @return void
931 *
932 * @since 1.16.10 (J) - 1.6.10 (WP)
933 */
934 public function update_taking()
935 {
936 if (!JSession::checkToken()) {
937 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
938 }
939
940 $dbo = JFactory::getDbo();
941 $app = JFactory::getApplication();
942
943 $bid = $app->input->getInt('bid', 0);
944 $hid = $app->input->getInt('hid', 0);
945 $amount = $app->input->getFloat('amount', 0);
946 $descr = $app->input->getString('descr', '');
947 $htype = $app->input->getString('htype', 'PU');
948
949 if (!$bid || !$hid || !$amount || $amount < 0) {
950 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.');
951 }
952
953 // ensure the booking exists
954 $booking = VikBooking::getBookingInfoFromID($bid);
955 if (!$booking) {
956 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.');
957 }
958
959 // access the current history record
960 $q = $dbo->getQuery(true)
961 ->select('*')
962 ->from($dbo->qn('#__vikbooking_orderhistory'))
963 ->where($dbo->qn('id') . ' = ' . (int) $hid)
964 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
965 ->where($dbo->qn('type') . ' = ' . $dbo->q($htype));
966 $dbo->setQuery($q, 0, 1);
967 $history = $dbo->loadAssoc();
968
969 if (!$history) {
970 VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.');
971 }
972
973 // get previous amount paid
974 $history_data = (object) json_decode(($history['data'] ?: '{}'));
975 $prev_amount_paid = $history_data->amount_paid ?? 0;
976
977 // calculate new amount paid
978 if ($prev_amount_paid > $amount) {
979 $new_tot_paid = $booking['totpaid'] - ($prev_amount_paid - $amount);
980 } else {
981 $new_tot_paid = $booking['totpaid'] + ($amount - $prev_amount_paid);
982 }
983
984 // update booking record
985 $dbo->setQuery(
986 $dbo->getQuery(true)
987 ->update($dbo->qn('#__vikbooking_orders'))
988 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
989 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
990 );
991 $dbo->execute();
992
993 // get currently logged user
994 $user = JFactory::getUser();
995 $uname = $user->name;
996
997 // update history extra data
998 $history_data->register_new = 1;
999 $history_data->updated = JFactory::getDate()->toSql();
1000 $history_data->updated_by = $uname;
1001 $history_data->amount_paid = $amount;
1002 $history_data->payment_method = $descr;
1003
1004 // set new record description
1005 $new_descr = trim($history['descr'] . "\n* " . JText::sprintf('VBO_MODIFIED_ON_SMT', JFactory::getDate()->toSql(true) . ' (' . $uname . ')'));
1006
1007 // update history record
1008 $dbo->setQuery(
1009 $dbo->getQuery(true)
1010 ->update($dbo->qn('#__vikbooking_orderhistory'))
1011 ->set($dbo->qn('descr') . ' = ' . $dbo->q($new_descr))
1012 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
1013 ->set($dbo->qn('data') . ' = ' . $dbo->q(json_encode($history_data)))
1014 ->where($dbo->qn('id') . ' = ' . (int) $history['id'])
1015 );
1016 $dbo->execute();
1017
1018 // process completed
1019 VBOHttpDocument::getInstance()->json([
1020 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false),
1021 ]);
1022 }
1023
1024 /**
1025 * AJAX endpoint to delete a booking history event.
1026 *
1027 * @return void
1028 *
1029 * @since 1.16.10 (J) - 1.6.10 (WP)
1030 */
1031 public function delete_history_record()
1032 {
1033 if (!JSession::checkToken()) {
1034 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
1035 }
1036
1037 $dbo = JFactory::getDbo();
1038 $app = JFactory::getApplication();
1039
1040 $bid = $app->input->getInt('bid', 0);
1041 $hid = $app->input->getInt('hid', 0);
1042 $htype = $app->input->getString('htype', 'PU');
1043
1044 if (!$bid || !$hid) {
1045 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.');
1046 }
1047
1048 // ensure the booking exists
1049 $booking = VikBooking::getBookingInfoFromID($bid);
1050 if (!$booking) {
1051 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.');
1052 }
1053
1054 // access the current history record
1055 $q = $dbo->getQuery(true)
1056 ->select('*')
1057 ->from($dbo->qn('#__vikbooking_orderhistory'))
1058 ->where($dbo->qn('id') . ' = ' . (int) $hid)
1059 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1060 ->where($dbo->qn('type') . ' = ' . $dbo->q($htype));
1061 $dbo->setQuery($q, 0, 1);
1062 $history = $dbo->loadAssoc();
1063
1064 if (!$history) {
1065 VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.');
1066 }
1067
1068 // get previous amount paid
1069 $history_data = (object) json_decode(($history['data'] ?: '{}'));
1070 $prev_amount_paid = $history_data->amount_paid ?? 0;
1071
1072 // calculate new amount paid
1073 $new_tot_paid = $booking['totpaid'];
1074 if ($prev_amount_paid) {
1075 $new_tot_paid = $booking['totpaid'] - $prev_amount_paid;
1076 }
1077
1078 // update booking record
1079 $dbo->setQuery(
1080 $dbo->getQuery(true)
1081 ->update($dbo->qn('#__vikbooking_orders'))
1082 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
1083 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
1084 );
1085 $dbo->execute();
1086
1087 // delete history record
1088 $dbo->setQuery(
1089 $dbo->getQuery(true)
1090 ->delete($dbo->qn('#__vikbooking_orderhistory'))
1091 ->where($dbo->qn('id') . ' = ' . (int) $hid)
1092 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1093 ->where($dbo->qn('type') . ' = ' . $dbo->q($htype))
1094 );
1095
1096 $dbo->execute();
1097 $aff_rows = $dbo->getAffectedRows();
1098
1099 // process completed
1100 VBOHttpDocument::getInstance()->json([
1101 'rows' => $aff_rows,
1102 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false),
1103 ]);
1104 }
1105
1106 /**
1107 * AJAX endpoint to check whether a whole booking can be modified with new stay dates.
1108 *
1109 * @return void
1110 *
1111 * @since 1.18.2 (J) - 1.8.2 (WP)
1112 */
1113 public function is_booking_modifiable()
1114 {
1115 if (!JSession::checkToken()) {
1116 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
1117 }
1118
1119 $app = JFactory::getApplication();
1120
1121 $bid = $app->input->getInt('bid', 0);
1122 $checkin = $app->input->getString('checkin');
1123 $checkout = $app->input->getString('checkout');
1124
1125 if (!$bid || empty($checkin) || empty($checkout)) {
1126 VBOHttpDocument::getInstance()->close(400, 'Missing booking information.');
1127 }
1128
1129 try {
1130 // check whether the booking can be modified
1131 $modifiable = (new VBOModelReservation)->bookingModifiable($bid, $checkin, $checkout);
1132 } catch (Exception $e) {
1133 // propagate the error
1134 VBOHttpDocument::getInstance()->close($e->getCode() ?: 400, $e->getMessage() ?: 'Validation failure.');
1135 }
1136
1137 // validation process completed
1138 VBOHttpDocument::getInstance()->json([
1139 'modifiable' => $modifiable,
1140 'bid' => $bid,
1141 'checkin' => $checkin,
1142 'checkout' => $checkout,
1143 ]);
1144 }
1145
1146 /**
1147 * AJAX endpoint to resolve room (sub-unit) booking record assignment.
1148 *
1149 * @return void
1150 *
1151 * @since 1.18.7 (J) - 1.8.7 (WP)
1152 */
1153 public function resolve_room_assignment()
1154 {
1155 $app = JFactory::getApplication();
1156
1157 if (!JSession::checkToken()) {
1158 VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
1159 }
1160
1161 // gather request values
1162 $booking_id = $app->input->getUInt('bid', 0);
1163 $id_room = $app->input->getUInt('id_room', 0);
1164 $room_booking_id = $app->input->getUInt('room_booking_id', 0);
1165 $roomres_index = $app->input->getInt('booking_room_index', -1);
1166 $days_bound = $app->input->getUInt('days_bound', 0);
1167 $max_exec_time = $app->input->getUInt('max_exec_time', 0);
1168 $count_all = $app->input->getBool('count_all', false);
1169
1170 $skip_booking_ids = (array) $app->input->getInt('skip_booking_ids', []);
1171 $skip_moveset_signatures = (array) $app->input->getString('skip_moveset_signatures', []);
1172
1173 if (!$booking_id) {
1174 VBOHttpDocument::getInstance($app)->close(400, 'Missing booking information.');
1175 }
1176
1177 if ($room_booking_id) {
1178 // an exact booking room record ID was provided
1179 $booking_rooms = VikBooking::loadOrdersRoomsData($booking_id);
1180 foreach ($booking_rooms as $index => $booking_room) {
1181 if ($booking_room['id'] == $room_booking_id) {
1182 // match found, set reliable properties
1183 $id_room = $booking_room['idroom'];
1184 $roomres_index = $index;
1185 break;
1186 }
1187 }
1188 }
1189
1190 // build room re-assignment options
1191 $options = [
1192 'id' => $booking_id,
1193 'id_room' => $id_room,
1194 'booking_room_index' => $roomres_index,
1195 'days_bound' => $days_bound ?: null,
1196 'max_exec_time' => $max_exec_time,
1197 'count_all' => $count_all,
1198 'skip_booking_ids' => $skip_booking_ids,
1199 'skip_moveset_signatures' => $skip_moveset_signatures,
1200 ];
1201
1202 try {
1203 // let the room re-assignment operations start
1204 $moveset = VBOBookingRelocator::getInstance($options)
1205 ->findRelocation();
1206 } catch (Throwable $e) {
1207 // propagate the error
1208 VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage());
1209 }
1210
1211 // build moveset layout data
1212 $layout_data = [
1213 'booking_id' => $booking_id,
1214 'moveset' => $moveset,
1215 'options' => $options,
1216 ];
1217
1218 // render moveset layout HTML
1219 $movesetHtml = JLayoutHelper::render('overview.relocationmoveset', $layout_data);
1220
1221 // validation process completed
1222 VBOHttpDocument::getInstance($app)->json([
1223 'html' => $movesetHtml,
1224 'movesetSignature' => $moveset->getSignature(),
1225 'solutionsCount' => $moveset->getSolutionsCount(),
1226 ]);
1227 }
1228
1229 /**
1230 * AJAX endpoint to apply a moveset to resolve room assignments.
1231 *
1232 * @return void
1233 *
1234 * @since 1.18.7 (J) - 1.8.7 (WP)
1235 * @since 1.18.11 (J) - 1.8.11 (WP) operation is performed through a VBOBookingRelocator object.
1236 */
1237 public function apply_room_assignment()
1238 {
1239 $app = JFactory::getApplication();
1240 $dbo = JFactory::getDbo();
1241
1242 if (!JSession::checkToken()) {
1243 VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
1244 }
1245
1246 // gather request values
1247 $moveset = $app->input->getString('moveset');
1248 $undo = $app->input->getBool('undo', false);
1249
1250 if (!$moveset) {
1251 VBOHttpDocument::getInstance($app)->close(400, 'Missing reassignment moveset.');
1252 }
1253
1254 // identify booking ID
1255 $bookingId = null;
1256
1257 // scan operation steps
1258 $operationSteps = explode('-', $moveset);
1259 foreach ($operationSteps as $operationStep) {
1260 // obtain move details
1261 $moveData = explode('.', $operationStep);
1262 if (count($moveData) < 4) {
1263 VBOHttpDocument::getInstance($app)->close(400, 'Invalid moveset operation.');
1264 }
1265
1266 // keep setting booking ID until last moveset operation
1267 $bookingId = (int) ($moveData[0] ?? 0);
1268 }
1269
1270 if (!$bookingId) {
1271 VBOHttpDocument::getInstance($app)->close(400, 'No valid moveset operations.');
1272 }
1273
1274 try {
1275 // access booking relocator object
1276 $relocator = VBOBookingRelocator::getInstance([
1277 'id' => $bookingId,
1278 ]);
1279
1280 // execute given relocation moveset
1281 $relocator->execMovesetSignature($moveset, $undo);
1282 } catch (Exception $e) {
1283 // raise error
1284 VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage());
1285 }
1286
1287 // process completed
1288 VBOHttpDocument::getInstance($app)->json([
1289 'success' => true,
1290 ]);
1291 }
1292 }
1293