PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.4
VikBooking Hotel Booking Engine & PMS v1.6.4
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
vikbooking / site / helpers / history.php

history.php in VikBooking Hotel Booking Engine & PMS 1.6.4, at site/helpers/history.php

759 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Handles all the events involving a reservation.
15 */
16 class VboBookingHistory
17 {
18 /**
19 * @var int
20 */
21 protected $bid = null;
22
23 /**
24 * @var array
25 */
26 protected $prevBooking = null;
27
28 /**
29 * @var mixed
30 */
31 protected $data = null;
32
33 /**
34 * @var object
35 */
36 protected $dbo = null;
37
38 /**
39 * @var array
40 */
41 protected $typesMap = [];
42
43 /**
44 * List of event types worthy of a notification.
45 *
46 * @var array
47 *
48 * @since 1.15.0 (J) - 1.5.0 (WP)
49 */
50 protected $worthy_events = [
51 'NC',
52 'MW',
53 'NP',
54 'P0',
55 'PN',
56 'CR',
57 'CW',
58 'MC',
59 'CC',
60 'NO',
61 'PO',
62 'IR',
63 'PC',
64 'UR',
65 ];
66
67 /**
68 * Class constructor.
69 */
70 public function __construct()
71 {
72 $this->dbo = JFactory::getDbo();
73 $this->typesMap = $this->getTypesMap();
74 }
75
76 /**
77 * Returns an array of types mapped to
78 * the corresponding language definition.
79 * All the history types should be listed here.
80 *
81 * @return array
82 */
83 public function getTypesMap()
84 {
85 return [
86 //New booking with status Confirmed
87 'NC' => JText::translate('VBOBOOKHISTORYTNC'),
88 //Booking modified from website
89 'MW' => JText::translate('VBOBOOKHISTORYTMW'),
90 //Booking modified from back-end
91 'MB' => JText::translate('VBOBOOKHISTORYTMB'),
92 //New booking from back-end
93 'NB' => JText::translate('VBOBOOKHISTORYTNB'),
94 //New booking with status Pending
95 'NP' => JText::translate('VBOBOOKHISTORYTNP'),
96 //Booking paid for the first time
97 'P0' => JText::translate('VBOBOOKHISTORYTP0'),
98 //Booking paid for a second time
99 'PN' => JText::translate('VBOBOOKHISTORYTPN'),
100 //Cancellation request message
101 'CR' => JText::translate('VBOBOOKHISTORYTCR'),
102 //Booking cancelled via front-end website
103 'CW' => JText::translate('VBOBOOKHISTORYTCW'),
104 //Booking auto cancelled via front-end
105 'CA' => JText::translate('VBOBOOKHISTORYTCA'),
106 //Booking cancelled via back-end by admin
107 'CB' => JText::translate('VBOBOOKHISTORYTCB'),
108 //Booking Receipt generated via back-end
109 'BR' => JText::translate('VBOBOOKHISTORYTBR'),
110 //Booking Invoice generated
111 'BI' => JText::translate('VBOBOOKHISTORYTBI'),
112 //Booking registration unset status by admin
113 'RA' => JText::translate('VBOBOOKHISTORYTRA'),
114 //Booking checked-in status set by admin
115 'RB' => JText::translate('VBOBOOKHISTORYTRB'),
116 //Booking checked-out status set by admin
117 'RC' => JText::translate('VBOBOOKHISTORYTRC'),
118 //Booking no-show status set by admin
119 'RZ' => JText::translate('VBOBOOKHISTORYTRZ'),
120 //Booking set to Confirmed by admin
121 'TC' => JText::translate('VBOBOOKHISTORYTTC'),
122 //Booking set to Confirmed via App
123 'AC' => JText::translate('VBOBOOKHISTORYTAC'),
124 //Booking modified from channel
125 'MC' => JText::translate('VBOBOOKHISTORYTMC'),
126 //Booking cancelled from channel
127 'CC' => JText::translate('VBOBOOKHISTORYTCC'),
128 //Booking removed via App
129 'AR' => JText::translate('VBOBOOKHISTORYTAR'),
130 //Booking modified via App
131 'AM' => JText::translate('VBOBOOKHISTORYTAM'),
132 //New booking via App
133 'AN' => JText::translate('VBOBOOKHISTORYTAN'),
134 //New Booking from OTA
135 'NO' => JText::translate('VBOBOOKHISTORYTNO'),
136 //Report affecting the booking
137 'RP' => JText::translate('VBOBOOKHISTORYTRP'),
138 //Custom email sent to the customer by admin
139 'CE' => JText::translate('VBOBOOKHISTORYTCE'),
140 //Custom SMS sent to the customer by admin
141 'CS' => JText::translate('VBOBOOKHISTORYTCS'),
142 //Confirmation email re-sent by admin to guest
143 'ER' => JText::translate('VBRESENDORDEMAIL'),
144 //Payment Update (a new amount paid has been set)
145 'PU' => JText::translate('VBOBOOKHISTORYTPU'),
146 // Upsell Extras via front-end
147 'UE' => JText::translate('VBOBOOKHISTORYTUE'),
148 // Report guest misconduct to OTA
149 'GM' => JText::translate('VBOBOOKHISTORYTGM'),
150 // Send Email Cancellation by admin
151 'EC' => JText::translate('VBOBOOKHISTORYTEC'),
152 // Amount refunded
153 'RF' => JText::translate('VBOBOOKHISTORYTRF'),
154 // Refund Updated
155 'RU' => JText::translate('VBOBOOKHISTORYTRU'),
156 // Payable Amount Updated
157 'PB' => JText::translate('VBOBOOKHISTORYTPB'),
158 // Channel Manager custom event
159 'CM' => JText::translate('VBOBOOKHISTORYTCM'),
160 // Channel Manager payout notification
161 'PO' => JText::translate('VBOBOOKHISTORYTPO'),
162 // Inquiry reservation (website)
163 'IR' => JText::translate('VBOBOOKHISTORYTIR'),
164 // Pre-checkin updated via front-end
165 'PC' => JText::translate('VBOBOOKHISTORYTPC'),
166 // Guest review received
167 'GR' => JText::translate('VBOBOOKHISTORYTGR'),
168 // Upgrade room
169 'UR' => JText::translate('VBOBOOKHISTORYTUR'),
170 ];
171 }
172
173 /**
174 * Accesses the groups of history event types, useful to group multiple events.
175 * Groups are categories of events containing worthy event types of the same context.
176 *
177 * @param string $group get the list of event types of this group.
178 *
179 * @return array associative array of types group or empty array.
180 *
181 * @since 1.16.0 (J) - 1.6.0 (WP)
182 */
183 public function getTypeGroups($group = null)
184 {
185 // we group various types of events per category
186 $type_groups = [
187 // group "Bookings"
188 'GBK' => [
189 'name' => JText::translate('VBMENUTHREE'),
190 'types' => [
191 'NC',
192 'MW',
193 'NP',
194 'CR',
195 'CW',
196 'MC',
197 'CC',
198 'NO',
199 'IR',
200 'UR',
201 ],
202 ],
203 // group "Channel Manager"
204 'GCM' => [
205 'name' => JText::translate('VBMENUCHANNELMANAGER'),
206 'types' => [
207 'MC',
208 'CC',
209 'NO',
210 'PO',
211 'CM',
212 ],
213 ],
214 // group "Website"
215 'GWB' => [
216 'name' => JText::translate('VBORDFROMSITE'),
217 'types' => [
218 'NC',
219 'MW',
220 'NP',
221 'CR',
222 'CW',
223 'IR',
224 'PC',
225 'UE',
226 'GR',
227 'UR',
228 ],
229 ],
230 // group "Payments"
231 'GPM' => [
232 'name' => JText::translate('VBO_HISTORY_GPM'),
233 'types' => [
234 'P0',
235 'PN',
236 'PO',
237 'RF',
238 ],
239 ],
240 ];
241
242 if (!empty($group)) {
243 // attempt to fetch the requested group
244 return isset($type_groups[$group]) ? $type_groups[$group] : [];
245 }
246
247 // return the whole associative list
248 return $type_groups;
249 }
250
251 /**
252 * Sets the current booking ID.
253 *
254 * @param int $bid
255 *
256 * @return self
257 **/
258 public function setBid($bid)
259 {
260 $this->bid = (int)$bid;
261
262 return $this;
263 }
264
265 /**
266 * Sets the previous booking array.
267 * To calculate what has changed in the booking after the
268 * modification, VBO uses the method getLogBookingModification().
269 * VCM instead should use this method to tell the class that
270 * what has changed should be calculated to obtain the 'descr'
271 * text of the history record that will be stored.
272 *
273 * @param array $booking
274 *
275 * @return self
276 **/
277 public function setPrevBooking($booking)
278 {
279 if (is_array($booking)) {
280 $this->prevBooking = $booking;
281 }
282
283 return $this;
284 }
285
286 /**
287 * Sets extra data for the current history log.
288 *
289 * @param mixed $data array, object or string of extra data.
290 * Useful to store the amount paid to be invoiced,
291 * or the transaction details of the payments.
292 *
293 * @return self
294 **/
295 public function setExtraData($data)
296 {
297 $this->data = $data;
298
299 return $this;
300 }
301
302 /**
303 * Checks whether the type for the history record is valid.
304 *
305 * @param string $type
306 * @param [bool] $returnit if true, the translated value is returned. Otherwise boolean is returned
307 *
308 * @return boolean
309 */
310 public function validType($type, $returnit = false)
311 {
312 if ($returnit) {
313 return isset($this->typesMap[strtoupper($type)]) ? $this->typesMap[strtoupper($type)] : $type;
314 }
315
316 return isset($this->typesMap[strtoupper($type)]);
317 }
318
319 /**
320 * Reads the booking record.
321 * Returns false in case of failure.
322 *
323 * @param mixed
324 *
325 * @return array
326 */
327 protected function getBookingInfo()
328 {
329 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$this->bid;
330 $this->dbo->setQuery($q, 0, 1);
331
332 $record = $this->dbo->loadAssoc();
333
334 if (!$record) {
335 return false;
336 }
337
338 return $record;
339 }
340
341 /**
342 * Stores a new history record for the booking.
343 *
344 * @param string $type the char-type of store we are making for the history
345 * @param [string] $descr the description of this booking record (optional)
346 *
347 * @return boolean
348 */
349 public function store($type, $descr = '')
350 {
351 if (is_null($this->bid) || !$this->validType($type)) {
352 return false;
353 }
354
355 if (!$booking_info = $this->getBookingInfo()) {
356 return false;
357 }
358
359 if (empty($descr) && is_array($this->prevBooking)) {
360 /**
361 * VCM (including the App) could set the previous booking information,
362 * so we need to calculate what has changed with the booking.
363 * Load VBO language.
364 */
365 $lang = JFactory::getLanguage();
366 $lang->load('com_vikbooking', (defined('ABSPATH') && defined('VIKBOOKING_ADMIN_LANG') ? VIKBOOKING_ADMIN_LANG : JPATH_ADMINISTRATOR), $lang->getTag(), true);
367 if (!class_exists('VikBooking')) {
368 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikbooking.php');
369 }
370 $descr = VikBooking::getLogBookingModification($this->prevBooking);
371 }
372
373 // get the event dispatcher
374 $dispatcher = VBOFactory::getPlatform()->getDispatcher();
375
376 // build record object
377 $history_record = new stdClass;
378 $history_record->idorder = $this->bid;
379 $history_record->dt = JFactory::getDate()->toSql();
380 $history_record->type = $type;
381 $history_record->descr = $descr ?: null;
382 $history_record->totpaid = (float)$booking_info['totpaid'];
383 $history_record->total = (float)$booking_info['total'];
384 $history_record->data = $this->data;
385
386 /**
387 * Trigger event before saving a new history record.
388 *
389 * @since 1.16.0 (J) - 1.6.0 (WP)
390 */
391 $results = $dispatcher->filter('onBeforeSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
392 if (in_array(false, $results, true)) {
393 return false;
394 }
395
396 /**
397 * Store the extra data for this history log. Useful to store
398 * information about the amount paid and its invoicing status.
399 *
400 * @since 1.12 (J) - 1.1.7 (WP)
401 */
402 if (!is_null($history_record->data) && !is_scalar($history_record->data)) {
403 $history_record->data = json_encode($history_record->data);
404 }
405
406 // store record
407 if (!$this->dbo->insertObject('#__vikbooking_orderhistory', $history_record, 'id') || !isset($history_record->id)) {
408 return false;
409 }
410
411 /**
412 * Trigger event for the newly history record added.
413 *
414 * @since 1.16.0 (J) - 1.6.0 (WP)
415 */
416 $dispatcher->trigger('onAfterSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
417
418 return true;
419 }
420
421 /**
422 * Loads all the history records for this booking
423 *
424 * @return array
425 */
426 public function loadHistory()
427 {
428 if (empty($this->bid)) {
429 return [];
430 }
431
432 $q = "SELECT * FROM `#__vikbooking_orderhistory` WHERE `idorder`=" . (int)$this->bid . " ORDER BY `dt` DESC;";
433 $this->dbo->setQuery($q);
434
435 $history = $this->dbo->loadAssocList();
436
437 if (!$history) {
438 return [];
439 }
440
441 return $history;
442 }
443
444 /**
445 * Checks whether this booking has an event of the given type.
446 *
447 * @param string $type the type of the event.
448 *
449 * @return mixed last date on success, false otherwise.
450 *
451 * @since 1.13.5 (J) - 1.3.5 (WP)
452 */
453 public function hasEvent($type)
454 {
455 if (empty($type) || empty($this->bid)) {
456 return false;
457 }
458
459 $q = "SELECT `dt` FROM `#__vikbooking_orderhistory` WHERE `idorder`=" . (int)$this->bid . " AND `type`=" . $this->dbo->quote($type) . " ORDER BY `dt` DESC";
460 $this->dbo->setQuery($q, 0, 1);
461
462 $dt = $this->dbo->loadResult();
463
464 if (!$dt) {
465 return false;
466 }
467
468 return $dt;
469 }
470
471 /**
472 * Returns a list of records with data defined for the given event type.
473 * Useful to get a list of transactions data for the refund operations.
474 *
475 * @param mixed $type string or array event type(s).
476 * @param callable $callvalid callback for the data validation.
477 * @param bool $onlydata whether to get just the event data.
478 *
479 * @return mixed false or array with data records.
480 *
481 * @since 1.14.0 (J) - 1.4.0 (WP)
482 */
483 public function getEventsWithData($type, $callvalid = null, $onlydata = true)
484 {
485 if (empty($type) || empty($this->bid)) {
486 return false;
487 }
488
489 if (!is_array($type)) {
490 $type = [$type];
491 }
492
493 // quote all given types
494 $types = array_map(array($this->dbo, 'quote'), $type);
495
496 $q = "SELECT * FROM `#__vikbooking_orderhistory` WHERE `idorder`=" . (int)$this->bid . " AND `type` IN (" . implode(', ', $types) . ") ORDER BY `dt` ASC;";
497 $this->dbo->setQuery($q);
498 $events = $this->dbo->loadAssocList();
499
500 if ($events) {
501 $datas = [];
502 foreach ($events as $k => $e) {
503 $data = json_decode($e['data']);
504 if (!empty($data)) {
505 $events[$k]['data'] = $data;
506 }
507 $valid_data = true;
508 if (is_callable($callvalid)) {
509 $valid_data = call_user_func($callvalid, $events[$k]['data']);
510 }
511 if ($valid_data) {
512 array_push($datas, $events[$k]['data']);
513 }
514 }
515
516 return $onlydata ? $datas : $events;
517 }
518
519 return false;
520 }
521
522 /**
523 * Gets the latest history event per booking within a list.
524 * Only the latest event per booking will be considered.
525 *
526 * @param int $start the query start offset.
527 * @param int $limit the query limit.
528 * @param int $min_id optional minimum history ID to fetch.
529 * @param array $types optional list of event types (or groups) to fetch.
530 *
531 * @return array the list of recent history record objects.
532 *
533 * @since 1.15.0 (J) - 1.5.0 (WP)
534 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
535 * @since 1.16.0 (J) - 1.6.0 (WP) fourth argument $types introduced.
536 */
537 public function getLatestBookingEvents($start = 0, $limit = 20, $min_id = 0, array $types = [])
538 {
539 $clauses = [];
540
541 if ($min_id > 0) {
542 $clauses[] = "`h`.`id` > " . (int)$min_id;
543 } else {
544 $clauses[] = "`h`.`id` > 0";
545 }
546
547 if ($types) {
548 // scan for groups
549 $groups = [];
550 foreach ($types as $k => $type) {
551 if ($type && strlen($type) === 3 && $group = $this->getTypeGroups($type)) {
552 $groups = array_merge($groups, $group['types']);
553 unset($types[$k]);
554 }
555 }
556
557 if ($groups) {
558 // merge all types
559 $types = array_merge(array_values($types), array_unique($groups));
560 }
561
562 // quote all values
563 $types = array_map(array($this->dbo, 'q'), array_filter($types));
564
565 if ($types) {
566 // add query clause
567 $clauses[] = "`h`.`type` IN (" . implode(', ', $types) . ")";
568 }
569 }
570
571 /**
572 * On some servers with just 30k records in the history and 5k in bookings,
573 * the INNER JOIN to group the latest history events by booking ID is taking
574 * ~25 seconds to complete, which is not acceptable. This doesn't seem to happen
575 * on every database, but since the admin widget "latest events" requires to get
576 * just the latest history event ID by passing the limit to 1, we should not
577 * use the INNER JOIN all the times to save server resources.
578 * Refactoring performed to abandon also the LEFT JOINS which were timing out in
579 * case of several thousands of records. The widget "latest_events" crashed websites.
580 *
581 * @since 1.15.6 (J) - 1.5.12 (WP)
582 * @since 1.16.0 (J) - 1.6.0 (WP) INNER JOIN dismissed because too slow and not needed.
583 * @since 1.16.1 (J) - 1.6.1 (WP) LEFT JOIN dismissed because too slow, queries split.
584 */
585
586 // query just the history records without joining any other table, or the records would become hundreds of thousands
587 $q = "SELECT `h`.*
588 FROM `#__vikbooking_orderhistory` AS `h`
589 WHERE " . implode(' AND ', $clauses) . " ORDER BY `h`.`dt` DESC, `h`.`id` DESC";
590
591 $this->dbo->setQuery($q, $start, $limit);
592 $rows = $this->dbo->loadObjectList();
593 if (!$rows) {
594 return [];
595 }
596
597 // build a list of reservation IDs
598 $history_orders = [];
599 foreach ($rows as $row) {
600 $history_orders[] = (int)$row->idorder;
601 }
602 $history_orders = array_unique($history_orders);
603
604 // fetch the rest of the information
605 $q = "SELECT `o`.`id` AS `idorder`, `o`.`custdata`, `o`.`status`, `o`.`days`, `o`.`checkin`, `o`.`checkout`, `o`.`totpaid`,
606 `o`.`total`, `o`.`idorderota`, `o`.`channel`, `o`.`country`, `o`.`closure`, `c`.`pic`,
607 (
608 SELECT CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`)
609 FROM `#__vikbooking_ordersrooms` AS `or`
610 WHERE `or`.`idorder`=`o`.`id` LIMIT 1
611 ) AS `nominative`
612 FROM `#__vikbooking_orders` AS `o`
613 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
614 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
615 WHERE `o`.`id` IN (" . implode(', ', $history_orders) . ");";
616
617 $this->dbo->setQuery($q);
618 $rows_data = $this->dbo->loadObjectList();
619 if (!$rows_data) {
620 return [];
621 }
622
623 // merge information to object records
624 foreach ($rows as &$row) {
625 foreach ($rows_data as $row_data) {
626 if ($row_data->idorder != $row->idorder) {
627 continue;
628 }
629
630 // merge object properties by using casting
631 $row = (object) array_merge((array) $row, (array) $row_data);
632
633 break;
634 }
635 }
636
637 // unset last reference
638 unset($row);
639
640 // return the final list
641 return $rows;
642 }
643
644 /**
645 * Helper method to set a new list of worthy event types.
646 *
647 * @param array $worthy_events list of event type identifiers.
648 *
649 * @return self
650 *
651 * @since 1.15.0 (J) - 1.5.0 (WP)
652 */
653 public function setWorthyEventTypes($worthy_events = [])
654 {
655 if (is_array($worthy_events)) {
656 $this->worthy_events = $worthy_events;
657 }
658
659 return $this;
660 }
661
662 /**
663 * Helper method to get the list of worthy event types.
664 *
665 * @return array
666 *
667 * @since 1.15.4 (J) - 1.5.10 (WP)
668 */
669 public function getWorthyEventTypes()
670 {
671 return $this->worthy_events;
672 }
673
674 /**
675 * Gets the latest history events worthy of a notification.
676 *
677 * @param int $min_id optional minimum history ID to fetch.
678 * @param int $limit the query limit.
679 *
680 * @return array the list of worthy history record objects.
681 *
682 * @since 1.15.0 (J) - 1.5.0 (WP)
683 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
684 * INNER JOIN dismissed.
685 */
686 public function getWorthyEvents($min_id = 0, $limit = 0)
687 {
688 $clauses = [
689 "`o`.`status` IS NOT NULL",
690 "`o`.`closure` = 0"
691 ];
692
693 if ($min_id > 0) {
694 $clauses[] = "`h`.`id` > " . (int)$min_id;
695 }
696
697 if (count($this->worthy_events)) {
698 // quote all worthy types of event
699 $worthy_types = array_map(array($this->dbo, 'quote'), $this->worthy_events);
700 $clauses[] = "`h`.`type` IN (" . implode(', ', $worthy_types) . ")";
701 }
702
703 // join as few tables as possible to reduce the execution timing
704 $q = "SELECT `h`.*, `o`.`status`, `o`.`days`, `o`.`checkin`, `o`.`checkout`,
705 `o`.`totpaid`, `o`.`total`, `o`.`idorderota`, `o`.`channel`, `o`.`country`
706 FROM `#__vikbooking_orderhistory` AS `h`
707 LEFT JOIN `#__vikbooking_orders` AS `o` ON `h`.`idorder`=`o`.`id`
708 WHERE " . implode(' AND ', $clauses) . " ORDER BY `h`.`dt` DESC";
709
710 $this->dbo->setQuery($q, 0, $limit);
711 $rows = $this->dbo->loadObjectList();
712 if (!$rows) {
713 return [];
714 }
715
716 // set the "pic" property to a default empty value
717 foreach ($rows as &$row) {
718 $row->pic = '';
719 }
720
721 // unset last reference
722 unset($row);
723
724 // build a list of reservation IDs
725 $history_orders = [];
726 foreach ($rows as $row) {
727 $history_orders[] = (int)$row->idorder;
728 }
729 $history_orders = array_unique($history_orders);
730
731 // fetch the rest of the information
732 $q = "SELECT `o`.`id`, `c`.`pic`
733 FROM `#__vikbooking_orders` AS `o`
734 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
735 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
736 WHERE `o`.`id` IN (" . implode(', ', $history_orders) . ");";
737
738 $this->dbo->setQuery($q);
739 $rows_data = $this->dbo->loadObjectList();
740 if ($rows_data) {
741 // merge information
742 foreach ($rows as &$row) {
743 foreach ($rows_data as $row_data) {
744 if ($row_data->id != $row->idorder) {
745 continue;
746 }
747
748 // set picture property
749 $row->pic = $row_data->pic;
750 break;
751 }
752 }
753 }
754
755 // return all records found
756 return $rows;
757 }
758 }
759