| @@ -1,126 +1,198 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentBooking\App\Http\Controllers; |
| 4 | 4 | |
| 5 | -use FluentBooking\App\App; | |
| 6 | 5 | use FluentBooking\App\Models\Booking; |
| 7 | 6 | use FluentBooking\App\Models\Calendar; |
| 7 | +use FluentBooking\App\Models\CalendarSlot; | |
| 8 | 8 | use FluentBooking\App\Models\BookingActivity; |
| 9 | -use FluentBooking\App\Models\Meta; | |
| 9 | +use FluentBooking\Framework\Database\Orm\ModelNotFoundException; | |
| 10 | 10 | use FluentBooking\App\Services\EmailNotificationService; |
| 11 | 11 | use FluentBooking\App\Services\Helper; |
| 12 | +use FluentBooking\Framework\Support\Arr; | |
| 12 | 13 | use FluentBooking\App\Services\CurrenciesHelper; |
| 13 | -use FluentBooking\Framework\Support\Arr; | |
| 14 | 14 | use FluentBooking\Framework\Http\Request\Request; |
| 15 | 15 | use FluentBooking\App\Services\PermissionManager; |
| 16 | 16 | use FluentBooking\App\Services\CalendarService; |
| 17 | +use FluentBooking\App\Services\ExportHelper; | |
| 18 | +use FluentBooking\App\Services\Integrations\FluentCRM\CrmContactService; | |
| 19 | +use FluentBooking\App\Services\Integrations\FluentCart\CustomerProfileService; | |
| 20 | +use FluentCrm\App\Services\PermissionManager as CrmPermissionManager; | |
| 17 | 21 | |
| 18 | 22 | class SchedulesController extends Controller |
| 19 | 23 | { |
| 20 | 24 | public function index(Request $request) |
| 21 | 25 | { |
| 22 | - $filters = $request->get('filters', []); | |
| 26 | + $author = $this->resolveAuthor($request); | |
| 23 | 27 | |
| 24 | - $period = Arr::get($filters, 'period', 'upcoming'); | |
| 28 | + $query = $this->buildSchedulesQuery($request, $author); | |
| 25 | 29 | |
| 26 | - $eventId = Arr::get($filters, 'event'); | |
| 30 | + $query->groupBy('group_id'); | |
| 27 | 31 | |
| 28 | - $eventType = Arr::get($filters, 'event_type'); | |
| 32 | + $schedules = $query->paginate(); | |
| 29 | 33 | |
| 30 | - $query = Booking::with(['calendar_event']); | |
| 34 | + foreach ($schedules as $schedule) { | |
| 35 | + $this->formatBooking($schedule); | |
| 36 | + } | |
| 31 | 37 | |
| 32 | - $author = Arr::get($filters, 'author'); | |
| 38 | + $data = [ | |
| 39 | + 'schedules' => $schedules, | |
| 40 | + 'timezone' => 'UTC' | |
| 41 | + ]; | |
| 33 | 42 | |
| 34 | - if ($author !== 'all') { | |
| 35 | - $author = (int)$author; | |
| 43 | + $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle(); | |
| 44 | + | |
| 45 | + if ($author == 'me') { | |
| 46 | + $slotOptions = CalendarService::getSlotOptions(null, get_current_user_id()); | |
| 47 | + $data['slot_options'] = $slotOptions; | |
| 36 | 48 | } |
| 37 | 49 | |
| 38 | - if (!PermissionManager::userCanSeeAllBookings()) { | |
| 39 | - if (!$author || $author == 'all') { | |
| 40 | - $authorCalendar = Calendar::where('user_id', get_current_user_id()) | |
| 41 | - ->where('type', 'simple') | |
| 42 | - ->first(); | |
| 43 | - if($authorCalendar) { | |
| 44 | - $author = $authorCalendar->id; | |
| 45 | - } | |
| 46 | - } | |
| 50 | + if ($request->get('page') == 1) { | |
| 51 | + $this->addCountsForFirstPage($author, $data); | |
| 47 | 52 | } |
| 48 | 53 | |
| 49 | - if ($author && $author !== 'all') { | |
| 50 | - $query->where('calendar_id', $author); | |
| 54 | + return $data; | |
| 55 | + } | |
| 51 | 56 | |
| 52 | - if ($eventId && $eventId !== 'all') { | |
| 53 | - $query->where('event_id', $eventId); | |
| 54 | - } | |
| 57 | + public function export(Request $request) | |
| 58 | + { | |
| 59 | + $limit = (int) apply_filters('fluent_booking/data_export_limit', 2000); | |
| 55 | 60 | |
| 56 | - if ($eventType && $eventType !== 'all') { | |
| 57 | - $query->where('event_type', $eventType); | |
| 58 | - } | |
| 61 | + $author = $this->resolveAuthor($request); | |
| 62 | + | |
| 63 | + $query = $this->buildSchedulesQuery($request, $author); | |
| 64 | + | |
| 65 | + $relations = [ | |
| 66 | + 'calendar_event', | |
| 67 | + 'user', | |
| 68 | + 'booking_activities' => function ($q) { | |
| 69 | + $q->where('type', 'cancel_reason'); | |
| 70 | + }, | |
| 71 | + 'booking_meta', | |
| 72 | + ]; | |
| 73 | + | |
| 74 | + if (defined('FLUENT_BOOKING_PRO_DIR_FILE')) { | |
| 75 | + $relations[] = 'payment_order.transaction'; | |
| 59 | 76 | } |
| 60 | 77 | |
| 61 | - do_action_ref_array('fluent_booking/schedules_query', [&$query]); | |
| 78 | + $query->with($relations); | |
| 62 | 79 | |
| 63 | - $query->applyComputedStatus($period); | |
| 80 | + $total = (clone $query)->withoutEagerLoads()->count(); | |
| 81 | + $limited = $total > $limit; | |
| 64 | 82 | |
| 65 | - if ($period == 'upcoming') { | |
| 66 | - $query = $query->orderBy('start_time', 'ASC'); | |
| 67 | - } else if ($period == 'latest_bookings') { | |
| 68 | - $query = $query->orderBy('created_at', 'DESC'); | |
| 69 | - } else if ($period == 'no_show') { | |
| 70 | - $query = $query->orderBy('start_time', 'DESC'); | |
| 71 | - } else if ($period == 'latest_bookings') { | |
| 72 | - $query = $query->orderBy('id', 'DESC'); | |
| 83 | + $bookings = $query->take($limit)->get(); | |
| 84 | + | |
| 85 | + $rows = $bookings->map(function ($booking) { | |
| 86 | + $row = ExportHelper::mapBooking($booking); | |
| 87 | + return apply_filters('fluent_booking/booking_export_row', $row, $booking); | |
| 88 | + })->all(); | |
| 89 | + | |
| 90 | + $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings); | |
| 91 | + | |
| 92 | + return [ | |
| 93 | + 'bookings' => $rows, | |
| 94 | + 'limited' => $limited, | |
| 95 | + 'total' => $total, | |
| 96 | + ]; | |
| 97 | + } | |
| 98 | + | |
| 99 | + protected function resolveAuthor(Request $request) | |
| 100 | + { | |
| 101 | + $author = Arr::get($request->get('filters', []), 'author'); | |
| 102 | + | |
| 103 | + if (is_numeric($author)) { | |
| 104 | + $author = (int) $author; | |
| 73 | 105 | } else { |
| 74 | - $query = $query->orderBy('start_time', 'DESC'); | |
| 106 | + $author = sanitize_text_field($author); | |
| 75 | 107 | } |
| 76 | 108 | |
| 77 | - $query->groupBy('group_id'); | |
| 109 | + $hasPermission = PermissionManager::userCanSeeAllBookings(); | |
| 78 | 110 | |
| 79 | - $search = Arr::get($filters, 'search'); | |
| 111 | + if ($hasPermission) { | |
| 112 | + return $author; | |
| 113 | + } | |
| 80 | 114 | |
| 81 | - if (!empty($search)) { | |
| 82 | - $author = 'all'; | |
| 83 | - $query = $query->orderBy('start_time', 'DESC'); | |
| 84 | - $query = $query->searchBy($search); | |
| 115 | + if (!$author || $author == 'all') { | |
| 116 | + $authorCalendar = Calendar::where('user_id', get_current_user_id()) | |
| 117 | + ->where('type', 'simple') | |
| 118 | + ->first(); | |
| 119 | + | |
| 120 | + $author = $authorCalendar ? $authorCalendar->id : ''; | |
| 85 | 121 | } |
| 86 | 122 | |
| 87 | - $schedules = $query->paginate(); | |
| 123 | + return $author; | |
| 124 | + } | |
| 88 | 125 | |
| 89 | - foreach ($schedules as $schedule) { | |
| 90 | - $this->formatBooking($schedule); | |
| 126 | + protected function buildSchedulesQuery(Request $request, $author = null) | |
| 127 | + { | |
| 128 | + $filters = $request->get('filters', []); | |
| 129 | + $search = $request->getSafe('search'); | |
| 130 | + $eventVal = Arr::get($filters, 'event'); | |
| 131 | + $eventId = is_numeric($eventVal) ? (int) $eventVal : 0; | |
| 132 | + $eventType = sanitize_text_field(Arr::get($filters, 'event_type')); | |
| 133 | + $period = sanitize_text_field(Arr::get($filters, 'period', 'upcoming')); | |
| 134 | + $range = array_map('sanitize_text_field', Arr::get($filters, 'range', [])); | |
| 135 | + $email = sanitize_email(Arr::get($filters, 'email', '')); | |
| 136 | + | |
| 137 | + $allowedPeriods = ['upcoming', 'completed', 'cancelled', 'pending', 'no_show', 'latest_bookings', 'all']; | |
| 138 | + if (!in_array($period, $allowedPeriods, true)) { | |
| 139 | + $period = 'upcoming'; | |
| 91 | 140 | } |
| 92 | 141 | |
| 93 | - $data = [ | |
| 94 | - 'schedules' => $schedules, | |
| 95 | - 'timezone' => 'UTC' | |
| 96 | - ]; | |
| 142 | + $query = Booking::with(['calendar_event']); | |
| 97 | 143 | |
| 98 | - $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle(); | |
| 144 | + $hasPermission = PermissionManager::userCanSeeAllBookings(); | |
| 99 | 145 | |
| 100 | - if ($author && $author != 'all') { | |
| 101 | - $slotOptions = CalendarService::getSlotOptions($author); | |
| 102 | - $data['slot_options'] = $slotOptions; | |
| 146 | + if (!$hasPermission || $author == 'me') { | |
| 147 | + $query->where('host_user_id', get_current_user_id()); | |
| 103 | 148 | } |
| 104 | 149 | |
| 105 | - if ($request->get('page') == 1) { | |
| 106 | - $pendingQuery = Booking::query() | |
| 107 | - ->whereIn('status', ['pending', 'reserved']) | |
| 108 | - ->distinct('group_id'); | |
| 109 | - if ($author && $author !== 'all') { | |
| 110 | - $pendingCount = $pendingQuery->where('calendar_id', $author)->count('group_id'); | |
| 111 | - } else { | |
| 112 | - $pendingCount = $pendingQuery->count('group_id'); | |
| 150 | + if ($author && $author !== 'all') { | |
| 151 | + if ($author != 'me') { | |
| 152 | + $query->where('calendar_id', $author); | |
| 113 | 153 | } |
| 154 | + if ($eventId > 0) { | |
| 155 | + $query->where('event_id', $eventId); | |
| 156 | + } | |
| 157 | + if ($eventType && $eventType !== 'all') { | |
| 158 | + $query->where('event_type', $eventType); | |
| 159 | + } | |
| 160 | + } | |
| 114 | 161 | |
| 115 | - $data['no_show_count'] = Booking::where('status', 'no_show')->count(); | |
| 116 | - $data['pending_count'] = $pendingCount; | |
| 117 | - $data['cancelled_count'] = Booking::where('status', 'cancelled')->count(); | |
| 162 | + if (!empty($email) && is_email($email)) { | |
| 163 | + $query->where('email', $email); | |
| 118 | 164 | } |
| 119 | 165 | |
| 120 | - return $data; | |
| 166 | + do_action_ref_array('fluent_booking/schedules_query', [&$query]); | |
| 167 | + | |
| 168 | + $query->applyDateRangeFilter($range); | |
| 169 | + $query->applyComputedStatus($period); | |
| 170 | + $query->applyBookingOrderByStatus($period); | |
| 171 | + | |
| 172 | + if (!empty($search)) { | |
| 173 | + $query->searchBy($search); | |
| 174 | + } | |
| 175 | + | |
| 176 | + return $query; | |
| 121 | 177 | } |
| 122 | 178 | |
| 179 | + private function addCountsForFirstPage($author, &$data) | |
| 180 | + { | |
| 181 | + $bookingQuery = Booking::query() | |
| 182 | + ->when(!PermissionManager::userCanSeeAllBookings() || $author == 'me', function($query) { | |
| 183 | + return $query->where('host_user_id', get_current_user_id()); | |
| 184 | + }) | |
| 185 | + ->when($author && is_numeric($author), function($query) use ($author) { | |
| 186 | + return $query->where('calendar_id', $author); | |
| 187 | + }) | |
| 188 | + ->distinct('group_id'); | |
| 189 | + | |
| 190 | + $data['pending_count'] = (clone $bookingQuery)->whereIn('status', ['pending', 'reserved'])->count('group_id'); | |
| 191 | + $data['no_show_count'] = (clone $bookingQuery)->where('status', 'no_show')->count('group_id'); | |
| 192 | + $data['cancelled_count'] = (clone $bookingQuery)->where('status', 'cancelled')->count('group_id'); | |
| 193 | + } | |
| 194 | + | |
| 123 | 195 | public function patchBooking(Request $request, $bookingId) |
| 124 | 196 | { |
| 125 | 197 | $booking = Booking::findOrFail($bookingId); |
| 126 | 198 | $oldBooking = clone $booking; |
| @@ -188,23 +260,37 @@ | ||
| 188 | 260 | |
| 189 | 261 | if ($value == 'cancelled') { |
| 190 | 262 | $cancelReason = sanitize_text_field($data['cancel_reason']); |
| 191 | 263 | $booking->cancelMeeting($cancelReason, 'host', get_current_user_id()); |
| 192 | - return [ | |
| 193 | - 'message' => __('The booking has been cancelled', 'fluent-booking') | |
| 194 | - ]; | |
| 195 | 264 | } |
| 196 | 265 | |
| 197 | 266 | if ($value == 'rejected') { |
| 198 | 267 | $rejectReason = sanitize_text_field($data['reject_reason']); |
| 199 | 268 | $booking->rejectMeeting($rejectReason, get_current_user_id()); |
| 269 | + } | |
| 270 | + | |
| 271 | + if (in_array($value, ['cancelled', 'rejected'])) { | |
| 272 | + if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') { | |
| 273 | + do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event); | |
| 274 | + } | |
| 200 | 275 | return [ |
| 201 | - 'message' => __('The booking has been rejected', 'fluent-booking') | |
| 276 | + /* translators: %s: Booking status */ | |
| 277 | + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value) | |
| 202 | 278 | ]; |
| 203 | 279 | } |
| 204 | 280 | |
| 205 | - if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes' && in_array($value, ['cancelled', 'rejected'])) { | |
| 206 | - do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event); | |
| 281 | + $updateAll = Arr::isTrue($data, 'update_all') && $booking->isMultiGuestBooking(); | |
| 282 | + | |
| 283 | + if ($updateAll && in_array($value, ['no_show', 'completed'])) { | |
| 284 | + Booking::where('event_id', $booking->event_id) | |
| 285 | + ->where('group_id', $booking->group_id) | |
| 286 | + ->update([ | |
| 287 | + 'status' => $value | |
| 288 | + ]); | |
| 289 | + return [ | |
| 290 | + /* translators: %s: Booking status */ | |
| 291 | + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value) | |
| 292 | + ]; | |
| 207 | 293 | } |
| 208 | 294 | } |
| 209 | 295 | |
| 210 | 296 | if ($column == 'payment_status') { |
| @@ -218,8 +304,12 @@ | ||
| 218 | 304 | |
| 219 | 305 | if ($value == 'pending') { |
| 220 | 306 | do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id)); |
| 221 | 307 | } |
| 308 | + | |
| 309 | + if ($booking->payment_order) { | |
| 310 | + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value); | |
| 311 | + } | |
| 222 | 312 | } |
| 223 | 313 | |
| 224 | 314 | $updateData[$column] = $value; |
| 225 | 315 | $booking->fill($updateData); |
| @@ -249,11 +339,9 @@ | ||
| 249 | 339 | { |
| 250 | 340 | $booking = Booking::with('calendar_event'); |
| 251 | 341 | |
| 252 | 342 | if (!PermissionManager::userCanSeeAllBookings()) { |
| 253 | - $booking->whereHas('calendar', function ($q) { | |
| 254 | - $q->where('user_id', get_current_user_id()); | |
| 255 | - }); | |
| 343 | + $booking->whereHostAccess(get_current_user_id()); | |
| 256 | 344 | } |
| 257 | 345 | |
| 258 | 346 | $booking = $booking->findOrFail($bookingId); |
| 259 | 347 | $booking = $this->formatBooking($booking); |
| @@ -291,9 +379,9 @@ | ||
| 291 | 379 | } |
| 292 | 380 | |
| 293 | 381 | public function sendConfirmationEmail(Request $request, $bookingId) |
| 294 | 382 | { |
| 295 | - $booking = Booking::with(['calendar', 'calendar_event'])->find($bookingId); | |
| 383 | + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId); | |
| 296 | 384 | |
| 297 | 385 | $emailTo = $request->get('email_to', 'guest'); |
| 298 | 386 | |
| 299 | 387 | $notifications = $booking->calendar_event->getNotifications(); |
| @@ -318,11 +406,9 @@ | ||
| 318 | 406 | { |
| 319 | 407 | $booking = Booking::with('slot'); |
| 320 | 408 | |
| 321 | 409 | if (!PermissionManager::userCanSeeAllBookings()) { |
| 322 | - $booking->whereHas('calendar', function ($q) { | |
| 323 | - $q->where('user_id', get_current_user_id()); | |
| 324 | - }); | |
| 410 | + $booking->whereHostAccess(get_current_user_id()); | |
| 325 | 411 | } |
| 326 | 412 | |
| 327 | 413 | $booking = $booking->where('group_id', $groupId)->first(); |
| 328 | 414 | |
| @@ -330,9 +416,9 @@ | ||
| 330 | 416 | return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]); |
| 331 | 417 | } |
| 332 | 418 | |
| 333 | 419 | $attendees = Booking::where('group_id', $booking->group_id); |
| 334 | - $search = sanitize_text_field($request->get('search')); | |
| 420 | + $search = $request->getSafe('search'); | |
| 335 | 421 | |
| 336 | 422 | if (!empty($search)) { |
| 337 | 423 | $attendees = $attendees->searchBy($search); |
| 338 | 424 | } |
| @@ -348,8 +434,10 @@ | ||
| 348 | 434 | } |
| 349 | 435 | |
| 350 | 436 | public function getBookingActivities(Request $request, $bookingId) |
| 351 | 437 | { |
| 438 | + $this->resolveOwnedBookingOrFail($bookingId); | |
| 439 | + | |
| 352 | 440 | $activities = BookingActivity::where('booking_id', $bookingId) |
| 353 | 441 | ->orderBy('id', 'DESC') |
| 354 | 442 | ->get(); |
| 355 | 443 | |
| @@ -359,32 +447,52 @@ | ||
| 359 | 447 | } |
| 360 | 448 | |
| 361 | 449 | public function getBookingMetaInfo(Request $request, $bookingId) |
| 362 | 450 | { |
| 363 | - $booking = Booking::findOrFail($bookingId); | |
| 451 | + $booking = $this->resolveOwnedBookingOrFail($bookingId); | |
| 364 | 452 | |
| 365 | 453 | $activities = BookingActivity::where('booking_id', $booking->id) |
| 366 | 454 | ->orderBy('id', 'DESC') |
| 367 | 455 | ->get(); |
| 456 | + | |
| 457 | + $activities->each(function ($activity) { | |
| 458 | + $activity->description = wp_unslash($activity->description); | |
| 459 | + }); | |
| 368 | 460 | |
| 369 | 461 | $sidebarContents = []; |
| 370 | 462 | $mainBodyContents = []; |
| 371 | 463 | |
| 372 | - if (defined('FLUENTCRM')) { | |
| 373 | - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false); | |
| 374 | - if ($profileHtml) { | |
| 375 | - $sidebarContents[] = [ | |
| 376 | - 'id' => 'fluent_crm_profule', | |
| 377 | - 'title' => __('CRM Profile', 'fluent-booking'), | |
| 378 | - 'content' => $profileHtml | |
| 379 | - ]; | |
| 380 | - } | |
| 464 | + $canReadCrm = CrmContactService::isActive() && CrmPermissionManager::currentUserCan('fcrm_read_contacts'); | |
| 465 | + $crmProfile = $canReadCrm ? CrmContactService::getProfileData($booking->email) : null; | |
| 466 | + if ($crmProfile) { | |
| 467 | + $sidebarContents[] = [ | |
| 468 | + 'id' => 'fluent_crm_profule', | |
| 469 | + 'title' => __('CRM Profile', 'fluent-booking'), | |
| 470 | + 'type' => 'crm_profile', | |
| 471 | + 'profile' => $crmProfile, | |
| 472 | + ]; | |
| 381 | 473 | } |
| 382 | 474 | |
| 475 | + $cartProfile = CustomerProfileService::canView() | |
| 476 | + ? CustomerProfileService::getProfileData($booking->email) | |
| 477 | + : null; | |
| 478 | + if ($cartProfile) { | |
| 479 | + $sidebarContents[] = [ | |
| 480 | + 'id' => 'fluent_cart_profile', | |
| 481 | + 'title' => __('Cart Profile', 'fluent-booking'), | |
| 482 | + 'type' => 'cart_profile', | |
| 483 | + 'profile' => $cartProfile, | |
| 484 | + ]; | |
| 485 | + } | |
| 486 | + | |
| 383 | 487 | $order = null; |
| 384 | - if ($booking->payment_method && $booking->payment_order) { | |
| 488 | + if ($booking->payment_status && $booking->payment_order) { | |
| 385 | 489 | $order = $booking->payment_order; |
| 386 | - $order->load(['items', 'transaction']); | |
| 490 | + $relations = ['items', 'transaction']; | |
| 491 | + if (method_exists($order, 'discounts')) { | |
| 492 | + $relations[] = 'discounts'; | |
| 493 | + } | |
| 494 | + $order->load($relations); | |
| 387 | 495 | $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency); |
| 388 | 496 | } |
| 389 | 497 | |
| 390 | 498 | $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking); |
| @@ -397,8 +505,198 @@ | ||
| 397 | 505 | 'main_body_contents' => $mainBodyContents |
| 398 | 506 | ]; |
| 399 | 507 | } |
| 400 | 508 | |
| 509 | + /** | |
| 510 | + * Return the CRM contact state for a booking plus the full tag/list option | |
| 511 | + * sets, so the admin can manage tags/lists inline from the CRM Profile card. | |
| 512 | + */ | |
| 513 | + public function getCrmContact(Request $request, $bookingId) | |
| 514 | + { | |
| 515 | + $booking = $this->resolveOwnedBookingOrFail($bookingId); | |
| 516 | + | |
| 517 | + if (!CrmContactService::isActive()) { | |
| 518 | + return $this->sendError([ | |
| 519 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 520 | + ]); | |
| 521 | + } | |
| 522 | + | |
| 523 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 524 | + return $this->sendError([ | |
| 525 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 526 | + ], 403); | |
| 527 | + } | |
| 528 | + | |
| 529 | + $state = CrmContactService::getContactState($booking->email); | |
| 530 | + | |
| 531 | + if (!$state) { | |
| 532 | + return $this->sendError([ | |
| 533 | + 'message' => __('No CRM contact found for this booking.', 'fluent-booking') | |
| 534 | + ], 404); | |
| 535 | + } | |
| 536 | + | |
| 537 | + return $state; | |
| 538 | + } | |
| 539 | + | |
| 540 | + /** | |
| 541 | + * Guest-field prefill for the CRM "Book Appointment" action. | |
| 542 | + * Keyed by contact id, so it carries its own CRM-read gate. | |
| 543 | + */ | |
| 544 | + public function getCrmContactPrefill(Request $request) | |
| 545 | + { | |
| 546 | + if (!CrmContactService::isActive()) { | |
| 547 | + return $this->sendError([ | |
| 548 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 549 | + ]); | |
| 550 | + } | |
| 551 | + | |
| 552 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 553 | + return $this->sendError([ | |
| 554 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 555 | + ], 403); | |
| 556 | + } | |
| 557 | + | |
| 558 | + $contactId = absint($request->get('contact_id')); | |
| 559 | + | |
| 560 | + if (!$contactId) { | |
| 561 | + return $this->sendError([ | |
| 562 | + 'message' => __('Invalid contact.', 'fluent-booking') | |
| 563 | + ], 422); | |
| 564 | + } | |
| 565 | + | |
| 566 | + $prefill = CrmContactService::getBookingPrefillData($contactId); | |
| 567 | + | |
| 568 | + if (!$prefill) { | |
| 569 | + return $this->sendError([ | |
| 570 | + 'message' => __('No CRM contact found.', 'fluent-booking') | |
| 571 | + ], 404); | |
| 572 | + } | |
| 573 | + | |
| 574 | + return [ | |
| 575 | + 'prefill' => $prefill, | |
| 576 | + ]; | |
| 577 | + } | |
| 578 | + | |
| 579 | + /** | |
| 580 | + * Typeahead search for the admin booking modal's CRM contact picker. | |
| 581 | + * Same intersection gate as getCrmContactPrefill: booking access (policy) AND fcrm_read_contacts. | |
| 582 | + */ | |
| 583 | + public function searchCrmContacts(Request $request) | |
| 584 | + { | |
| 585 | + if (!CrmContactService::isActive()) { | |
| 586 | + return $this->sendError([ | |
| 587 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 588 | + ]); | |
| 589 | + } | |
| 590 | + | |
| 591 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 592 | + return $this->sendError([ | |
| 593 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 594 | + ], 403); | |
| 595 | + } | |
| 596 | + | |
| 597 | + $search = sanitize_text_field($request->get('search', '')); | |
| 598 | + | |
| 599 | + return [ | |
| 600 | + 'contacts' => CrmContactService::searchContacts($search), | |
| 601 | + ]; | |
| 602 | + } | |
| 603 | + | |
| 604 | + /** | |
| 605 | + * Bounded, searchable tag/list options for the CRM Profile picker. | |
| 606 | + */ | |
| 607 | + public function getCrmOptions(Request $request, $bookingId) | |
| 608 | + { | |
| 609 | + $this->resolveOwnedBookingOrFail($bookingId); | |
| 610 | + | |
| 611 | + if (!CrmContactService::isActive()) { | |
| 612 | + return $this->sendError([ | |
| 613 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 614 | + ]); | |
| 615 | + } | |
| 616 | + | |
| 617 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 618 | + return $this->sendError([ | |
| 619 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 620 | + ], 403); | |
| 621 | + } | |
| 622 | + | |
| 623 | + $type = $request->get('type') === 'lists' ? 'lists' : 'tags'; | |
| 624 | + $search = sanitize_text_field($request->get('search', '')); | |
| 625 | + | |
| 626 | + return [ | |
| 627 | + 'options' => CrmContactService::getOptions($type, $search), | |
| 628 | + ]; | |
| 629 | + } | |
| 630 | + | |
| 631 | + public function updateCrmTags(Request $request, $bookingId) | |
| 632 | + { | |
| 633 | + return $this->updateCrmTaxonomy($request, $bookingId, 'tags'); | |
| 634 | + } | |
| 635 | + | |
| 636 | + public function updateCrmLists(Request $request, $bookingId) | |
| 637 | + { | |
| 638 | + return $this->updateCrmTaxonomy($request, $bookingId, 'lists'); | |
| 639 | + } | |
| 640 | + | |
| 641 | + private function updateCrmTaxonomy(Request $request, $bookingId, $type) | |
| 642 | + { | |
| 643 | + $booking = $this->resolveOwnedBookingOrFail($bookingId); | |
| 644 | + | |
| 645 | + if (!CrmContactService::isActive()) { | |
| 646 | + return $this->sendError([ | |
| 647 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 648 | + ]); | |
| 649 | + } | |
| 650 | + | |
| 651 | + if (!CrmPermissionManager::currentUserCan('fcrm_manage_contacts')) { | |
| 652 | + return $this->sendError([ | |
| 653 | + 'message' => __('You do not have permission to manage CRM contacts.', 'fluent-booking') | |
| 654 | + ], 403); | |
| 655 | + } | |
| 656 | + | |
| 657 | + $requestKey = $type === 'tags' ? 'tag_ids' : 'list_ids'; | |
| 658 | + $desired = (array) $request->get($requestKey, []); | |
| 659 | + | |
| 660 | + $result = CrmContactService::syncTaxonomy($booking->email, $type, $desired); | |
| 661 | + | |
| 662 | + if ($result === null) { | |
| 663 | + return $this->sendError([ | |
| 664 | + 'message' => __('No CRM contact found for this booking.', 'fluent-booking') | |
| 665 | + ], 404); | |
| 666 | + } | |
| 667 | + | |
| 668 | + return $this->sendSuccess(array_merge([ | |
| 669 | + 'message' => __('CRM contact updated successfully.', 'fluent-booking'), | |
| 670 | + ], $result)); | |
| 671 | + } | |
| 672 | + | |
| 673 | + private function resolveOwnedBookingOrFail($bookingId) | |
| 674 | + { | |
| 675 | + $booking = Booking::findOrFail($bookingId); | |
| 676 | + | |
| 677 | + if (PermissionManager::userCanSeeAllBookings()) { | |
| 678 | + return $booking; | |
| 679 | + } | |
| 680 | + | |
| 681 | + $allowedIds = $booking->getHostIds(); | |
| 682 | + | |
| 683 | + if (PermissionManager::userCan('manage_own_calendar')) { | |
| 684 | + if ($event = CalendarSlot::find($booking->event_id)) { | |
| 685 | + $allowedIds = array_merge($allowedIds, $event->getHostIds()); | |
| 686 | + } | |
| 687 | + } | |
| 688 | + | |
| 689 | + if (in_array(get_current_user_id(), $allowedIds)) { | |
| 690 | + return $booking; | |
| 691 | + } | |
| 692 | + | |
| 693 | + $exception = new ModelNotFoundException(); | |
| 694 | + $exception->setModel(Booking::class, [$bookingId]); | |
| 695 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped | |
| 696 | + throw $exception; | |
| 697 | + } | |
| 698 | + | |
| 401 | 699 | private function formatBooking(&$booking) |
| 402 | 700 | { |
| 403 | 701 | $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes |
| 404 | 702 | |
| @@ -405,9 +703,10 @@ | ||
| 405 | 703 | if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) { |
| 406 | 704 | $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed'; |
| 407 | 705 | $booking->status = $bookingStatus; |
| 408 | 706 | $booking->save(); |
| 409 | - do_action('fluent_booking/booking_schedule_' . $bookingStatus, $booking, $booking->calendar_event); | |
| 707 | + $hookName = $bookingStatus === 'cancelled' ? 'auto_cancelled' : $bookingStatus; | |
| 708 | + do_action('fluent_booking/booking_schedule_' . $hookName, $booking, $booking->calendar_event); | |
| 410 | 709 | } |
| 411 | 710 | |
| 412 | 711 | if ($booking->isMultiHostBooking()) { |
| 413 | 712 | $booking->host_profiles = $booking->getHostProfiles(); |
| @@ -421,8 +720,9 @@ | ||
| 421 | 720 | } |
| 422 | 721 | |
| 423 | 722 | $booking->title = $booking->getBookingTitle(true); |
| 424 | 723 | $booking->author = $booking->getHostDetails(false); |
| 724 | + $booking->details = $booking->getConfirmationData(true); | |
| 425 | 725 | $booking->location = $booking->getLocationDetailsHtml(); |
| 426 | 726 | $booking->reschedule_url = $booking->getRescheduleUrl(); |
| 427 | 727 | $booking->happening_status = $booking->getOngoingStatus(); |
| 428 | 728 | $booking->booking_status_text = $booking->getBookingStatus(); |
| @@ -433,8 +733,19 @@ | ||
| 433 | 733 | |
| 434 | 734 | return $booking; |
| 435 | 735 | } |
| 436 | 736 | |
| 737 | + private function getConfirmedBy() | |
| 738 | + { | |
| 739 | + $confirmedBy = 'host'; | |
| 740 | + $userId = get_current_user_id(); | |
| 741 | + if ($userId && $user = get_user_by('ID', $userId)) { | |
| 742 | + $confirmedBy = $user->display_name; | |
| 743 | + } | |
| 744 | + | |
| 745 | + return $confirmedBy; | |
| 746 | + } | |
| 747 | + | |
| 437 | 748 | private function getPaymentPaidLog($bookingId) |
| 438 | 749 | { |
| 439 | 750 | return [ |
| 440 | 751 | 'booking_id' => $bookingId, |
| @@ -440,9 +751,9 @@ | ||
| 440 | 751 | 'booking_id' => $bookingId, |
| 441 | 752 | 'status' => 'closed', |
| 442 | 753 | 'type' => 'success', |
| 443 | 754 | 'title' => __('Payment Successfully Completed', 'fluent-booking'), |
| 444 | - 'description' => __('Payment marked as paid by admin', 'fluent-booking') | |
| 755 | + 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy() | |
| 445 | 756 | ]; |
| 446 | 757 | } |
| 447 | 758 | |
| 448 | 759 | private function getPaymentPendingLog($bookingId) |
| @@ -451,25 +762,19 @@ | ||
| 451 | 762 | 'booking_id' => $bookingId, |
| 452 | 763 | 'status' => 'closed', |
| 453 | 764 | 'type' => 'success', |
| 454 | 765 | 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'), |
| 455 | - 'description' => __('Payment marked as pending by admin', 'fluent-booking') | |
| 766 | + 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy() | |
| 456 | 767 | ]; |
| 457 | 768 | } |
| 458 | 769 | |
| 459 | 770 | private function getConfirmLog($bookingId) |
| 460 | 771 | { |
| 461 | - $confirmedBy = 'host'; | |
| 462 | - $userId = get_current_user_id(); | |
| 463 | - if ($userId && $user = get_user_by('ID', $userId)) { | |
| 464 | - $confirmedBy = $user->display_name; | |
| 465 | - } | |
| 466 | - | |
| 467 | 772 | return [ |
| 468 | 773 | 'booking_id' => $bookingId, |
| 469 | 774 | 'status' => 'closed', |
| 470 | 775 | 'type' => 'success', |
| 471 | 776 | 'title' => __('Booking Confirmed', 'fluent-booking'), |
| 472 | - 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $confirmedBy | |
| 777 | + 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy() | |
| 473 | 778 | ]; |
| 474 | 779 | } |
| 475 | 780 | } |