get('startTime'); $endTime = $request->get('endTime'); if ($startTime && $endTime) { $timeZone = DateTimeHelper::getTimeZone(); $startTime = DateTimeHelper::convertToUtc($startTime, $timeZone); $endTime = DateTimeHelper::convertToUtc($endTime, $timeZone); $bookingWidgetNumbers = $this->getBookingWidgetNumbers($startTime, $endTime); } else { $startTime = gmdate('Y-m-d H:i:s', strtotime('-30 days')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $endTime = gmdate('Y-m-d H:i:s', strtotime('now UTC')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $bookingWidgetNumbers = $this->getAllBookingWidgetNumbers(); } $bookingWidgetStats = $this->getBookingWidgetStats($startTime, $endTime); $paymentWidget = $this->getPaymentWidgets($startTime, $endTime); $widgets = [ [ 'title' => __('Total Bookings', 'fluent-booking'), 'period' => 'all', 'number' => $bookingWidgetNumbers['totalBooked'], 'content' => $bookingWidgetStats['bookedComparison'], 'icon' => ' ', 'stat' => $bookingWidgetStats['bookedStat'] ], [ 'title' => __('Completed Bookings', 'fluent-booking'), 'period' => 'completed', 'number' => $bookingWidgetNumbers['bookingCompleted'], 'content' => $bookingWidgetStats['completedComparison'], 'icon' => ' ', 'stat' => $bookingWidgetStats['completedStat'] ] ]; $totalPaymentWidget = apply_filters('fluent_booking/total_payment_widget', [], $paymentWidget); $widgets[] = $totalPaymentWidget ?: [ 'title' => __('Cancelled Bookings', 'fluent-booking'), 'period' => 'cancelled', 'number' => $bookingWidgetNumbers['bookingCancelled'], 'content' => $bookingWidgetStats['cancelledComparison'], 'icon' => ' ', 'stat' => $bookingWidgetStats['cancelledStat'] ]; $widgets[] = [ 'title' => __('Total Guests', 'fluent-booking'), 'number' => $bookingWidgetNumbers['totalGuests'], 'content' => $bookingWidgetStats['guestComparison'], 'icon' => ' ', 'stat' => $bookingWidgetStats['guestStat'] ]; apply_filters('fluent_booking/dashboard_widgets', $widgets); return [ 'overview' => $widgets, 'latest_books' => $this->getLatestBooks(), 'next_meetings' => $this->getNextMeetings() ]; } /** * @return array */ public function getGraphReports(Request $request) { list($startDate, $endDate) = $request->get('date_range') ?: ['', '']; $period = $this->makeDatePeriod( $from = $this->makeFromDate($startDate), $to = $this->makeToDate($endDate), $frequency = $this->getFrequency($from, $to) ); list($groupBy, $orderBy) = $this->getGroupAndOrder($frequency); // Define a function to fetch booking data based on status // Scoped through BookingReportService so this graph counts the same // bookings as the widgets above it and the schedules list beside it. // It previously scoped on the host_user_id column alone, which hid // bookings a limited host was a secondary host on. $fetchBookingsByStatus = function ($status) use ($groupBy, $orderBy, $frequency, $from, $to) { return BookingReportService::scoped() ->select($this->prepareSelect($frequency)) ->where('status', $status) ->whereBetween('created_at', [$from->format('Y-m-d'), $to->format('Y-m-d')]) ->groupBy($groupBy) ->orderBy($orderBy, 'ASC') ->get(); }; // Fetch bookings for different statuses $totalBooked = $fetchBookingsByStatus('scheduled'); $totalCompleted = $fetchBookingsByStatus('completed'); $totalCancelled = $fetchBookingsByStatus('cancelled'); return [ 'booked_stats' => $this->getResult($period, $totalBooked), 'completed_stats' => $this->getResult($period, $totalCompleted), 'cancelled_stats' => $this->getResult($period, $totalCancelled) ]; } private function getBookingWidgetStats($startTime, $endTime) { $startTimeStamp = strtotime($startTime); $endTimeStamp = strtotime($endTime); $differenceInDays = ($endTimeStamp - $startTimeStamp) / (60 * 60 * 24); $lastMonthStartTime = gmdate('Y-m-d H:i:s', strtotime("$startTime - $differenceInDays days")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $bookingStats = $this->getBookingStats($startTime, $endTime, $lastMonthStartTime, $startTime); $comparison = $this->getComparisonLabel($differenceInDays); $bookingStats['bookedComparison'] = $comparison; $bookingStats['completedComparison'] = $comparison; $bookingStats['cancelledComparison'] = $comparison; $bookingStats['guestComparison'] = $comparison; return $bookingStats; } private function getBookingStats($currentMonthStart, $currentMonthEnd, $lastMonthStart, $lastMonthEnd) { $createdBetween = function ($start, $end) { return BookingReportService::scoped()->whereBetween('created_at', [$start, $end]); }; $endTimeBetween = function ($start, $end) { return BookingReportService::scoped()->whereBetween('end_time', [$start, $end]); }; // Bookings and guests based on 'created_at' $totalBookedCurrentMonth = $createdBetween($currentMonthStart, $currentMonthEnd)->count(); $totalBookedLastMonth = $createdBetween($lastMonthStart, $lastMonthEnd)->count(); $totalGuestsCurrentMonth = $createdBetween($currentMonthStart, $currentMonthEnd)->distinct()->count('email'); $totalGuestsLastMonth = $createdBetween($lastMonthStart, $lastMonthEnd)->distinct()->count('email'); // Completed and cancelled based on 'end_time' $bookingCompletedCurrentMonth = $endTimeBetween($currentMonthStart, $currentMonthEnd)->where('status', 'completed')->count(); $bookingCompletedLastMonth = $endTimeBetween($lastMonthStart, $lastMonthEnd)->where('status', 'completed')->count(); $bookingCancelledCurrentMonth = $endTimeBetween($currentMonthStart, $currentMonthEnd)->where('status', 'cancelled')->count(); $bookingCancelledLastMonth = $endTimeBetween($lastMonthStart, $lastMonthEnd)->where('status', 'cancelled')->count(); $bookingStats['bookedStat'] = $this->getPercentage($totalBookedCurrentMonth, $totalBookedLastMonth); $bookingStats['completedStat'] = $this->getPercentage($bookingCompletedCurrentMonth, $bookingCompletedLastMonth); $bookingStats['cancelledStat'] = $this->getPercentage($bookingCancelledCurrentMonth, $bookingCancelledLastMonth); $bookingStats['guestStat'] = $this->getPercentage($totalGuestsCurrentMonth, $totalGuestsLastMonth); return $bookingStats; } private function getPercentage($currentMonthTotal, $lastMonthTotal) { if ($lastMonthTotal > 0) { return round((($currentMonthTotal - $lastMonthTotal) / $lastMonthTotal) * 100, 2); } // Nothing before and nothing now is no change, not a rise from nothing - which is what // every tile on a fresh install used to claim. if (!$currentMonthTotal) { return 0; } return 100; } private function getBookingWidgetNumbers($startTime, $endTime) { $createdBetween = function () use ($startTime, $endTime) { return BookingReportService::scoped()->whereBetween('created_at', [$startTime, $endTime]); }; $endTimeBetween = function () use ($startTime, $endTime) { return BookingReportService::scoped()->whereBetween('end_time', [$startTime, $endTime]); }; $totalBooked = $createdBetween()->count(); $totalGuests = $createdBetween()->distinct()->count('email'); $bookingCompleted = $endTimeBetween()->where('status', 'completed')->count(); $bookingCancelled = $endTimeBetween()->where('status', 'cancelled')->count(); return [ 'totalBooked' => $totalBooked, 'totalGuests' => $totalGuests, 'bookingCompleted' => $bookingCompleted, 'bookingCancelled' => $bookingCancelled ]; } private function getAllBookingWidgetNumbers() { // Four undated counts, so four full reads of the bookings table on // every dashboard load. They are lifetime totals; five minutes stale // is invisible. $cacheKey = 'fcal_report_all_widgets_' . get_current_blog_id() . '_' . get_current_user_id(); $cached = get_transient($cacheKey); if (is_array($cached)) { return $cached; } $totalBooked = BookingReportService::scoped()->count(); $bookingCompleted = BookingReportService::scoped()->where('status', 'completed')->count(); $bookingCancelled = BookingReportService::scoped()->where('status', 'cancelled')->count(); $totalGuests = BookingReportService::scoped()->distinct()->count('email'); $numbers = [ 'totalBooked' => $totalBooked, 'totalGuests' => $totalGuests, 'bookingCompleted' => $bookingCompleted, 'bookingCancelled' => $bookingCancelled ]; set_transient($cacheKey, $numbers, 300); return $numbers; } /** * The window the percentage beside each number is measured against. * * This used to describe the direction of the change - "More than last month" - which is * what the percentage next to it already says, so the tile stated one fact twice and left * the window it was comparing against unstated. It was also wrong whenever a custom range * was picked, since the comparison is always against a preceding window of the same * length, not against a calendar month. * * @param float $days Length of the reporting window, in days. * * @return string */ private function getComparisonLabel($days) { $days = max(1, (int) round($days)); if ($days === 1) { return __('vs. previous day', 'fluent-booking'); } /* translators: %d - the number of days being compared against */ return sprintf(__('vs. previous %d days', 'fluent-booking'), $days); } private function getPaymentWidgets($startTime, $endTime) { if (!defined('FLUENT_BOOKING_PRO_DIR_FILE')) { return []; } $stripSettings = get_option('fluent_booking_payment_settings_stripe'); $isActive = Arr::get($stripSettings, 'is_active'); if ($isActive == 'no') { return []; } $startTimeStamp = strtotime($startTime); $endTimeStamp = strtotime($endTime); $differenceInDays = ($endTimeStamp - $startTimeStamp) / (60 * 60 * 24); $lastMonthStartTime = gmdate('Y-m-d H:i:s', strtotime("$startTime - $differenceInDays days")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $current_user_email = null; $cantSeeTotal = PermissionManager::userCan(['manage_all_data', 'read_all_bookings', 'manage_all_bookings', 'read_other_calendars', 'manage_other_calendars']); if (!$cantSeeTotal) { $current_user_email = wp_get_current_user()->user_email; } $currentMonthTotal = \FluentBookingPro\App\Models\Order::where('status', 'paid') ->whereBetween('created_at', [$startTime, $endTime]) ->when($current_user_email, function ($query, $email) { return $query->whereHas('booking', function ($query) use ($email) { $query->where('email', $email); }); }) ->selectRaw('SUM(total_amount / 100) as total') ->first() ->total; $lastMonthTotal = \FluentBookingPro\App\Models\Order::where('status', 'paid') ->whereBetween('created_at', [$lastMonthStartTime, $startTime]) ->when($current_user_email, function ($query, $email) { return $query->whereHas('booking', function ($query) use ($email) { $query->where('email', $email); }); }) ->selectRaw('SUM(total_amount / 100) as total') ->first() ->total; $paymentPercentage = $this->getPercentage($currentMonthTotal, $lastMonthTotal); $paymentComparison = $this->getComparisonLabel($differenceInDays); $paymentStats['totalPayment'] = intval($currentMonthTotal); $paymentStats['paymentComparison'] = $paymentComparison; $paymentStats['paymentStat'] = $paymentPercentage; return $paymentStats; } public function getNextMeetings() { $bookingQuery = Booking::with(['slot']) ->where('status', 'scheduled') ->upcoming() ->orderBy('start_time', 'ASC'); if (!PermissionManager::userCanSeeAllBookings()) { $bookingQuery->whereHostAccess(get_current_user_id()); } $nextMeetings = $bookingQuery->limit(50)->get() ->unique('group_id') ->take(5) ->values(); foreach ($nextMeetings as $meeting) { if (!$meeting->slot) { $meeting->author = [ 'name' => 'unknown' ]; $meeting->slot = (object)[]; } else { $meeting->author = $meeting->slot->getAuthorProfile(false); } $meeting->title = $meeting->getBookingTitle(true); if ($meeting->isMultiGuestBooking()) { $meeting->booked_count = Booking::where('group_id', $meeting->group_id) ->whereIn('status', ['scheduled', 'completed'])->count(); } } return $nextMeetings; } public function getLatestBooks() { $bookingQuery = Booking::whereIn('status', ['pending', 'scheduled', 'completed']); if (!PermissionManager::userCanSeeAllBookings()) { $bookingQuery->whereHostAccess(get_current_user_id()); } return $bookingQuery->latest()->take(5)->get(); } public function getActivities() { $activityQuery = BookingActivity::query(); if (!PermissionManager::userCanSeeAllBookings()) { $activityQuery->whereHas('booking.calendar', function ($q) { $q->where('user_id', get_current_user_id()); }); } $activities = $activityQuery->latest()->take(100)->get(); return [ 'activities' => $activities ]; } }