PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 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 All 37 releases
← All changes | admin/controllers/bookings.php +1249 -333 1.6.5 → 1.8.16 View file →
@@ -13,414 +13,1330 @@
13 13
14 14 /**
15 15 * VikBooking bookings controller.
16 16 *
17 - * @since 1.16.0 (J) - 1.6.0 (WP)
17 + * @since 1.16.0 (J) - 1.6.0 (WP)
18 18 */
19 19 class VikBookingControllerBookings extends JControllerAdmin
20 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 - }
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 31
32 - $dbo = JFactory::getDbo();
32 + $dbo = JFactory::getDbo();
33 33
34 - $service_name = VikRequest::getString('service_name', '', 'request');
35 - $max_results = VikRequest::getInt('max_results', 10, 'request');
34 + $service_name = VikRequest::getString('service_name', '', 'request');
35 + $max_results = VikRequest::getInt('max_results', 10, 'request');
36 36
37 - $sql_term = $dbo->quote("%{$service_name}%");
38 - $sql_clause = !empty($service_name) ? 'LIKE ' . $sql_term : 'IS NOT NULL';
37 + $sql_term = $dbo->quote("%{$service_name}%");
38 + $sql_clause = !empty($service_name) ? 'LIKE ' . $sql_term : 'IS NOT NULL';
39 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 - }
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 52
53 - $results = $dbo->loadAssocList();
53 + $results = $dbo->loadAssocList();
54 54
55 - $matching_services = [];
55 + $matching_services = [];
56 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 - }
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 78
79 - // output the JSON encoded list of matching results found
80 - VBOHttpDocument::getInstance()->json($matching_services);
81 - }
79 + // output the JSON encoded list of matching results found
80 + VBOHttpDocument::getInstance()->json($matching_services);
81 + }
82 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 - }
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 93
94 - $dbo = JFactory::getDbo();
94 + $dbo = JFactory::getDbo();
95 95
96 - $coupon_codes = VikRequest::getVar('coupon_codes', array());
96 + $coupon_codes = VikRequest::getVar('coupon_codes', array());
97 97
98 - $use_counts = [];
98 + $use_counts = [];
99 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 - }
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 109
110 - // output the JSON encoded list of coupon use counts
111 - VBOHttpDocument::getInstance()->json($use_counts);
112 - }
110 + // output the JSON encoded list of coupon use counts
111 + VBOHttpDocument::getInstance()->json($use_counts);
112 + }
113 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 - }
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 124
125 - $dbo = JFactory::getDbo();
125 + $dbo = JFactory::getDbo();
126 126
127 - $term = VikRequest::getString('term', '', 'request');
127 + $term = VikRequest::getString('term', '', 'request');
128 128
129 - $response = [
130 - 'results' => [],
131 - 'pagination' => [
132 - 'more' => false,
133 - ],
134 - ];
129 + $response = [
130 + 'results' => [],
131 + 'pagination' => [
132 + 'more' => false,
133 + ],
134 + ];
135 135
136 - if (empty($term)) {
137 - // output the JSON object with no results
138 - VBOHttpDocument::getInstance()->json($response);
139 - }
136 + if (empty($term)) {
137 + // output the JSON object with no results
138 + VBOHttpDocument::getInstance()->json($response);
139 + }
140 140
141 - $sql_term = $dbo->quote("%{$term}%");
141 + $sql_term = $dbo->quote("%{$term}%");
142 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 - $dbo->execute();
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 151
152 - if ($dbo->getNumRows()) {
153 - $customers = $dbo->loadAssocList();
154 - foreach ($customers as $k => $customer) {
155 - $customers[$k]['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']) . ' (' . $customer['tot_bookings'] . ')';
156 - }
157 - // push results found
158 - $response['results'] = $customers;
159 - }
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 + }
160 159
161 - // output the JSON encoded object with results found
162 - VBOHttpDocument::getInstance()->json($response);
163 - }
160 + // output the JSON encoded object with results found
161 + VBOHttpDocument::getInstance()->json($response);
162 + }
164 163
165 - /**
166 - * Regular task to update the status of a cancelled booking to pending (stand-by).
167 - *
168 - * @return void
169 - */
170 - public function set_to_pending()
171 - {
172 - $dbo = JFactory::getDbo();
173 - $app = JFactory::getApplication();
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 + }
174 176
175 - $bid = $app->input->getInt('bid', 0);
177 + $dbo = JFactory::getDbo();
178 + $app = JFactory::getApplication();
176 179
177 - if (!JSession::checkToken() && !JSession::checkToken('get')) {
178 - $app->enqueueMessage(JText::translate('JINVALID_TOKEN'), 'error');
179 - $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $bid);
180 - $app->close();
181 - }
180 + $term = $app->input->getString('term', '');
182 181
183 - $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid;
184 - $dbo->setQuery($q, 0, 1);
185 - $dbo->execute();
186 - if (!$dbo->getNumRows()) {
187 - $app->enqueueMessage('Booking not found', 'error');
188 - $app->redirect('index.php?option=com_vikbooking&task=orders');
189 - $app->close();
190 - }
182 + $response = [
183 + 'results' => [],
184 + 'pagination' => [
185 + 'more' => false,
186 + ],
187 + ];
191 188
192 - $booking = $dbo->loadAssoc();
193 - if ($booking['status'] != 'cancelled') {
194 - $app->enqueueMessage('Booking status must be -Cancelled-', 'error');
195 - $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']);
196 - $app->close();
197 - }
189 + if (empty($term)) {
190 + // output the JSON object with no results
191 + VBOHttpDocument::getInstance($app)->json($response);
192 + }
198 193
199 - $q = "UPDATE `#__vikbooking_orders` SET `status`='standby' WHERE `id`=" . $booking['id'];
200 - $dbo->setQuery($q);
201 - $dbo->execute();
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 + );
202 206
203 - $app->enqueueMessage(JText::translate('JLIB_APPLICATION_SAVE_SUCCESS'));
204 - $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']);
205 - $app->close();
206 - }
207 + // set results found
208 + $response['results'] = $dbo->loadAssocList();
207 209
208 - /**
209 - * AJAX endpoint to assign a room index to a room booking record.
210 - *
211 - * @return void
212 - */
213 - public function set_room_booking_subunit()
214 - {
215 - if (!JSession::checkToken()) {
216 - VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
217 - }
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']);
218 221
219 - $dbo = JFactory::getDbo();
220 - $app = JFactory::getApplication();
222 + // output the JSON encoded object with results found
223 + VBOHttpDocument::getInstance()->json($response);
224 + }
221 225
222 - $bid = $app->input->getInt('bid', 0);
223 - $rid = $app->input->getInt('rid', 0);
224 - $orkey = $app->input->getInt('orkey', 0);
225 - $rindex = $app->input->getInt('rindex', 0);
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 + }
226 238
227 - if (empty($bid) || empty($rid)) {
228 - VBOHttpDocument::getInstance()->close(500, 'Missing request values');
229 - }
239 + $dbo = JFactory::getDbo();
240 + $app = JFactory::getApplication();
230 241
231 - $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid;
232 - $dbo->setQuery($q, 0, 1);
233 - $booking = $dbo->loadAssoc();
234 - if (!$booking) {
235 - VBOHttpDocument::getInstance()->close(404, 'Booking not found');
236 - }
242 + $booking_key = trim($app->input->getString('term', ''));
243 + $booking_status = array_values(array_filter((array) $app->input->get('status', [], 'array')));
237 244
238 - $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
239 - if (!$booking_rooms) {
240 - VBOHttpDocument::getInstance()->close(500, 'No rooms booking found');
241 - }
245 + $response = [
246 + 'results' => [],
247 + 'pagination' => [
248 + 'more' => false,
249 + ],
250 + ];
242 251
243 - if (!isset($booking_rooms[$orkey]) || $booking_rooms[$orkey]['idroom'] != $rid) {
244 - VBOHttpDocument::getInstance()->close(500, 'Invalid room booking record');
245 - }
252 + if (empty($booking_key)) {
253 + // output the JSON object with no results
254 + VBOHttpDocument::getInstance($app)->json($response);
255 + }
246 256
247 - // update room record
248 - $room_record = new stdClass;
249 - $room_record->id = $booking_rooms[$orkey]['id'];
250 - $room_record->roomindex = $rindex;
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 + }
251 274
252 - $dbo->updateObject('#__vikbooking_ordersrooms', $room_record, 'id');
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');
253 294
254 - // build list of affected nights
255 - $nights_list_ymd = [];
256 - $from_checkin_info = getdate($booking['checkin']);
257 - for ($n = 0; $n < $booking['days']; $n++) {
258 - // push affected night
259 - $nights_list_ymd[] = date('Y-m-d', mktime(0, 0, 0, $from_checkin_info['mon'], ($from_checkin_info['mday'] + $n), $from_checkin_info['year']));
260 - }
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 + }
261 305
262 - // build return values
263 - $response = [
264 - 'bid' => $booking['id'],
265 - 'rid' => $booking_rooms[$orkey]['idroom'],
266 - 'rindex' => $rindex,
267 - 'from' => date('Y-m-d', $booking['checkin']),
268 - 'to' => date('Y-m-d', $booking['checkout']),
269 - 'nights' => $nights_list_ymd,
270 - ];
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 + }
271 364
272 - // output the JSON encoded object
273 - VBOHttpDocument::getInstance()->json($response);
274 - }
365 + // order by most recent bookings
366 + $q->order($dbo->qn('id') . ' DESC');
275 367
276 - /**
277 - * AJAX endpoint to swap one sub-unit index with another for the same room ID and dates.
278 - *
279 - * @return void
280 - *
281 - * @since 1.16.2 (J) - 1.6.2 (WP)
282 - */
283 - public function swap_room_subunits()
284 - {
285 - if (!JSession::checkToken()) {
286 - VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
287 - }
368 + $dbo->setQuery($q);
288 369
289 - $dbo = JFactory::getDbo();
290 - $app = JFactory::getApplication();
370 + // set results found
371 + $response['results'] = $dbo->loadAssocList();
291 372
292 - $bid_one = $app->input->getInt('bid_one', 0);
293 - $bid_two = $app->input->getInt('bid_two', 0);
294 - $rid = $app->input->getInt('rid', 0);
295 - $index_one = $app->input->getInt('index_one', 0);
296 - $index_two = $app->input->getInt('index_two', 0);
297 - $checkin = $app->input->getString('checkin', '');
373 + // default icon for website reservations
374 + $source_def_icon_cls = VikBookingIcons::i('hotel');
298 375
299 - if (!$bid_one || !$bid_two || !$rid || !$index_one || !$index_two) {
300 - VBOHttpDocument::getInstance()->close(500, 'Missing request values');
301 - }
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;
302 387
303 - // collect the booking information
304 - $booking_one = VikBooking::getBookingInfoFromID($bid_one);
305 - $booking_two = VikBooking::getBookingInfoFromID($bid_two);
306 - if (!$booking_one || !$booking_two) {
307 - VBOHttpDocument::getInstance()->close(500, 'Could not find the involved reservations');
308 - }
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 + }
309 397
310 - // get room reservation records
311 - $rooms_one = VikBooking::loadOrdersRoomsData($bid_one);
312 - $rooms_two = VikBooking::loadOrdersRoomsData($bid_two);
313 - if (!$rooms_one || !$rooms_two) {
314 - VBOHttpDocument::getInstance()->close(500, 'Could not find the involved room reservation records');
315 - }
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 + }
316 404
317 - // find the record IDs involved and room name
318 - $update_id_one = null;
319 - $update_id_two = null;
320 - $room_name = '';
405 + // return the mapped booking element
406 + return $booking;
407 + }, $response['results']);
321 408
322 - foreach ($rooms_one as $room_one) {
323 - if ($room_one['idroom'] == $rid && $room_one['roomindex'] == $index_one) {
324 - $update_id_one = $room_one['id'];
325 - $room_name = $room_one['room_name'];
326 - break;
327 - }
328 - }
409 + // output the JSON encoded object with results found
410 + VBOHttpDocument::getInstance()->json($response);
411 + }
329 412
330 - foreach ($rooms_two as $room_two) {
331 - if ($room_two['idroom'] == $rid && $room_two['roomindex'] == $index_two) {
332 - $update_id_two = $room_two['id'];
333 - $room_name = $room_two['room_name'];
334 - break;
335 - }
336 - }
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 + }
337 425
338 - if (!$update_id_one || !$update_id_two) {
339 - VBOHttpDocument::getInstance()->close(500, 'Could not find the involved room reservation record IDs');
340 - }
426 + $dbo = JFactory::getDbo();
427 + $app = JFactory::getApplication();
341 428
342 - // swap first room record
343 - $q = $dbo->getQuery(true);
429 + $search_key = trim($app->input->getString('term', ''));
344 430
345 - $q->update($dbo->qn('#__vikbooking_ordersrooms'))
346 - ->set($dbo->qn('roomindex') . ' = ' . $index_two)
347 - ->where($dbo->qn('id') . ' = ' . (int)$update_id_one);
431 + $response = [
432 + 'results' => [],
433 + 'pagination' => [
434 + 'more' => false,
435 + ],
436 + ];
348 437
349 - $dbo->setQuery($q);
350 - $dbo->execute();
438 + if (empty($search_key)) {
439 + // output the JSON object with no results
440 + VBOHttpDocument::getInstance($app)->json($response);
441 + }
351 442
352 - $result = (bool)$dbo->getAffectedRows();
443 + // start the query
444 + $q = $dbo->getQuery(true)
445 + ->select('*')
446 + ->from($dbo->qn('#__vikbooking_customers'))
447 + ->where(1);
353 448
354 - // swap second room record
355 - $q = $dbo->getQuery(true);
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 = [];
356 455
357 - $q->update($dbo->qn('#__vikbooking_ordersrooms'))
358 - ->set($dbo->qn('roomindex') . ' = ' . $index_one)
359 - ->where($dbo->qn('id') . ' = ' . (int)$update_id_two);
456 + // search by nominative
457 + $seek_clauses[] = 'CONCAT_WS(" ", ' . $dbo->qn('first_name') . ', ' . $dbo->qn('last_name') . ') LIKE ' . $dbo->q('%' . $search_key . '%');
360 458
361 - $dbo->setQuery($q);
362 - $dbo->execute();
459 + // search by company name
460 + $seek_clauses[] = $dbo->qn('company') . ' LIKE ' . $dbo->q('%' . $search_key . '%');
363 461
364 - $result = $result || (bool)$dbo->getAffectedRows();
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 + }
365 466
366 - if (!$result) {
367 - VBOHttpDocument::getInstance()->close(500, 'No records were updated for the involved room reservation IDs');
368 - }
467 + if (strpos($search_key, '@') !== false) {
468 + // search by email address
469 + $seek_clauses[] = $dbo->qn('email') . ' LIKE ' . $dbo->q('%' . $search_key . '%');
470 + }
369 471
370 - // update history records
371 - $user = JFactory::getUser();
372 - VikBooking::getBookingHistoryInstance()->setBid($booking_one['id'])->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_one, $index_two) . " ({$user->name})");
373 - if ($booking_one['id'] != $booking_two['id']) {
374 - VikBooking::getBookingHistoryInstance()->setBid($booking_two['id'])->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_two, $index_one) . " ({$user->name})");
375 - }
472 + if (preg_match('/[0-9]+/', $search_key)) {
473 + // search by company VAT number
474 + $seek_clauses[] = $dbo->qn('vat') . ' = ' . $dbo->q($search_key);
376 475
377 - // output the JSON encoded response object
378 - VBOHttpDocument::getInstance()->json([
379 - 'swap_from' => $index_one,
380 - 'swap_to' => $index_two,
381 - ]);
382 - }
476 + // search by PIN code
477 + $seek_clauses[] = $dbo->qn('pin') . ' = ' . $dbo->q($search_key);
478 + }
383 479
384 - /**
385 - * AJAX endpoint to remove the type flag (i.e. "overbooking") from a booking ID.
386 - *
387 - * @return void
388 - */
389 - public function delete_type_flag()
390 - {
391 - if (!JSession::checkToken()) {
392 - VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
393 - }
480 + // set multiple search clauses
481 + $q->andWhere($seek_clauses, 'OR');
482 + }
394 483
395 - $dbo = JFactory::getDbo();
396 - $app = JFactory::getApplication();
484 + // order by customer nominative
485 + $q->order($dbo->qn('first_name') . ' ASC');
486 + $q->order($dbo->qn('last_name') . ' ASC');
397 487
398 - $bid = $app->input->getUInt('bid', 0);
399 - $flag = $app->input->getString('flag', '');
488 + $dbo->setQuery($q);
400 489
401 - if (!$bid) {
402 - VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
403 - }
490 + // set results found
491 + $response['results'] = $dbo->loadAssocList();
404 492
405 - $q = $dbo->getQuery(true);
493 + // default icon for customers
494 + $source_def_icon_cls = VikBookingIcons::i('user');
406 495
407 - $q->update($dbo->qn('#__vikbooking_orders'))
408 - ->set($dbo->qn('type') . ' = ' . $dbo->q(''))
409 - ->where($dbo->qn('id') . ' = ' . $bid);
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']);
410 500
411 - $dbo->setQuery($q);
412 - $dbo->execute();
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 + }
413 510
414 - if (!(bool)$dbo->getAffectedRows()) {
415 - VBOHttpDocument::getInstance()->close(500, 'Could not update the booking record');
416 - }
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 + }
417 517
418 - if (!strcasecmp($flag, 'overbooking')) {
419 - // update history records
420 - $user = JFactory::getUser();
421 - VikBooking::getBookingHistoryInstance($bid)->store('OB', JText::translate('VBO_OVERBOOKING_FLAG_REMOVED') . " ({$user->name})");
422 - }
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 + }
423 529
424 - VBOHttpDocument::getInstance()->json([$bid => 'ok']);
425 - }
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 + /**
1294 + * Regular task to create passcodes for a booking that should have had them.
1295 + *
1296 + * @since 1.18.16 (J) - 1.8.16 (WP)
1297 + */
1298 + public function generatePasscodes()
1299 + {
1300 + $app = JFactory::getApplication();
1301 +
1302 + $bookingId = $app->input->getUInt('bid', 0);
1303 +
1304 + if (!$bookingId) {
1305 + $app->enqueueMessage('Missing booking ID.', 'error');
1306 + $app->redirect('index.php?option=com_vikbooking');
1307 + $app->close();
1308 + }
1309 +
1310 + if (!JSession::checkToken() && !JSession::checkToken('get')) {
1311 + $app->enqueueMessage(JText::translate('JINVALID_TOKEN'), 'error');
1312 + $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $bookingId);
1313 + $app->close();
1314 + }
1315 +
1316 + try {
1317 + // access booking registry
1318 + $registry = VBOBookingRegistry::getInstance(['id' => $bookingId]);
1319 + } catch (Exception $e) {
1320 + $app->enqueueMessage($e->getMessage(), 'error');
1321 + $app->redirect('index.php?option=com_vikbooking');
1322 + $app->close();
1323 + }
1324 +
1325 + // do not check the history to see if previous passcode generation events occurred
1326 + // always simulate the creation of this booking
1327 + $generated = VBOFactory::getDoorAccessControl()->processBookingConfirmation($registry->getData(), $registry->getRooms());
1328 +
1329 + if ($registry->hasPreCheckedIn()) {
1330 + // simulate the pre-check-in completion of the booking
1331 + $generatedTwo = VBOFactory::getDoorAccessControl()->processPrecheckinCompleted($registry->getData(), $registry->getRooms());
1332 + }
1333 +
1334 + // evaluate result
1335 + $generated = $generated || $generatedTwo;
1336 +
1337 + // complete with a successful message
1338 + $app->enqueueMessage(JText::translate('VBO_W_DOORACCESSCONTROL_TITLE'), ($generated ? 'success' : 'warning'));
1339 + $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $bookingId);
1340 + $app->close();
1341 + }
426 1342 }