PluginProbe
Easy Hotel – Powerful Hotel Booking / 2.0.4
Easy Hotel – Powerful Hotel Booking v2.0.4
2.0.8 2.0.7 2.0.6 2.0.5 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 1.9.9 1.9.8 1.9.7 1.9.6 1.9.5 1.9.4 1.9.3 1.9.2 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 All 110 releases
easy-hotel / admin / includes / dashboard / class-eshb-dashboard-data.php

class-eshb-dashboard-data.php in Easy Hotel – Powerful Hotel Booking 2.0.4, at admin/includes/dashboard/class-eshb-dashboard-data.php

548 lines 14.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Easy Hotel – Dashboard data layer.
4 *
5 * Reads bookings / accommodations straight from the DB and computes every
6 * stat the dashboard UI needs. No presentation here, pure data.
7 *
8 * @package Easy_Hotel
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 class ESHB_Dashboard_Data {
16
17 /**
18 * Booking statuses we treat as an active / occupying reservation.
19 *
20 * @var string[]
21 */
22 protected $active_statuses = array( 'processing', 'completed', 'on-hold', 'deposit-payment' );
23
24 /**
25 * Cached bookings list so multiple stats don't re-query.
26 *
27 * @var array|null
28 */
29 protected $bookings = null;
30
31 /**
32 * Today's date in the site timezone (Y-m-d).
33 *
34 * @var string
35 */
36 protected $today;
37
38 public function __construct() {
39 $this->today = current_time( 'Y-m-d' );
40 }
41
42 /**
43 * Build the full payload returned by the REST endpoint.
44 *
45 * @return array
46 */
47 public function get_dashboard_data() {
48 $bookings = $this->get_bookings();
49
50 return array(
51 'today' => $this->today,
52 'currency' => $this->get_currency(),
53 'stats' => $this->get_stats( $bookings ),
54 'recentBookings' => $this->get_recent_bookings(),
55 'roomAvailability' => $this->get_room_availability( $bookings ),
56 'bookingTrend' => $this->get_booking_trend(),
57 'calendar' => $this->get_calendar( $bookings ),
58 );
59 }
60
61 /**
62 * Currency symbol + position from plugin settings.
63 *
64 * @return array
65 */
66 protected function get_currency() {
67 $symbol = '$';
68 $position = 'left';
69
70 if ( class_exists( 'ESHB_Core' ) ) {
71 $core = ESHB_Core::instance();
72 $symbol = $core->get_eshb_currency_symbol();
73 $position = $core->get_eshb_currency_position();
74 }
75
76 return array(
77 'symbol' => $symbol,
78 'position' => $position,
79 );
80 }
81
82 /**
83 * Load every booking once and normalise the meta we care about.
84 *
85 * @return array
86 */
87 protected function get_bookings() {
88 if ( null !== $this->bookings ) {
89 return $this->bookings;
90 }
91
92 $query = new WP_Query(
93 array(
94 'post_type' => 'eshb_booking',
95 'post_status' => 'any',
96 'posts_per_page' => -1,
97 'no_found_rows' => true,
98 'orderby' => 'date',
99 'order' => 'DESC',
100 )
101 );
102
103 $bookings = array();
104
105 foreach ( $query->posts as $post ) {
106 $meta = get_post_meta( $post->ID, 'eshb_booking_metaboxes', true );
107 $meta = is_array( $meta ) ? $meta : array();
108
109 $bookings[] = array(
110 'id' => $post->ID,
111 'post_date' => get_post_time( 'Y-m-d', false, $post ),
112 'status' => ! empty( $meta['booking_status'] ) ? $meta['booking_status'] : $post->post_status,
113 'accomodation_id' => ! empty( $meta['booking_accomodation_id'] ) ? (int) $meta['booking_accomodation_id'] : 0,
114 'start_date' => ! empty( $meta['booking_start_date'] ) ? $meta['booking_start_date'] : '',
115 'end_date' => ! empty( $meta['booking_end_date'] ) ? $meta['booking_end_date'] : '',
116 'total_price' => isset( $meta['total_price'] ) ? (float) $meta['total_price'] : 0,
117 'room_quantity' => ! empty( $meta['room_quantity'] ) ? (int) $meta['room_quantity'] : 1,
118 'adults' => isset( $meta['adult_quantity'] ) ? (int) $meta['adult_quantity'] : 0,
119 'children' => isset( $meta['children_quantity'] ) ? (int) $meta['children_quantity'] : 0,
120 'first_name' => ! empty( $meta['first_name'] ) ? $meta['first_name'] : '',
121 'last_name' => ! empty( $meta['last_name'] ) ? $meta['last_name'] : '',
122 'title' => $post->post_title,
123 );
124 }
125
126 wp_reset_postdata();
127
128 $this->bookings = $bookings;
129
130 return $this->bookings;
131 }
132
133 /**
134 * Whether a booking is "active" (occupies a room) on a given date range.
135 */
136 protected function is_active( $status ) {
137 return in_array( $status, $this->active_statuses, true );
138 }
139
140 /**
141 * Does a booking cover the given date (start <= date < end, or single day)?
142 */
143 protected function covers_date( $booking, $date ) {
144 if ( empty( $booking['start_date'] ) ) {
145 return false;
146 }
147 $start = $booking['start_date'];
148 $end = ! empty( $booking['end_date'] ) ? $booking['end_date'] : $start;
149
150 if ( $start === $end ) {
151 return $date === $start;
152 }
153
154 return ( $date >= $start && $date < $end );
155 }
156
157 /**
158 * Top stat cards + month-over-month deltas.
159 *
160 * @param array $bookings Normalised bookings.
161 * @return array
162 */
163 protected function get_stats( $bookings ) {
164 $total = count( $bookings );
165 $pending = 0;
166 $checkins = 0;
167 $checkouts = 0;
168
169 $yesterday = gmdate( 'Y-m-d', strtotime( $this->today . ' -1 day' ) );
170 $checkins_yday = 0;
171 $checkouts_yday = 0;
172
173 $this_month = gmdate( 'Y-m', strtotime( $this->today ) );
174 $last_month = gmdate( 'Y-m', strtotime( $this->today . ' first day of last month' ) );
175
176 $total_this = 0;
177 $total_last = 0;
178 $pend_this = 0;
179 $pend_last = 0;
180
181 foreach ( $bookings as $b ) {
182 if ( 'pending' === $b['status'] || 'deposit-payment' === $b['status'] ) {
183 $pending++;
184 }
185 if ( $b['start_date'] === $this->today ) {
186 $checkins++;
187 } elseif ( $b['start_date'] === $yesterday ) {
188 $checkins_yday++;
189 }
190 if ( $b['end_date'] === $this->today ) {
191 $checkouts++;
192 } elseif ( $b['end_date'] === $yesterday ) {
193 $checkouts_yday++;
194 }
195
196 $b_month = substr( $b['post_date'], 0, 7 );
197 if ( $b_month === $this_month ) {
198 $total_this++;
199 if ( 'pending' === $b['status'] ) {
200 $pend_this++;
201 }
202 } elseif ( $b_month === $last_month ) {
203 $total_last++;
204 if ( 'pending' === $b['status'] ) {
205 $pend_last++;
206 }
207 }
208 }
209
210 $available = $this->get_available_rooms_today( $bookings );
211
212 return array(
213 'totalBookings' => array(
214 'value' => $total,
215 'delta' => $this->delta( $total_this, $total_last, 'fromLast' ),
216 ),
217 'pendingBookings' => array(
218 'value' => $pending,
219 'delta' => $this->delta( $pend_this, $pend_last, 'fromLast' ),
220 ),
221 'checkinsToday' => array(
222 'value' => $checkins,
223 'delta' => $this->delta( $checkins, $checkins_yday, 'fromYday' ),
224 ),
225 'checkoutsToday' => array(
226 'value' => $checkouts,
227 'delta' => $this->delta( $checkouts, $checkouts_yday, 'fromYday' ),
228 ),
229 'availableRooms' => array(
230 'value' => $available['available'],
231 'total' => $available['total'],
232 'delta' => null,
233 ),
234 );
235 }
236
237 /**
238 * Percentage change helper. Returns null when there's no baseline.
239 *
240 * @param int $current Current period count.
241 * @param int $previous Previous period count (baseline).
242 * @param string $suffix i18n key for the trailing label (fromLast|fromYday).
243 * @return array|null
244 */
245 protected function delta( $current, $previous, $suffix = 'fromLast' ) {
246 $current = (int) $current;
247 $previous = (int) $previous;
248
249 // No baseline last period. If there's activity now, it's all-new growth
250 // (↑ 100%); if both are zero there's genuinely nothing to show.
251 if ( $previous <= 0 ) {
252 if ( $current <= 0 ) {
253 return null;
254 }
255
256 return array(
257 'percent' => 100,
258 'direction' => 'up',
259 'suffix' => $suffix,
260 );
261 }
262
263 $change = ( ( $current - $previous ) / $previous ) * 100;
264
265 return array(
266 'percent' => round( abs( $change ) ),
267 'direction' => ( $change >= 0 ) ? 'up' : 'down',
268 'suffix' => $suffix,
269 );
270 }
271
272 /**
273 * Total room units across all accommodations minus those occupied today.
274 */
275 protected function get_available_rooms_today( $bookings ) {
276 $total = 0;
277
278 $rooms = get_posts(
279 array(
280 'post_type' => 'eshb_accomodation',
281 'post_status' => 'publish',
282 'posts_per_page' => -1,
283 'fields' => 'ids',
284 'no_found_rows' => true,
285 )
286 );
287
288 foreach ( $rooms as $room_id ) {
289 $total += $this->get_room_total_units( $room_id );
290 }
291
292 $occupied = 0;
293 foreach ( $bookings as $b ) {
294 if ( $this->is_active( $b['status'] ) && $this->covers_date( $b, $this->today ) ) {
295 $occupied += max( 1, $b['room_quantity'] );
296 }
297 }
298
299 return array(
300 'total' => $total,
301 'available' => max( 0, $total - $occupied ),
302 );
303 }
304
305 /**
306 * Total room units configured for an accommodation.
307 */
308 protected function get_room_total_units( $room_id ) {
309 $meta = get_post_meta( $room_id, 'eshb_accomodation_metaboxes', true );
310 $total = is_array( $meta ) && ! empty( $meta['total_rooms'] ) ? (int) $meta['total_rooms'] : 1;
311
312 return max( 1, $total );
313 }
314
315 /**
316 * Latest 5 bookings for the table.
317 */
318 protected function get_recent_bookings() {
319 $bookings = array_slice( $this->get_bookings(), 0, 5 );
320 $rows = array();
321
322 foreach ( $bookings as $b ) {
323 $name = trim( $b['first_name'] . ' ' . $b['last_name'] );
324 if ( '' === $name ) {
325 $name = __( 'Guest', 'easy-hotel' );
326 }
327
328 $guests = array();
329 if ( $b['adults'] > 0 ) {
330 /* translators: %d: number of adults. */
331 $guests[] = sprintf( _n( '%d Adult', '%d Adults', $b['adults'], 'easy-hotel' ), $b['adults'] );
332 }
333 if ( $b['children'] > 0 ) {
334 /* translators: %d: number of children. */
335 $guests[] = sprintf( _n( '%d Child', '%d Children', $b['children'], 'easy-hotel' ), $b['children'] );
336 }
337
338 $rows[] = array(
339 'id' => $b['id'],
340 'label' => '#' . ( '' !== $b['title'] ? $b['title'] : 'EHB' . $b['id'] ),
341 'editLink' => get_edit_post_link( $b['id'], 'raw' ),
342 'guest' => $name,
343 'room' => $b['accomodation_id'] ? get_the_title( $b['accomodation_id'] ) : '—',
344 'checkIn' => $this->format_date( $b['start_date'] ),
345 'checkOut' => $this->format_date( $b['end_date'] ),
346 'guests' => implode( ', ', $guests ),
347 'total' => (float) $b['total_price'],
348 'status' => $b['status'],
349 'statusLabel' => $this->status_label( $b['status'] ),
350 );
351 }
352
353 return $rows;
354 }
355
356 /**
357 * Per-room availability summary (available / total units).
358 */
359 protected function get_room_availability( $bookings ) {
360 $rooms = get_posts(
361 array(
362 'post_type' => 'eshb_accomodation',
363 'post_status' => 'publish',
364 'posts_per_page' => 6,
365 'no_found_rows' => true,
366 )
367 );
368
369 $summary = array();
370
371 foreach ( $rooms as $room ) {
372 $total = $this->get_room_total_units( $room->ID );
373 $occupied = 0;
374
375 foreach ( $bookings as $b ) {
376 if ( $b['accomodation_id'] === $room->ID && $this->is_active( $b['status'] ) && $this->covers_date( $b, $this->today ) ) {
377 $occupied += max( 1, $b['room_quantity'] );
378 }
379 }
380
381 $available = max( 0, $total - $occupied );
382
383 $summary[] = array(
384 'name' => get_the_title( $room->ID ),
385 'available' => $available,
386 'total' => $total,
387 'percent' => $total > 0 ? round( ( $available / $total ) * 100 ) : 0,
388 );
389 }
390
391 return $summary;
392 }
393
394 /**
395 * Booking trend series for each selectable range. The dashboard switches
396 * between these client-side, so no extra request is needed per range.
397 *
398 * @return array{ '7d': array, '30d': array, '1y': array }
399 */
400 protected function get_booking_trend() {
401 return array(
402 '7d' => $this->trend_daily( 7 ),
403 '30d' => $this->trend_daily( 30 ),
404 '1y' => $this->trend_monthly( 12 ),
405 );
406 }
407
408 /**
409 * Bookings created per day for the last N days.
410 */
411 protected function trend_daily( $days ) {
412 $counts = array();
413
414 for ( $i = $days - 1; $i >= 0; $i-- ) {
415 $date = gmdate( 'Y-m-d', strtotime( $this->today . " -{$i} day" ) );
416 $counts[ $date ] = 0;
417 }
418
419 foreach ( $this->get_bookings() as $b ) {
420 if ( isset( $counts[ $b['post_date'] ] ) ) {
421 $counts[ $b['post_date'] ]++;
422 }
423 }
424
425 $series = array();
426 foreach ( $counts as $date => $count ) {
427 $series[] = array(
428 'date' => $date,
429 'label' => gmdate( 'M j', strtotime( $date ) ),
430 'count' => $count,
431 );
432 }
433
434 return $series;
435 }
436
437 /**
438 * Bookings created per month for the last N months.
439 */
440 protected function trend_monthly( $months ) {
441 $counts = array();
442 $base = strtotime( gmdate( 'Y-m-01', strtotime( $this->today ) ) );
443
444 for ( $i = $months - 1; $i >= 0; $i-- ) {
445 $key = gmdate( 'Y-m', strtotime( "-{$i} month", $base ) );
446 $counts[ $key ] = 0;
447 }
448
449 foreach ( $this->get_bookings() as $b ) {
450 $key = substr( $b['post_date'], 0, 7 );
451 if ( isset( $counts[ $key ] ) ) {
452 $counts[ $key ]++;
453 }
454 }
455
456 $series = array();
457 foreach ( $counts as $key => $count ) {
458 $series[] = array(
459 'date' => $key,
460 'label' => date_i18n( 'M', strtotime( $key . '-01' ) ),
461 'count' => $count,
462 );
463 }
464
465 return $series;
466 }
467
468 /**
469 * Per-day status map for the current month's calendar.
470 *
471 * Each day is one of: booked, blocked, available, past.
472 */
473 protected function get_calendar( $bookings ) {
474 $year = (int) gmdate( 'Y', strtotime( $this->today ) );
475 $month = (int) gmdate( 'n', strtotime( $this->today ) );
476 $days = (int) gmdate( 't', strtotime( $this->today ) );
477
478 $blocked = $this->get_blocked_dates();
479
480 $cells = array();
481
482 for ( $d = 1; $d <= $days; $d++ ) {
483 $date = sprintf( '%04d-%02d-%02d', $year, $month, $d );
484 $status = 'available';
485
486 if ( in_array( $date, $blocked, true ) ) {
487 $status = 'blocked';
488 } else {
489 foreach ( $bookings as $b ) {
490 if ( $this->is_active( $b['status'] ) && $this->covers_date( $b, $date ) ) {
491 $status = 'booked';
492 break;
493 }
494 }
495 }
496
497 $cells[] = array(
498 'date' => $date,
499 'day' => $d,
500 'status' => $status,
501 );
502 }
503
504 return array(
505 'year' => $year,
506 'month' => $month,
507 'monthName' => date_i18n( 'F Y', strtotime( $this->today ) ),
508 'firstDow' => (int) gmdate( 'w', strtotime( sprintf( '%04d-%02d-01', $year, $month ) ) ),
509 'days' => $cells,
510 );
511 }
512
513 /**
514 * Blocked dates, if the calendar add-on exposes them. Filterable.
515 *
516 * @return string[]
517 */
518 protected function get_blocked_dates() {
519 return apply_filters( 'eshb_dashboard_blocked_dates', array() );
520 }
521
522 /**
523 * Human readable booking status label.
524 */
525 protected function status_label( $status ) {
526 if ( class_exists( 'ESHB_Helper' ) ) {
527 $statuses = ESHB_Helper::eshb_get_booking_statuses();
528 if ( isset( $statuses[ $status ] ) ) {
529 return $statuses[ $status ];
530 }
531 }
532
533 return ucfirst( str_replace( '-', ' ', $status ) );
534 }
535
536 /**
537 * Format a Y-m-d date for display, e.g. "Jun 18, 2025".
538 */
539 protected function format_date( $date ) {
540 if ( empty( $date ) ) {
541 return '—';
542 }
543 $ts = strtotime( $date );
544
545 return $ts ? date_i18n( 'M j, Y', $ts ) : $date;
546 }
547 }
548