PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
1.8.15 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 All 36 releases
vikbooking / admin / controllers / bookings.php

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

1,133 lines 34.8 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 // find the record IDs involved and room name
691 $update_id_one = null;
692 $update_id_two = null;
693 $room_name = '';
694
695 foreach ($rooms_one as $room_one) {
696 if ($room_one['idroom'] == $rid && $room_one['roomindex'] == $index_one) {
697 $update_id_one = $room_one['id'];
698 $room_name = $room_one['room_name'];
699 break;
700 }
701 }
702
703 foreach ($rooms_two as $room_two) {
704 if ($room_two['idroom'] == $rid && $room_two['roomindex'] == $index_two) {
705 $update_id_two = $room_two['id'];
706 $room_name = $room_two['room_name'];
707 break;
708 }
709 }
710
711 if (!$update_id_one || !$update_id_two) {
712 VBOHttpDocument::getInstance()->close(500, 'Could not find the involved room reservation record IDs');
713 }
714
715 // swap first room record
716 $q = $dbo->getQuery(true);
717
718 $q->update($dbo->qn('#__vikbooking_ordersrooms'))
719 ->set($dbo->qn('roomindex') . ' = ' . $index_two)
720 ->where($dbo->qn('id') . ' = ' . (int)$update_id_one);
721
722 $dbo->setQuery($q);
723 $dbo->execute();
724
725 $result = (bool)$dbo->getAffectedRows();
726
727 // swap second room record
728 $q = $dbo->getQuery(true);
729
730 $q->update($dbo->qn('#__vikbooking_ordersrooms'))
731 ->set($dbo->qn('roomindex') . ' = ' . $index_one)
732 ->where($dbo->qn('id') . ' = ' . (int)$update_id_two);
733
734 $dbo->setQuery($q);
735 $dbo->execute();
736
737 $result = $result || (bool)$dbo->getAffectedRows();
738
739 if (!$result) {
740 VBOHttpDocument::getInstance()->close(500, 'No records were updated for the involved room reservation IDs');
741 }
742
743 // update history records
744 $user = JFactory::getUser();
745 VikBooking::getBookingHistoryInstance()->setBid($booking_one['id'])->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_one, $index_two) . " ({$user->name})");
746 if ($booking_one['id'] != $booking_two['id']) {
747 VikBooking::getBookingHistoryInstance()->setBid($booking_two['id'])->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_two, $index_one) . " ({$user->name})");
748 }
749
750 // output the JSON encoded response object
751 VBOHttpDocument::getInstance()->json([
752 'swap_from' => $index_one,
753 'swap_to' => $index_two,
754 ]);
755 }
756
757 /**
758 * AJAX endpoint to remove the type flag (i.e. "overbooking") from a booking ID.
759 *
760 * @return void
761 */
762 public function delete_type_flag()
763 {
764 if (!JSession::checkToken()) {
765 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
766 }
767
768 $dbo = JFactory::getDbo();
769 $app = JFactory::getApplication();
770
771 $bid = $app->input->getUInt('bid', 0);
772 $flag = $app->input->getString('flag', '');
773
774 if (!$bid) {
775 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
776 }
777
778 $q = $dbo->getQuery(true);
779
780 $q->update($dbo->qn('#__vikbooking_orders'))
781 ->set($dbo->qn('type') . ' = ' . $dbo->q(''))
782 ->where($dbo->qn('id') . ' = ' . $bid);
783
784 $dbo->setQuery($q);
785 $dbo->execute();
786
787 if (!(bool)$dbo->getAffectedRows()) {
788 VBOHttpDocument::getInstance()->close(500, 'Could not update the booking record');
789 }
790
791 if (!strcasecmp($flag, 'overbooking')) {
792 // update history records
793 $user = JFactory::getUser();
794 VikBooking::getBookingHistoryInstance($bid)->store('OB', JText::translate('VBO_OVERBOOKING_FLAG_REMOVED') . " ({$user->name})");
795 }
796
797 VBOHttpDocument::getInstance()->json([$bid => 'ok']);
798 }
799
800 /**
801 * AJAX endpoint to set the AI options related to automatic guest review for a booking.
802 *
803 * @return void
804 *
805 * @since 1.16.10 (J) - 1.6.10 (WP)
806 */
807 public function set_ai_auto_guest_review_opt()
808 {
809 if (!JSession::checkToken()) {
810 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
811 }
812
813 $dbo = JFactory::getDbo();
814 $app = JFactory::getApplication();
815
816 $bid = $app->input->getInt('bid', 0);
817 $opt = $app->input->get('opt', [], 'array');
818
819 if (!$bid || !$opt) {
820 VBOHttpDocument::getInstance()->close(500, 'Missing request values');
821 }
822
823 // ensure the booking exists
824 $booking = VikBooking::getBookingInfoFromID($bid);
825 if (!$booking) {
826 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation');
827 }
828
829 // get AI options for this booking
830 $booking_ai_opts = (array) VBOFactory::getConfig()->getArray('ai_auto_guest_review_opt_' . $booking['id'], []);
831
832 // update the requested options
833 foreach ($opt as $param => $val) {
834 if (is_bool($val) || is_numeric($val)) {
835 $val = (int) $val;
836 }
837 // set new option value
838 $booking_ai_opts[$param] = $val;
839 }
840
841 // update AI options for this booking
842 VBOFactory::getConfig()->set('ai_auto_guest_review_opt_' . $booking['id'], $booking_ai_opts);
843
844 // return the new preferences
845 VBOHttpDocument::getInstance()->json($booking_ai_opts);
846 }
847
848 /**
849 * AJAX endpoint to register a new taking (payment).
850 *
851 * @return void
852 *
853 * @since 1.16.10 (J) - 1.6.10 (WP)
854 */
855 public function add_taking()
856 {
857 if (!JSession::checkToken()) {
858 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
859 }
860
861 $dbo = JFactory::getDbo();
862 $app = JFactory::getApplication();
863
864 $bid = $app->input->getInt('bid', 0);
865 $amount = $app->input->getFloat('amount', 0);
866 $payid = $app->input->getInt('payid', 0);
867 $descr = $app->input->getString('descr', '');
868
869 if (!$bid || !$amount || $amount < 0) {
870 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.');
871 }
872
873 // ensure the booking exists
874 $booking = VikBooking::getBookingInfoFromID($bid);
875 if (!$booking) {
876 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.');
877 }
878
879 $new_tot_paid = $booking['totpaid'] + $amount;
880
881 // update booking record
882 $dbo->setQuery(
883 $dbo->getQuery(true)
884 ->update($dbo->qn('#__vikbooking_orders'))
885 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
886 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
887 );
888 $dbo->execute();
889
890 // update booking history
891 $extra_data = new stdClass;
892 $extra_data->register_new = 1;
893 $extra_data->amount_paid = $amount;
894 $extra_data->payment_method = $descr;
895 if (!empty($payid)) {
896 $pay_info = VikBooking::getPayment($payid);
897 if ($pay_info) {
898 $extra_data->payment_method = $pay_info['name'];
899 }
900 }
901 VikBooking::getBookingHistoryInstance($booking['id'])
902 ->setExtraData($extra_data)
903 ->store(
904 'PU',
905 JText::sprintf('VBOPREVAMOUNTPAID', VikBooking::numberFormat($booking['totpaid']) . (!empty($extra_data->payment_method) ? ' (' . $extra_data->payment_method . ')' : ''))
906 );
907
908 // process completed
909 VBOHttpDocument::getInstance()->json([
910 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false),
911 ]);
912 }
913
914 /**
915 * AJAX endpoint to update a taking (payment) and related history record.
916 *
917 * @return void
918 *
919 * @since 1.16.10 (J) - 1.6.10 (WP)
920 */
921 public function update_taking()
922 {
923 if (!JSession::checkToken()) {
924 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
925 }
926
927 $dbo = JFactory::getDbo();
928 $app = JFactory::getApplication();
929
930 $bid = $app->input->getInt('bid', 0);
931 $hid = $app->input->getInt('hid', 0);
932 $amount = $app->input->getFloat('amount', 0);
933 $descr = $app->input->getString('descr', '');
934 $htype = $app->input->getString('htype', 'PU');
935
936 if (!$bid || !$hid || !$amount || $amount < 0) {
937 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.');
938 }
939
940 // ensure the booking exists
941 $booking = VikBooking::getBookingInfoFromID($bid);
942 if (!$booking) {
943 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.');
944 }
945
946 // access the current history record
947 $q = $dbo->getQuery(true)
948 ->select('*')
949 ->from($dbo->qn('#__vikbooking_orderhistory'))
950 ->where($dbo->qn('id') . ' = ' . (int) $hid)
951 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
952 ->where($dbo->qn('type') . ' = ' . $dbo->q($htype));
953 $dbo->setQuery($q, 0, 1);
954 $history = $dbo->loadAssoc();
955
956 if (!$history) {
957 VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.');
958 }
959
960 // get previous amount paid
961 $history_data = (object) json_decode(($history['data'] ?: '{}'));
962 $prev_amount_paid = $history_data->amount_paid ?? 0;
963
964 // calculate new amount paid
965 if ($prev_amount_paid > $amount) {
966 $new_tot_paid = $booking['totpaid'] - ($prev_amount_paid - $amount);
967 } else {
968 $new_tot_paid = $booking['totpaid'] + ($amount - $prev_amount_paid);
969 }
970
971 // update booking record
972 $dbo->setQuery(
973 $dbo->getQuery(true)
974 ->update($dbo->qn('#__vikbooking_orders'))
975 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
976 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
977 );
978 $dbo->execute();
979
980 // get currently logged user
981 $user = JFactory::getUser();
982 $uname = $user->name;
983
984 // update history extra data
985 $history_data->register_new = 1;
986 $history_data->updated = JFactory::getDate()->toSql();
987 $history_data->updated_by = $uname;
988 $history_data->amount_paid = $amount;
989 $history_data->payment_method = $descr;
990
991 // set new record description
992 $new_descr = trim($history['descr'] . "\n* " . JText::sprintf('VBO_MODIFIED_ON_SMT', JFactory::getDate()->toSql(true) . ' (' . $uname . ')'));
993
994 // update history record
995 $dbo->setQuery(
996 $dbo->getQuery(true)
997 ->update($dbo->qn('#__vikbooking_orderhistory'))
998 ->set($dbo->qn('descr') . ' = ' . $dbo->q($new_descr))
999 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
1000 ->set($dbo->qn('data') . ' = ' . $dbo->q(json_encode($history_data)))
1001 ->where($dbo->qn('id') . ' = ' . (int) $history['id'])
1002 );
1003 $dbo->execute();
1004
1005 // process completed
1006 VBOHttpDocument::getInstance()->json([
1007 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false),
1008 ]);
1009 }
1010
1011 /**
1012 * AJAX endpoint to delete a booking history event.
1013 *
1014 * @return void
1015 *
1016 * @since 1.16.10 (J) - 1.6.10 (WP)
1017 */
1018 public function delete_history_record()
1019 {
1020 if (!JSession::checkToken()) {
1021 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
1022 }
1023
1024 $dbo = JFactory::getDbo();
1025 $app = JFactory::getApplication();
1026
1027 $bid = $app->input->getInt('bid', 0);
1028 $hid = $app->input->getInt('hid', 0);
1029 $htype = $app->input->getString('htype', 'PU');
1030
1031 if (!$bid || !$hid) {
1032 VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.');
1033 }
1034
1035 // ensure the booking exists
1036 $booking = VikBooking::getBookingInfoFromID($bid);
1037 if (!$booking) {
1038 VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.');
1039 }
1040
1041 // access the current history record
1042 $q = $dbo->getQuery(true)
1043 ->select('*')
1044 ->from($dbo->qn('#__vikbooking_orderhistory'))
1045 ->where($dbo->qn('id') . ' = ' . (int) $hid)
1046 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1047 ->where($dbo->qn('type') . ' = ' . $dbo->q($htype));
1048 $dbo->setQuery($q, 0, 1);
1049 $history = $dbo->loadAssoc();
1050
1051 if (!$history) {
1052 VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.');
1053 }
1054
1055 // get previous amount paid
1056 $history_data = (object) json_decode(($history['data'] ?: '{}'));
1057 $prev_amount_paid = $history_data->amount_paid ?? 0;
1058
1059 // calculate new amount paid
1060 $new_tot_paid = $booking['totpaid'];
1061 if ($prev_amount_paid) {
1062 $new_tot_paid = $booking['totpaid'] - $prev_amount_paid;
1063 }
1064
1065 // update booking record
1066 $dbo->setQuery(
1067 $dbo->getQuery(true)
1068 ->update($dbo->qn('#__vikbooking_orders'))
1069 ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid))
1070 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
1071 );
1072 $dbo->execute();
1073
1074 // delete history record
1075 $dbo->setQuery(
1076 $dbo->getQuery(true)
1077 ->delete($dbo->qn('#__vikbooking_orderhistory'))
1078 ->where($dbo->qn('id') . ' = ' . (int) $hid)
1079 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1080 ->where($dbo->qn('type') . ' = ' . $dbo->q($htype))
1081 );
1082
1083 $dbo->execute();
1084 $aff_rows = $dbo->getAffectedRows();
1085
1086 // process completed
1087 VBOHttpDocument::getInstance()->json([
1088 'rows' => $aff_rows,
1089 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false),
1090 ]);
1091 }
1092
1093 /**
1094 * AJAX endpoint to check whether a whole booking can be modified with new stay dates.
1095 *
1096 * @return void
1097 *
1098 * @since 1.18.2 (J) - 1.8.2 (WP)
1099 */
1100 public function is_booking_modifiable()
1101 {
1102 if (!JSession::checkToken()) {
1103 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
1104 }
1105
1106 $app = JFactory::getApplication();
1107
1108 $bid = $app->input->getInt('bid', 0);
1109 $checkin = $app->input->getString('checkin');
1110 $checkout = $app->input->getString('checkout');
1111
1112 if (!$bid || empty($checkin) || empty($checkout)) {
1113 VBOHttpDocument::getInstance()->close(400, 'Missing booking information.');
1114 }
1115
1116 try {
1117 // check whether the booking can be modified
1118 $modifiable = (new VBOModelReservation)->bookingModifiable($bid, $checkin, $checkout);
1119 } catch (Exception $e) {
1120 // propagate the error
1121 VBOHttpDocument::getInstance()->close($e->getCode() ?: 400, $e->getMessage() ?: 'Validation failure.');
1122 }
1123
1124 // validation process completed
1125 VBOHttpDocument::getInstance()->json([
1126 'modifiable' => $modifiable,
1127 'bid' => $bid,
1128 'checkin' => $checkin,
1129 'checkout' => $checkout,
1130 ]);
1131 }
1132 }
1133