close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $service_name = VikRequest::getString('service_name', '', 'request'); $max_results = VikRequest::getInt('max_results', 10, 'request'); $sql_term = $dbo->quote("%{$service_name}%"); $sql_clause = !empty($service_name) ? 'LIKE ' . $sql_term : 'IS NOT NULL'; $q = "SELECT `or`.`idorder`, `or`.`idroom`, `or`.`adults`, `or`.`children`, `or`.`extracosts`, `o`.`days` AS `nights`, `o`.`ts`, `r`.`name` AS `room_name` FROM `#__vikbooking_ordersrooms` AS `or` LEFT JOIN `#__vikbooking_orders` AS `o` ON `or`.`idorder`=`o`.`id` LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` WHERE `or`.`extracosts` {$sql_clause} ORDER BY `or`.`idorder` DESC"; $dbo->setQuery($q, 0, $max_results); $dbo->execute(); if (!$dbo->getNumRows()) { // no results VBOHttpDocument::getInstance()->json([]); } $results = $dbo->loadAssocList(); $matching_services = []; foreach ($results as $k => $result) { $extra_services = json_decode($result['extracosts'], true); if (empty($extra_services)) { continue; } foreach ($extra_services as $extra_service) { if (empty($service_name) || stristr($extra_service['name'], $service_name) !== false || stristr($service_name, $extra_service['name']) !== false) { // matching service found $matching_service = $result; unset($matching_service['extracosts']); $matching_service['service'] = $extra_service; $matching_service['service']['format_cost'] = VikBooking::getCurrencySymb() . ' ' . VikBooking::numberFormat($extra_service['cost']); $matching_service['format_dt'] = VikBooking::formatDateTs($result['ts']); // push result $matching_services[] = $matching_service; if (count($matching_services) >= $max_results) { break 2; } } } } // output the JSON encoded list of matching results found VBOHttpDocument::getInstance()->json($matching_services); } /** * AJAX endpoint to count the number of uses for various coupon codes. * * @return void */ public function coupons_use_count() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $coupon_codes = VikRequest::getVar('coupon_codes', array()); $use_counts = []; foreach ($coupon_codes as $coupon_code) { $q = "SELECT COUNT(*) FROM `#__vikbooking_orders` WHERE `coupon` LIKE " . $dbo->quote("%;{$coupon_code}"); $dbo->setQuery($q); $dbo->execute(); $use_counts[] = [ 'code' => $coupon_code, 'count' => (int)$dbo->loadResult(), ]; } // output the JSON encoded list of coupon use counts VBOHttpDocument::getInstance()->json($use_counts); } /** * AJAX endpoint to dynamically search for customers. Compatible with select2. * * @return void */ public function customers_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $term = VikRequest::getString('term', '', 'request'); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($term)) { // output the JSON object with no results VBOHttpDocument::getInstance()->json($response); } $sql_term = $dbo->quote("%{$term}%"); $q = "SELECT `c`.`id`, `c`.`first_name`, `c`.`last_name`, `c`.`country`, (SELECT COUNT(*) FROM `#__vikbooking_customers_orders` AS `co` WHERE `co`.`idcustomer`=`c`.`id`) AS `tot_bookings` FROM `#__vikbooking_customers` AS `c` WHERE CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) LIKE {$sql_term} OR `email` LIKE {$sql_term} ORDER BY `c`.`first_name` ASC, `c`.`last_name` ASC;"; $dbo->setQuery($q); $customers = $dbo->loadAssocList(); if ($customers) { foreach ($customers as $k => $customer) { $customers[$k]['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']) . ' (' . $customer['tot_bookings'] . ')'; } // push results found $response['results'] = $customers; } // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to dynamically search for rooms. Compatible with select2. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function rooms_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $term = $app->input->getString('term', ''); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($term)) { // output the JSON object with no results VBOHttpDocument::getInstance($app)->json($response); } $dbo->setQuery( $dbo->getQuery(true) ->select([ $dbo->qn('id'), $dbo->qn('name', 'text'), $dbo->qn('img'), ]) ->from($dbo->qn('#__vikbooking_rooms')) ->where($dbo->qn('name') . ' LIKE ' . $dbo->q("%{$term}%")) ->order($dbo->qn('avail') . ' DESC') ->order($dbo->qn('name') . ' ASC') ); // set results found $response['results'] = $dbo->loadAssocList(); // load and map mini thumbnails $mini_thumbnails = VBORoomHelper::getInstance()->loadMiniThumbnails($response['results']); $response['results'] = array_map(function($room) use ($mini_thumbnails) { if ($mini_thumbnails[$room['id']] ?? '') { // set mini thumbnail URL $room['img'] = $mini_thumbnails[$room['id']]; } else { unset($room['img']); } return $room; }, $response['results']); // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to dynamically search for bookings. Compatible with select2. * * @return void * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function bookings_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $booking_key = trim($app->input->getString('term', '')); $booking_status = array_values(array_filter((array) $app->input->get('status', [], 'array'))); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($booking_key)) { // output the JSON object with no results VBOHttpDocument::getInstance($app)->json($response); } // attempt to detect a booking ID $booking_id = 0; if (preg_match("/^[0-9]+$/", $booking_key)) { // only numbers should be a booking ID $booking_id = $booking_key; } elseif (preg_match('/^(?=.*?\d)(?=.*?[A-Z])[A-Z\d]+$/', $booking_key)) { /** * Matched both numbers and upper-case letters, so it has to be an OTA booking ID, not a customer name. * Regex breakdown: * beginning of string * lookahead for at least one digit * lookahead for at least one upper-case letter * match one or more upper-case letters or digits * end of string */ $booking_id = $booking_key; } // start the query $q = $dbo->getQuery(true) ->select([ $dbo->qn('o.id'), $dbo->qn('o.custdata'), $dbo->qn('o.days'), $dbo->qn('o.status'), $dbo->qn('o.checkin'), $dbo->qn('o.checkout'), $dbo->qn('o.idorderota'), $dbo->qn('o.channel'), $dbo->qn('c.first_name'), $dbo->qn('c.last_name'), $dbo->qn('c.pic'), ]) ->from($dbo->qn('#__vikbooking_orders', 'o')) ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id')) ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer')) ->where($dbo->qn('o.closure') . ' = 0'); if ($booking_status) { // filter by booking status if (count($booking_status) === 1) { // single booking status $q->where($dbo->qn('o.status') . ' = ' . $dbo->q($booking_status[0])); } else { // multiple booking statuses $q->where($dbo->qn('o.status') . ' IN (' . implode(', ', array_map([$dbo, 'q'], $booking_status)) . ')'); } } if (!empty($booking_id)) { // search by booking ID or OTA booking ID only if (preg_match("/^[0-9]+$/", (string) $booking_id)) { // only numbers could be both website and OTA $q->andWhere([ $dbo->qn('o.id') . ' = ' . (int) $booking_id, $dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_id), ], $glue = 'OR'); } else { // alphanumeric IDs can only belong to an OTA reservation $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_id)); } } else { // search by different values if (stripos($booking_key, 'id:') === 0) { // search by ID or OTA ID $seek_parts = explode('id:', $booking_key); $seek_value = trim($seek_parts[1]); $q->andWhere([ $dbo->qn('o.id') . ' = ' . $dbo->q($seek_value), $dbo->qn('o.idorderota') . ' = ' . $dbo->q($seek_value), ], $glue = 'OR'); } elseif (stripos($booking_key, 'otaid:') === 0) { // search by OTA Booking ID $seek_parts = explode('otaid:', $booking_key); $seek_value = trim($seek_parts[1]); $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($seek_value)); } elseif (stripos($booking_key, 'coupon:') === 0) { // search by coupon code $seek_parts = explode('coupon:', $booking_key); $seek_value = trim($seek_parts[1]); $q->where($dbo->qn('o.coupon') . ' LIKE ' . $dbo->q("%{$seek_value}%")); } elseif (stripos($booking_key, 'name:') === 0) { // search by customer nominative $seek_parts = explode('name:', $booking_key); $seek_value = trim($seek_parts[1]); $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$seek_value}%")); } elseif (strpos($booking_key, '@') !== false) { // search by customer email $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($booking_key)); } elseif (strpos($booking_key, '+') === 0) { // search by customer phone $q->where($dbo->qn('o.phone') . ' = ' . $dbo->q($booking_key)); } else { // seek for various values if (preg_match("/^[a-z\s]+$/i", (string) $booking_key)) { // when only letters (or spaces) look only for the customer name $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$booking_key}%")); } else { // look for both customer name and booking ID $q->andWhere([ 'CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$booking_key}%"), $dbo->qn('o.id') . ' = ' . $dbo->q($booking_key), $dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_key), ], $glue = 'OR'); } } } // order by most recent bookings $q->order($dbo->qn('id') . ' DESC'); $dbo->setQuery($q); // set results found $response['results'] = $dbo->loadAssocList(); // default icon for website reservations $source_def_icon_cls = VikBookingIcons::i('hotel'); // map the results with the required properties $response['results'] = array_map(function($booking) use ($source_def_icon_cls) { // build "text" property $text = $booking['id']; if (!empty($booking['first_name'])) { // use customer nominative when available $text = trim($booking['first_name'] . ' ' . $booking['last_name']); } elseif (!empty($booking['custdata'])) { $text = VikBooking::getFirstCustDataField($booking['custdata']); } $booking['text'] = $text; // build "img" property if (!empty($booking['pic'])) { // use guest profile picture $booking['img'] = strpos($booking['pic'], 'http') === 0 ? $booking['pic'] : VBO_SITE_URI . 'resources/uploads/' . $booking['pic']; } elseif (!empty($booking['channel'])) { // use channel logo $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true); $booking['img'] = is_object($ch_logo_obj) ? $ch_logo_obj->getTinyLogoURL() : ''; } if (empty($booking['img'])) { // always set an empty string $booking['img'] = ''; // set the default icon class $booking['icon_class'] = $source_def_icon_cls; } // return the mapped booking element return $booking; }, $response['results']); // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to dynamically search for customers and build elements. Compatible with select2. * * @return void * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function customer_elements_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $search_key = trim($app->input->getString('term', '')); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($search_key)) { // output the JSON object with no results VBOHttpDocument::getInstance($app)->json($response); } // start the query $q = $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_customers')) ->where(1); if (preg_match('/^[a-z0-9\.\-\_]+\@[a-z0-9\.\-\_]+\.[a-z0-9\.\-\_]+$/i', $search_key)) { // full email address detected $q->where($dbo->qn('email') . ' = ' . $dbo->q($search_key)); } else { // search by different values $seek_clauses = []; // search by nominative $seek_clauses[] = 'CONCAT_WS(" ", ' . $dbo->qn('first_name') . ', ' . $dbo->qn('last_name') . ') LIKE ' . $dbo->q('%' . $search_key . '%'); // search by company name $seek_clauses[] = $dbo->qn('company') . ' LIKE ' . $dbo->q('%' . $search_key . '%'); if (preg_match('/^\+?[0-9\s]+$/i', $search_key)) { // search by phone number $seek_clauses[] = $dbo->qn('phone') . ' = ' . $dbo->q($search_key); } if (strpos($search_key, '@') !== false) { // search by email address $seek_clauses[] = $dbo->qn('email') . ' LIKE ' . $dbo->q('%' . $search_key . '%'); } if (preg_match('/[0-9]+/', $search_key)) { // search by company VAT number $seek_clauses[] = $dbo->qn('vat') . ' = ' . $dbo->q($search_key); // search by PIN code $seek_clauses[] = $dbo->qn('pin') . ' = ' . $dbo->q($search_key); } // set multiple search clauses $q->andWhere($seek_clauses, 'OR'); } // order by customer nominative $q->order($dbo->qn('first_name') . ' ASC'); $q->order($dbo->qn('last_name') . ' ASC'); $dbo->setQuery($q); // set results found $response['results'] = $dbo->loadAssocList(); // default icon for customers $source_def_icon_cls = VikBookingIcons::i('user'); // map the results with the required properties $response['results'] = array_map(function($customer) use ($source_def_icon_cls) { // build "text" property $customer['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']); // build "img" property if (!empty($customer['pic'])) { // use customer profile picture $customer['img'] = strpos($customer['pic'], 'http') === 0 ? $customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $customer['pic']; } elseif (!empty($customer['country']) && is_file(implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'countries', $customer['country'] . '.png']))) { // use customer country flag $customer['img'] = VBO_ADMIN_URI . 'resources/countries/' . $customer['country'] . '.png'; $customer['img_title'] = $customer['country']; } if (empty($customer['img'])) { // always set an empty string $customer['img'] = ''; // set the default icon class $customer['icon_class'] = $source_def_icon_cls; } // handle custom fields if (!empty($customer['cfields'])) { $custom_fields = (array) json_decode($customer['cfields'], true); if ($custom_fields) { $customer['cfields'] = $custom_fields; } } if (!is_array($customer['cfields']) || !$customer['cfields']) { // ensure this is a null value $customer['cfields'] = null; } // return the mapped customer element return $customer; }, $response['results']); // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * Regular task to update the status of a cancelled booking to pending (stand-by). * * @return void */ public function set_to_pending() { $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); if (!JSession::checkToken() && !JSession::checkToken('get')) { $app->enqueueMessage(JText::translate('JINVALID_TOKEN'), 'error'); $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $bid); $app->close(); } $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid; $dbo->setQuery($q, 0, 1); $dbo->execute(); if (!$dbo->getNumRows()) { $app->enqueueMessage('Booking not found', 'error'); $app->redirect('index.php?option=com_vikbooking&task=orders'); $app->close(); } $booking = $dbo->loadAssoc(); if ($booking['status'] != 'cancelled') { $app->enqueueMessage('Booking status must be -Cancelled-', 'error'); $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']); $app->close(); } $q = "UPDATE `#__vikbooking_orders` SET `status`='standby' WHERE `id`=" . $booking['id']; $dbo->setQuery($q); $dbo->execute(); $app->enqueueMessage(JText::translate('JLIB_APPLICATION_SAVE_SUCCESS')); $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']); $app->close(); } /** * AJAX endpoint to assign a room index to a room booking record. * * @return void */ public function set_room_booking_subunit() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $rid = $app->input->getInt('rid', 0); $orkey = $app->input->getInt('orkey', 0); $rindex = $app->input->getInt('rindex', 0); if (empty($bid) || empty($rid)) { VBOHttpDocument::getInstance()->close(500, 'Missing request values'); } $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid; $dbo->setQuery($q, 0, 1); $booking = $dbo->loadAssoc(); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Booking not found'); } $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']); if (!$booking_rooms) { VBOHttpDocument::getInstance()->close(500, 'No rooms booking found'); } if (!isset($booking_rooms[$orkey]) || $booking_rooms[$orkey]['idroom'] != $rid) { VBOHttpDocument::getInstance()->close(500, 'Invalid room booking record'); } // update room record $room_record = new stdClass; $room_record->id = $booking_rooms[$orkey]['id']; $room_record->roomindex = $rindex; $dbo->updateObject('#__vikbooking_ordersrooms', $room_record, 'id'); // build list of affected nights $nights_list_ymd = []; $from_checkin_info = getdate($booking['checkin']); for ($n = 0; $n < $booking['days']; $n++) { // push affected night $nights_list_ymd[] = date('Y-m-d', mktime(0, 0, 0, $from_checkin_info['mon'], ($from_checkin_info['mday'] + $n), $from_checkin_info['year'])); } // build return values $response = [ 'bid' => $booking['id'], 'rid' => $booking_rooms[$orkey]['idroom'], 'rindex' => $rindex, 'from' => date('Y-m-d', $booking['checkin']), 'to' => date('Y-m-d', $booking['checkout']), 'nights' => $nights_list_ymd, ]; // output the JSON encoded object VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to swap one sub-unit index with another for the same room ID and dates. * * @return void * * @since 1.16.2 (J) - 1.6.2 (WP) */ public function swap_room_subunits() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid_one = $app->input->getInt('bid_one', 0); $bid_two = $app->input->getInt('bid_two', 0); $rid = $app->input->getInt('rid', 0); $index_one = $app->input->getInt('index_one', 0); $index_two = $app->input->getInt('index_two', 0); $checkin = $app->input->getString('checkin', ''); if (!$bid_one || !$bid_two || !$rid || !$index_one || !$index_two || $index_one < 0 || $index_two < 0) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values'); } // collect the booking information $booking_one = VikBooking::getBookingInfoFromID($bid_one); $booking_two = VikBooking::getBookingInfoFromID($bid_two); if (!$booking_one || !$booking_two) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservations'); } // get room reservation records $rooms_one = VikBooking::loadOrdersRoomsData($bid_one); $rooms_two = VikBooking::loadOrdersRoomsData($bid_two); if (!$rooms_one || !$rooms_two) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved room reservation records'); } // clone room reservation records $current_rooms_one = $rooms_one; $current_rooms_two = $rooms_two; // find the record IDs involved and room name $update_id_one = null; $update_id_two = null; $room_name = ''; foreach ($rooms_one as $k => $room_one) { if ($room_one['idroom'] == $rid && $room_one['roomindex'] == $index_one) { $update_id_one = $room_one['id']; $room_name = $room_one['room_name']; $current_rooms_one[$k]['roomindex'] = $index_two; break; } } foreach ($rooms_two as $k => $room_two) { if ($room_two['idroom'] == $rid && $room_two['roomindex'] == $index_two) { $update_id_two = $room_two['id']; $room_name = $room_two['room_name']; $current_rooms_two[$k]['roomindex'] = $index_one; break; } } if (!$update_id_one || !$update_id_two) { VBOHttpDocument::getInstance()->close(500, 'Could not find the involved room reservation record IDs'); } // swap first room record $q = $dbo->getQuery(true); $q->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('roomindex') . ' = ' . $index_two) ->where($dbo->qn('id') . ' = ' . (int)$update_id_one); $dbo->setQuery($q); $dbo->execute(); $result = (bool)$dbo->getAffectedRows(); // swap second room record $q = $dbo->getQuery(true); $q->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('roomindex') . ' = ' . $index_one) ->where($dbo->qn('id') . ' = ' . (int)$update_id_two); $dbo->setQuery($q); $dbo->execute(); $result = $result || (bool)$dbo->getAffectedRows(); if (!$result) { VBOHttpDocument::getInstance()->close(500, 'No records were updated for the involved room reservation IDs'); } // update history record(s) $user = JFactory::getUser(); VikBooking::getBookingHistoryInstance($booking_one['id']) ->setPrevBooking(array_merge($booking_one, ['rooms_info' => $rooms_one])) ->setBookingData($booking_one, $current_rooms_one) ->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_one, $index_two) . " ({$user->name})"); if ($booking_one['id'] != $booking_two['id']) { // update history for the second booking involved VikBooking::getBookingHistoryInstance($booking_two['id']) ->setPrevBooking(array_merge($booking_two, ['rooms_info' => $rooms_two])) ->setBookingData($booking_two, $current_rooms_two) ->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_two, $index_one) . " ({$user->name})"); } // output the JSON encoded response object VBOHttpDocument::getInstance()->json([ 'swap_from' => $index_one, 'swap_to' => $index_two, ]); } /** * AJAX endpoint to remove the type flag (i.e. "overbooking") from a booking ID. * * @return void */ public function delete_type_flag() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getUInt('bid', 0); $flag = $app->input->getString('flag', ''); if (!$bid) { VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } $q = $dbo->getQuery(true); $q->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('type') . ' = ' . $dbo->q('')) ->where($dbo->qn('id') . ' = ' . $bid); $dbo->setQuery($q); $dbo->execute(); if (!(bool)$dbo->getAffectedRows()) { VBOHttpDocument::getInstance()->close(500, 'Could not update the booking record'); } if (!strcasecmp($flag, 'overbooking')) { // update history records $user = JFactory::getUser(); VikBooking::getBookingHistoryInstance($bid)->store('OB', JText::translate('VBO_OVERBOOKING_FLAG_REMOVED') . " ({$user->name})"); } VBOHttpDocument::getInstance()->json([$bid => 'ok']); } /** * AJAX endpoint to set the AI options related to automatic guest review for a booking. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function set_ai_auto_guest_review_opt() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $opt = $app->input->get('opt', [], 'array'); if (!$bid || !$opt) { VBOHttpDocument::getInstance()->close(500, 'Missing request values'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation'); } // get AI options for this booking $booking_ai_opts = (array) VBOFactory::getConfig()->getArray('ai_auto_guest_review_opt_' . $booking['id'], []); // update the requested options foreach ($opt as $param => $val) { if (is_bool($val) || is_numeric($val)) { $val = (int) $val; } // set new option value $booking_ai_opts[$param] = $val; } // update AI options for this booking VBOFactory::getConfig()->set('ai_auto_guest_review_opt_' . $booking['id'], $booking_ai_opts); // return the new preferences VBOHttpDocument::getInstance()->json($booking_ai_opts); } /** * AJAX endpoint to register a new taking (payment). * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function add_taking() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $amount = $app->input->getFloat('amount', 0); $payid = $app->input->getInt('payid', 0); $descr = $app->input->getString('descr', ''); if (!$bid || !$amount || $amount < 0) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.'); } $new_tot_paid = $booking['totpaid'] + $amount; // update booking record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // update booking history $extra_data = new stdClass; $extra_data->register_new = 1; $extra_data->amount_paid = $amount; $extra_data->payment_method = $descr; if (!empty($payid)) { $pay_info = VikBooking::getPayment($payid); if ($pay_info) { $extra_data->payment_method = $pay_info['name']; } } VikBooking::getBookingHistoryInstance($booking['id']) ->setExtraData($extra_data) ->store( 'PU', JText::sprintf('VBOPREVAMOUNTPAID', VikBooking::numberFormat($booking['totpaid']) . (!empty($extra_data->payment_method) ? ' (' . $extra_data->payment_method . ')' : '')) ); // process completed VBOHttpDocument::getInstance()->json([ 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false), ]); } /** * AJAX endpoint to update a taking (payment) and related history record. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function update_taking() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $hid = $app->input->getInt('hid', 0); $amount = $app->input->getFloat('amount', 0); $descr = $app->input->getString('descr', ''); $htype = $app->input->getString('htype', 'PU'); if (!$bid || !$hid || !$amount || $amount < 0) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.'); } // access the current history record $q = $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('id') . ' = ' . (int) $hid) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ->where($dbo->qn('type') . ' = ' . $dbo->q($htype)); $dbo->setQuery($q, 0, 1); $history = $dbo->loadAssoc(); if (!$history) { VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.'); } // get previous amount paid $history_data = (object) json_decode(($history['data'] ?: '{}')); $prev_amount_paid = $history_data->amount_paid ?? 0; // calculate new amount paid if ($prev_amount_paid > $amount) { $new_tot_paid = $booking['totpaid'] - ($prev_amount_paid - $amount); } else { $new_tot_paid = $booking['totpaid'] + ($amount - $prev_amount_paid); } // update booking record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // get currently logged user $user = JFactory::getUser(); $uname = $user->name; // update history extra data $history_data->register_new = 1; $history_data->updated = JFactory::getDate()->toSql(); $history_data->updated_by = $uname; $history_data->amount_paid = $amount; $history_data->payment_method = $descr; // set new record description $new_descr = trim($history['descr'] . "\n* " . JText::sprintf('VBO_MODIFIED_ON_SMT', JFactory::getDate()->toSql(true) . ' (' . $uname . ')')); // update history record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orderhistory')) ->set($dbo->qn('descr') . ' = ' . $dbo->q($new_descr)) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->set($dbo->qn('data') . ' = ' . $dbo->q(json_encode($history_data))) ->where($dbo->qn('id') . ' = ' . (int) $history['id']) ); $dbo->execute(); // process completed VBOHttpDocument::getInstance()->json([ 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false), ]); } /** * AJAX endpoint to delete a booking history event. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function delete_history_record() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $hid = $app->input->getInt('hid', 0); $htype = $app->input->getString('htype', 'PU'); if (!$bid || !$hid) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.'); } // access the current history record $q = $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('id') . ' = ' . (int) $hid) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ->where($dbo->qn('type') . ' = ' . $dbo->q($htype)); $dbo->setQuery($q, 0, 1); $history = $dbo->loadAssoc(); if (!$history) { VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.'); } // get previous amount paid $history_data = (object) json_decode(($history['data'] ?: '{}')); $prev_amount_paid = $history_data->amount_paid ?? 0; // calculate new amount paid $new_tot_paid = $booking['totpaid']; if ($prev_amount_paid) { $new_tot_paid = $booking['totpaid'] - $prev_amount_paid; } // update booking record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // delete history record $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('id') . ' = ' . (int) $hid) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ->where($dbo->qn('type') . ' = ' . $dbo->q($htype)) ); $dbo->execute(); $aff_rows = $dbo->getAffectedRows(); // process completed VBOHttpDocument::getInstance()->json([ 'rows' => $aff_rows, 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false), ]); } /** * AJAX endpoint to check whether a whole booking can be modified with new stay dates. * * @return void * * @since 1.18.2 (J) - 1.8.2 (WP) */ public function is_booking_modifiable() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $checkin = $app->input->getString('checkin'); $checkout = $app->input->getString('checkout'); if (!$bid || empty($checkin) || empty($checkout)) { VBOHttpDocument::getInstance()->close(400, 'Missing booking information.'); } try { // check whether the booking can be modified $modifiable = (new VBOModelReservation)->bookingModifiable($bid, $checkin, $checkout); } catch (Exception $e) { // propagate the error VBOHttpDocument::getInstance()->close($e->getCode() ?: 400, $e->getMessage() ?: 'Validation failure.'); } // validation process completed VBOHttpDocument::getInstance()->json([ 'modifiable' => $modifiable, 'bid' => $bid, 'checkin' => $checkin, 'checkout' => $checkout, ]); } /** * AJAX endpoint to resolve room (sub-unit) booking record assignment. * * @return void * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function resolve_room_assignment() { $app = JFactory::getApplication(); if (!JSession::checkToken()) { VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN')); } // gather request values $booking_id = $app->input->getUInt('bid', 0); $id_room = $app->input->getUInt('id_room', 0); $room_booking_id = $app->input->getUInt('room_booking_id', 0); $roomres_index = $app->input->getInt('booking_room_index', -1); $days_bound = $app->input->getUInt('days_bound', 0); $max_exec_time = $app->input->getUInt('max_exec_time', 0); $count_all = $app->input->getBool('count_all', false); $skip_booking_ids = (array) $app->input->getInt('skip_booking_ids', []); $skip_moveset_signatures = (array) $app->input->getString('skip_moveset_signatures', []); if (!$booking_id) { VBOHttpDocument::getInstance($app)->close(400, 'Missing booking information.'); } if ($room_booking_id) { // an exact booking room record ID was provided $booking_rooms = VikBooking::loadOrdersRoomsData($booking_id); foreach ($booking_rooms as $index => $booking_room) { if ($booking_room['id'] == $room_booking_id) { // match found, set reliable properties $id_room = $booking_room['idroom']; $roomres_index = $index; break; } } } // build room re-assignment options $options = [ 'id' => $booking_id, 'id_room' => $id_room, 'booking_room_index' => $roomres_index, 'days_bound' => $days_bound ?: null, 'max_exec_time' => $max_exec_time, 'count_all' => $count_all, 'skip_booking_ids' => $skip_booking_ids, 'skip_moveset_signatures' => $skip_moveset_signatures, ]; try { // let the room re-assignment operations start $moveset = VBOBookingRelocator::getInstance($options) ->findRelocation(); } catch (Throwable $e) { // propagate the error VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage()); } // build moveset layout data $layout_data = [ 'booking_id' => $booking_id, 'moveset' => $moveset, 'options' => $options, ]; // render moveset layout HTML $movesetHtml = JLayoutHelper::render('overview.relocationmoveset', $layout_data); // validation process completed VBOHttpDocument::getInstance($app)->json([ 'html' => $movesetHtml, 'movesetSignature' => $moveset->getSignature(), 'solutionsCount' => $moveset->getSolutionsCount(), ]); } /** * AJAX endpoint to apply a moveset to resolve room assignments. * * @return void * * @since 1.18.7 (J) - 1.8.7 (WP) * @since 1.18.11 (J) - 1.8.11 (WP) operation is performed through a VBOBookingRelocator object. */ public function apply_room_assignment() { $app = JFactory::getApplication(); $dbo = JFactory::getDbo(); if (!JSession::checkToken()) { VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN')); } // gather request values $moveset = $app->input->getString('moveset'); $undo = $app->input->getBool('undo', false); if (!$moveset) { VBOHttpDocument::getInstance($app)->close(400, 'Missing reassignment moveset.'); } // identify booking ID $bookingId = null; // scan operation steps $operationSteps = explode('-', $moveset); foreach ($operationSteps as $operationStep) { // obtain move details $moveData = explode('.', $operationStep); if (count($moveData) < 4) { VBOHttpDocument::getInstance($app)->close(400, 'Invalid moveset operation.'); } // keep setting booking ID until last moveset operation $bookingId = (int) ($moveData[0] ?? 0); } if (!$bookingId) { VBOHttpDocument::getInstance($app)->close(400, 'No valid moveset operations.'); } try { // access booking relocator object $relocator = VBOBookingRelocator::getInstance([ 'id' => $bookingId, ]); // execute given relocation moveset $relocator->execMovesetSignature($moveset, $undo); } catch (Exception $e) { // raise error VBOHttpDocument::getInstance($app)->close($e->getCode(), $e->getMessage()); } // process completed VBOHttpDocument::getInstance($app)->json([ 'success' => true, ]); } }