widgetName = JText::translate('VBO_W_BULKMESSAGING_TITLE'); $this->widgetDescr = JText::translate('VBO_W_BULKMESSAGING_DESCR'); $this->widgetId = basename(__FILE__, '.php'); // define widget and icon and style name $this->widgetIcon = ''; $this->widgetStyleName = 'yellow'; // load widget's settings $this->widgetSettings = $this->loadSettings(); if (!is_object($this->widgetSettings)) { $this->widgetSettings = new stdClass; } } /** * Custom method for this widget only to load the reservations. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. */ public function loadBookings() { // get today's date $today_ymd = date('Y-m-d'); $wrapper = VikRequest::getString('wrapper', '', 'request'); $type = VikRequest::getString('type', 'stayover', 'request'); $from_dt = VikRequest::getString('from_dt', $today_ymd, 'request'); $to_dt = VikRequest::getString('to_dt', '', 'request') ?: $from_dt; if (empty($from_dt)) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS')); } // get date timestamps $from_ts = VikBooking::getDateTimestamp($from_dt, 0, 0); $to_ts = VikBooking::getDateTimestamp($to_dt, 23, 59, 59); $from_ts_end = VikBooking::getDateTimestamp($from_dt, 23, 59, 59); // query the db $dbo = JFactory::getDbo(); $q = $dbo->getQuery(true) ->select($dbo->qn('o') . '.*') ->select($dbo->qn('co.idcustomer')) ->select('CONCAT_WS(" ", ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') AS ' . $dbo->qn('customer_fullname')) ->select($dbo->qn('c.country', 'customer_country')) ->select($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 ($type == 'arrival') { $q->where($dbo->qn('o.checkin') . ' >= ' . $from_ts); $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts); } elseif ($type == 'departure') { $q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts); $q->where($dbo->qn('o.checkout') . ' <= ' . $to_ts); } elseif ($type == 'bookdate') { $q->where($dbo->qn('o.ts') . ' >= ' . $from_ts); $q->where($dbo->qn('o.ts') . ' <= ' . $to_ts); } else { // stayover $q->where($dbo->qn('o.checkin') . ' < ' . $from_ts_end); $q->where($dbo->qn('o.checkout') . ' > ' . $to_ts); } $dbo->setQuery($q); $bookings = $dbo->loadAssocList(); // total checkboxes checked $tot_checked = 0; // first booking ID "checked" $first_checked_bid = 0; $widget_id = $this->widgetId; // start output buffering ob_start(); if (!$bookings) { ?>

$booking) { // get channel logo and other details $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true); $channel_logo = is_object($ch_logo_obj) ? $ch_logo_obj->getSmallLogoURL() : ''; $nights_lbl = $booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'); $rooms_lbl = !empty($booking['roomsnum']) && $booking['roomsnum'] > 1 ? ', ' . $booking['roomsnum'] . ' ' . JText::translate('VBPVIEWORDERSTHREE') : ''; // compose customer name $customer_name = !empty($booking['customer_fullname']) ? $booking['customer_fullname'] : ''; if ($booking['closure'] > 0 || !strcasecmp($booking['custdata'], JText::translate('VBDBTEXTROOMCLOSED'))) { $customer_name = ' ' . JText::translate('VBDBTEXTROOMCLOSED') . ''; } if (empty($customer_name)) { $customer_name = VikBooking::getFirstCustDataField($booking['custdata']); } // customer country flag $customer_country = ''; $customer_cflag = ''; if (!empty($booking['customer_country'])) { $customer_country = $booking['customer_country']; } elseif (!empty($booking['country'])) { $customer_country = $booking['country']; } if ($customer_country && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'countries' . DIRECTORY_SEPARATOR . $customer_country . '.png')) { $customer_cflag = ''; } // check for any previous event triggered by this widget for the current reservation $last_notified = null; $history_obj = VikBooking::getBookingHistoryInstance($booking['id']); $prev_ev_data = $history_obj->getEventsWithData('CE', function($data) use ($widget_id) { return (is_object($data) && !empty($data->widget) && $data->widget == $widget_id); }, $onlydata = false); if (is_array($prev_ev_data) && $prev_ev_data) { $last_notified = $prev_ev_data[0]['dt']; } // default checked status $booking_is_checked = ($booking['status'] == 'confirmed' && !$last_notified); if ($booking_is_checked) { $tot_checked++; if (!$first_checked_bid) { $first_checked_bid = $booking['id']; } } ?>
/>
$html_content, 'tot_bookings' => count($bookings), 'tot_checked' => $tot_checked, ]; } /** * Custom method for this widget only to send the communication message. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. */ public function sendOneMessage() { $wrapper = VikRequest::getString('wrapper', '', 'request'); $bid = VikRequest::getInt('bid', 0, 'request'); $index = VikRequest::getInt('index', 0, 'request'); $message = VikRequest::getString('message', '', 'request', VIKREQUEST_ALLOWHTML); $subject = VikRequest::getString('subject', '', 'request'); $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE')); } if (empty($message)) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PLEASE_FILL_FIELDS')); } // inject the customer information $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($booking['id']); $booking['customer'] = $customer; if ($index === 0) { // update widget's settings with last message and subject $this->widgetSettings->last_subject = $subject; $this->widgetSettings->last_message = $message; $this->updateSettings(json_encode($this->widgetSettings)); } // determine the message dispatching method for this booking $sending_method = 'eMail'; // check if support for OTA messaging is available $ota_messaging_supported = class_exists('VCMChatMessaging'); if ($ota_messaging_supported && VCMChatMessaging::getInstance($booking)->supportsOtaMessaging($mandatory = true)) { // set flag to identify an OTA messaging notification over the regular email $sending_method = 'Message'; } elseif (preg_match("/^no-email-[^@]+@[a-z0-9\.]+\.com$/i", $booking['custmail'])) { // false email address, typical of Airbnb, ignore reservation record // this statement should be entered only if VCM is outdated, or the Guest Messaging API will be used VBOHttpDocument::getInstance()->close(500, 'The booking ID ' . $booking['id'] . ' does not have a valid email address that can be notified.'); } if ($sending_method === 'eMail' && empty($booking['custmail'])) { VBOHttpDocument::getInstance()->close(500, 'The booking ID ' . $booking['id'] . ' is missing the guest email address.'); } // language translation $lang = JFactory::getLanguage(); $vbo_tn = VikBooking::getTranslator(); $vbo_tn::$force_tolang = null; $website_def_lang = $vbo_tn->getDefaultLang(); if (!empty($booking['lang'])) { if ($lang->getTag() != $booking['lang']) { if (VBOPlatformDetection::isWordPress()) { // wp $lang->load('com_vikbooking', VIKBOOKING_SITE_LANG, $booking['lang'], true); $lang->load('com_vikbooking', VIKBOOKING_ADMIN_LANG, $booking['lang'], true); } else { // J $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true); $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true); $lang->load('joomla', JPATH_SITE, $booking['lang'], true); $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true); } } if ($website_def_lang != $booking['lang']) { // force the translation to start because contents should be translated $vbo_tn::$force_tolang = $booking['lang']; } } // dispatch the message to the guest reservation if ($sending_method === 'eMail') { // regular email notification $message = $this->notifyThroughEmail($booking, $message, $subject); } else { // notification through guest messaging API $message = $this->notifyThroughMessage($booking, $message); } // update history for this booking with the information about the communication sent VikBooking::getBookingHistoryInstance($booking['id']) ->setExtraData(['widget' => $this->widgetId]) ->store('CE', $message); // return an associative array of values return [ 'result' => 1, ]; } /** * Preload the necessary CSS/JS assets. * * @return void */ public function preload() { // load assets $this->vbo_app->loadDatePicker(); if (VBOPlatformDetection::isJoomla()) { // load assets $this->vbo_app->loadVisualEditorAssets(); } else { // load lang defs $this->vbo_app->loadVisualEditorDefinitions(); } // JS lang defs JText::script('VBO_PLEASE_SELECT'); JText::script('VBPVIEWORDERSPEOPLE'); JText::script('VBO_WANT_PROCEED'); JText::script('VBO_CONT_WRAPPER'); JText::script('VBO_CONT_WRAPPER_HELP'); } /** * Main method to invoke the widget. Contents will be loaded * through AJAX requests, not via PHP when the page loads. * * @param ?VBOMultitaskData $data * * @return void */ public function render(?VBOMultitaskData $data = null) { // increase widget's instance counter static::$instance_counter++; // check whether the widget is being rendered via AJAX when adding it through the customizer $is_ajax = $this->isAjaxRendering(); // generate a unique ID for the sticky notes wrapper instance $wrapper_instance = !$is_ajax ? static::$instance_counter : rand(); $wrapper_id = 'vbo-widget-bulkmess-' . $wrapper_instance; // get permissions $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking'); if (!$vbo_auth_bookings) { // display nothing return; } // date format $dtpicker_df = $this->getDateFormat('jui'); // build the default message subject $def_subject = !empty($this->widgetSettings->last_subject) ? $this->widgetSettings->last_subject : JText::sprintf('VBOMAILSUBJECT', VikBooking::getFrontTitle()); ?>

widgetIcon; ?> widgetName; ?>

renderEditor(); ?>

0 / 0
vbo_app->loadVisualEditorAssets(); // build a list of all special tags for the visual editor $special_tags_base = [ '{customer_name}', '{customer_pin}', '{booking_id}', '{checkin_date}', '{checkout_date}', '{num_nights}', '{rooms_booked}', '{tot_adults}', '{tot_children}', '{tot_guests}', '{total}', '{total_paid}', '{remaining_balance}', '{booking_link}', ]; // load all conditional text special tags $condtext_tags = array_keys(VikBooking::getConditionalRulesInstance()->getSpecialTags()); /** * Load Door Access Control special tags from the configured integration providers. * * @since 1.18.4 (J) - 1.8.4 (WP) */ $dac_tags = VBOFactory::getDoorAccessControl()->getInstalledSpecialTags(); // join special tags with conditional texts and DAC to construct a list of editor buttons, // displayed within the toolbar of Quill editor $editor_btns = array_merge($special_tags_base, $condtext_tags, $dac_tags); // build the default message value $def_message = !empty($this->widgetSettings->last_message) ? $this->widgetSettings->last_message : $this->getDefaultMessage(); return $this->vbo_app->renderVisualEditor( 'vbo_widget_bulkmess_mess', $def_message, [ 'class' => 'vbo-widget-bulkmess-messagecont', 'style' => 'width: 96%; height: 150px;', ], [ 'modes' => [ 'visual', 'text', ], ], $editor_btns ); } /** * Returns the default message. * * @return string */ private function getDefaultMessage() { $message = ''; $logo_html = ''; $sitelogo = VBOFactory::getConfig()->get('sitelogo'); $company_name = VikBooking::getFrontTitle(); if ($sitelogo && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) { $logo_html = '

' . '' . htmlspecialchars($company_name) . '

' . "\n"; } $message = << $company_name

Dear {customer_name},


This is a message for your stay from {checkin_date} to {checkout_date}.



Thank you.

$company_name



HTML ; return $message; } /** * Sends a message to the guest through a regular email. * * @param array $booking the reservation record to notify. * @param string $message the message to send. * @param string $subject the email subject. * * @return string the message built and sent. */ private function notifyThroughEmail(array $booking, $message, $subject) { // fetch booked room details $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']); // translate contents $vbo_tn = VikBooking::getTranslator(); $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', ['id' => 'idroom', 'name' => 'room_name']); $message = $this->parseCustomerEmailTemplate($message, $booking, $booking_rooms, $vbo_tn); $is_html = (strpos($message, '<') !== false || strpos($message, ')+/", $message)) { // when no br tags found, apply nl2br $message = nl2br($message); } // get sender email address $admin_sendermail = VikBooking::getSenderMail(); if (!$this->vbo_app->sendMail($admin_sendermail, $admin_sendermail, $booking['custmail'], $admin_sendermail, $subject, $message, $is_html)) { VBOHttpDocument::getInstance()->close(500, 'Sending the email message to the booking ID ' . $booking['id'] . ' failed.'); } return $message; } /** * Sends a message to the guest through a regular email. * * @param array $booking the reservation record to notify. * @param string $message the message to send. * * @return string the message built and sent. */ private function notifyThroughMessage(array $booking, $message) { // fetch booked room details $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']); // translate contents $vbo_tn = VikBooking::getTranslator(); $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', ['id' => 'idroom', 'name' => 'room_name']); $message = $this->parseCustomerEmailTemplate($message, $booking, $booking_rooms, $vbo_tn); if (empty($message)) { VBOHttpDocument::getInstance()->close(500, 'Message for the booking ID ' . $booking['id'] . ' is empty.'); } $messaging = VCMChatMessaging::getInstance($booking); $result = $messaging->setMessage($message) ->sendGuestMessage(); if (!$result && $error = $messaging->getError()) { // terminate with the error description VBOHttpDocument::getInstance()->close(500, "Message could not be sent to guest - Booking ID {$booking['id']} ({$booking['customer_name']}): {$error}"); } return $message; } /** * Composes the actual email message by parsing special tokens. * * @param string $message the message content for the email. * @param array $booking booking array record. * @param array $booking_rooms list of rooms booked. * @param array $vbo_tn translator object. * * @return string the email message content ready to be sent. */ private function parseCustomerEmailTemplate($message, $booking, $booking_rooms, $vbo_tn = null) { $tpl = $message; /** * Parse all conditional text rules. */ VikBooking::getConditionalRulesInstance() ->set(array('booking', 'rooms'), array($booking, $booking_rooms)) ->parseTokens($tpl); /** * Parse all Door Access Control tags. * * @since 1.18.4 (J) - 1.8.4 (WP) */ VBOFactory::getDoorAccessControl() ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $tpl); // normalize customer details if (empty($booking['customer_name']) && !empty($booking['customer'])) { $booking['customer_name'] = trim($booking['first_name'] . ' ' . $booking['last_name']); } $vbo_df = VikBooking::getDateFormat(); $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d'); $tpl = str_replace('{customer_name}', ($booking['customer_name'] ?: ''), $tpl); $tpl = str_replace('{booking_id}', $booking['id'], $tpl); $tpl = str_replace('{checkin_date}', date($df, $booking['checkin']), $tpl); $tpl = str_replace('{checkout_date}', date($df, $booking['checkout']), $tpl); $tpl = str_replace('{num_nights}', $booking['days'], $tpl); $rooms_booked = []; $tot_adults = 0; $tot_children = 0; $tot_guests = 0; foreach ($booking_rooms as $broom) { if (array_key_exists($broom['room_name'], $rooms_booked)) { $rooms_booked[$broom['room_name']] += 1; } else { $rooms_booked[$broom['room_name']] = 1; } $tot_adults += (int)$broom['adults']; $tot_children += (int)$broom['children']; $tot_guests += ((int)$broom['adults'] + (int)$broom['children']); } $tpl = str_replace('{tot_adults}', $tot_adults, $tpl); $tpl = str_replace('{tot_children}', $tot_children, $tpl); $tpl = str_replace('{tot_guests}', $tot_guests, $tpl); $rooms_booked_quant = []; foreach ($rooms_booked as $rname => $quant) { $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname; } $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl); $tpl = str_replace('{total}', VikBooking::numberFormat($booking['total']), $tpl); $tpl = str_replace('{total_paid}', VikBooking::numberFormat($booking['totpaid']), $tpl); $remaining_bal = $booking['total'] - $booking['totpaid']; $tpl = str_replace('{remaining_balance}', VikBooking::numberFormat($remaining_bal), $tpl); $tpl = str_replace('{customer_pin}', (($booking['customer_pin'] ?? '') ?: ($booking['customer']['pin'] ?? '')), $tpl); $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid']; $bestitemid = VikBooking::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null)); $lang_suffix = $bestitemid && !empty($booking['lang']) ? '&lang=' . $booking['lang'] : ''; $book_link = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $use_sid . "&ts=" . $booking['ts'] . $lang_suffix, false, (!empty($bestitemid) ? $bestitemid : null)); $tpl = str_replace('{booking_link}', $book_link, $tpl); /** * Rooms Distinctive Features parsing */ preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $tpl, $matches); if (isset($matches[1]) && $matches[1]) { foreach ($matches[1] as $reqf) { $rooms_features = []; foreach ($booking_rooms as $broom) { $distinctive_features = []; $rparams = json_decode($broom['params'], true); if (array_key_exists('features', $rparams) && count($rparams['features']) > 0 && array_key_exists('roomindex', $broom) && !empty($broom['roomindex']) && array_key_exists($broom['roomindex'], $rparams['features'])) { $distinctive_features = $rparams['features'][$broom['roomindex']]; } if (!count($distinctive_features)) { continue; } $feature_found = false; foreach ($distinctive_features as $dfk => $dfv) { if (stripos($dfk, $reqf) !== false) { $feature_found = $dfk; if (strlen(trim($dfk)) == strlen(trim($reqf))) { break; } } } if ($feature_found !== false && strlen((string)$distinctive_features[$feature_found])) { $rooms_features[] = $distinctive_features[$feature_found]; } } if ($rooms_features) { $rpval = implode(', ', $rooms_features); } else { $rpval = ''; } $tpl = str_replace("{roomfeature ".$reqf."}", $rpval, $tpl); } } return $tpl; } }