| @@ -14,8 +14,11 @@ | ||
| 14 | 14 | use FluentBooking\Framework\Http\Request\Request; |
| 15 | 15 | use FluentBooking\App\Services\PermissionManager; |
| 16 | 16 | use FluentBooking\App\Services\CalendarService; |
| 17 | 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; | |
| 18 | 21 | |
| 19 | 22 | class SchedulesController extends Controller |
| 20 | 23 | { |
| 21 | 24 | public function index(Request $request) |
| @@ -138,9 +141,9 @@ | ||
| 138 | 141 | |
| 139 | 142 | $query = Booking::with(['calendar_event']); |
| 140 | 143 | |
| 141 | 144 | $hasPermission = PermissionManager::userCanSeeAllBookings(); |
| 142 | - | |
| 145 | + | |
| 143 | 146 | if (!$hasPermission || $author == 'me') { |
| 144 | 147 | $query->where('host_user_id', get_current_user_id()); |
| 145 | 148 | } |
| 146 | 149 | |
| @@ -175,9 +178,9 @@ | ||
| 175 | 178 | |
| 176 | 179 | private function addCountsForFirstPage($author, &$data) |
| 177 | 180 | { |
| 178 | 181 | $bookingQuery = Booking::query() |
| 179 | - ->when($author == 'me', function($query) { | |
| 182 | + ->when(!PermissionManager::userCanSeeAllBookings() || $author == 'me', function($query) { | |
| 180 | 183 | return $query->where('host_user_id', get_current_user_id()); |
| 181 | 184 | }) |
| 182 | 185 | ->when($author && is_numeric($author), function($query) use ($author) { |
| 183 | 186 | return $query->where('calendar_id', $author); |
| @@ -237,10 +240,19 @@ | ||
| 237 | 240 | if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) { |
| 238 | 241 | return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]); |
| 239 | 242 | } |
| 240 | 243 | |
| 244 | + if (in_array($booking->status, ['cancelled', 'rejected'])) { | |
| 245 | + return $this->sendError(['message' => __('A cancelled or rejected booking can not be changed', 'fluent-booking')]); | |
| 246 | + } | |
| 247 | + | |
| 241 | 248 | if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) { |
| 242 | 249 | $order = $booking->payment_order; |
| 250 | + | |
| 251 | + if (in_array($order->status, ['refunded', 'partially-refunded'])) { | |
| 252 | + return $this->sendError(['message' => __('A refunded payment can not be marked as paid', 'fluent-booking')]); | |
| 253 | + } | |
| 254 | + | |
| 243 | 255 | $order->total_paid = $order->total_amount; |
| 244 | 256 | $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 245 | 257 | $order->status = 'paid'; |
| 246 | 258 | $order->save(); |
| @@ -255,18 +267,24 @@ | ||
| 255 | 267 | do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id)); |
| 256 | 268 | } |
| 257 | 269 | |
| 258 | 270 | if ($value == 'cancelled') { |
| 259 | - $cancelReason = sanitize_text_field($data['cancel_reason']); | |
| 271 | + $cancelReason = sanitize_text_field(Arr::get($data, 'cancel_reason', '')); | |
| 260 | 272 | $booking->cancelMeeting($cancelReason, 'host', get_current_user_id()); |
| 261 | 273 | } |
| 262 | 274 | |
| 263 | 275 | if ($value == 'rejected') { |
| 264 | - $rejectReason = sanitize_text_field($data['reject_reason']); | |
| 276 | + $rejectReason = sanitize_text_field(Arr::get($data, 'reject_reason', '')); | |
| 265 | 277 | $booking->rejectMeeting($rejectReason, get_current_user_id()); |
| 266 | 278 | } |
| 267 | 279 | |
| 268 | 280 | if (in_array($value, ['cancelled', 'rejected'])) { |
| 281 | + // cancelMeeting() and rejectMeeting() refuse some statuses without changing the booking. | |
| 282 | + if ($booking->status != $value) { | |
| 283 | + /* translators: %s: Booking status */ | |
| 284 | + return $this->sendError(['message' => sprintf(__('This booking can not be %s', 'fluent-booking'), $value)]); | |
| 285 | + } | |
| 286 | + | |
| 269 | 287 | if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') { |
| 270 | 288 | do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event); |
| 271 | 289 | } |
| 272 | 290 | return [ |
| @@ -336,11 +354,9 @@ | ||
| 336 | 354 | { |
| 337 | 355 | $booking = Booking::with('calendar_event'); |
| 338 | 356 | |
| 339 | 357 | if (!PermissionManager::userCanSeeAllBookings()) { |
| 340 | - $booking->whereHas('calendar', function ($q) { | |
| 341 | - $q->where('user_id', get_current_user_id()); | |
| 342 | - }); | |
| 358 | + $booking->whereHostAccess(get_current_user_id()); | |
| 343 | 359 | } |
| 344 | 360 | |
| 345 | 361 | $booking = $booking->findOrFail($bookingId); |
| 346 | 362 | $booking = $this->formatBooking($booking); |
| @@ -405,11 +421,9 @@ | ||
| 405 | 421 | { |
| 406 | 422 | $booking = Booking::with('slot'); |
| 407 | 423 | |
| 408 | 424 | if (!PermissionManager::userCanSeeAllBookings()) { |
| 409 | - $booking->whereHas('calendar', function ($q) { | |
| 410 | - $q->where('user_id', get_current_user_id()); | |
| 411 | - }); | |
| 425 | + $booking->whereHostAccess(get_current_user_id()); | |
| 412 | 426 | } |
| 413 | 427 | |
| 414 | 428 | $booking = $booking->where('group_id', $groupId)->first(); |
| 415 | 429 | |
| @@ -438,8 +452,9 @@ | ||
| 438 | 452 | { |
| 439 | 453 | $this->resolveOwnedBookingOrFail($bookingId); |
| 440 | 454 | |
| 441 | 455 | $activities = BookingActivity::where('booking_id', $bookingId) |
| 456 | + ->where('type', '!=', BookingActivity::TYPE_NOTE) | |
| 442 | 457 | ->orderBy('id', 'DESC') |
| 443 | 458 | ->get(); |
| 444 | 459 | |
| 445 | 460 | return [ |
| @@ -451,11 +466,12 @@ | ||
| 451 | 466 | { |
| 452 | 467 | $booking = $this->resolveOwnedBookingOrFail($bookingId); |
| 453 | 468 | |
| 454 | 469 | $activities = BookingActivity::where('booking_id', $booking->id) |
| 470 | + ->where('type', '!=', BookingActivity::TYPE_NOTE) | |
| 455 | 471 | ->orderBy('id', 'DESC') |
| 456 | 472 | ->get(); |
| 457 | - | |
| 473 | + | |
| 458 | 474 | $activities->each(function ($activity) { |
| 459 | 475 | $activity->description = wp_unslash($activity->description); |
| 460 | 476 | }); |
| 461 | 477 | |
| @@ -461,19 +477,31 @@ | ||
| 461 | 477 | |
| 462 | 478 | $sidebarContents = []; |
| 463 | 479 | $mainBodyContents = []; |
| 464 | 480 | |
| 465 | - if (defined('FLUENTCRM')) { | |
| 466 | - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false); | |
| 467 | - if ($profileHtml) { | |
| 468 | - $sidebarContents[] = [ | |
| 469 | - 'id' => 'fluent_crm_profule', | |
| 470 | - 'title' => __('CRM Profile', 'fluent-booking'), | |
| 471 | - 'content' => $profileHtml | |
| 472 | - ]; | |
| 473 | - } | |
| 481 | + $canReadCrm = CrmContactService::isActive() && CrmPermissionManager::currentUserCan('fcrm_read_contacts'); | |
| 482 | + $crmProfile = $canReadCrm ? CrmContactService::getProfileData($booking->email) : null; | |
| 483 | + if ($crmProfile) { | |
| 484 | + $sidebarContents[] = [ | |
| 485 | + 'id' => 'fluent_crm_profule', | |
| 486 | + 'title' => __('CRM Profile', 'fluent-booking'), | |
| 487 | + 'type' => 'crm_profile', | |
| 488 | + 'profile' => $crmProfile, | |
| 489 | + ]; | |
| 474 | 490 | } |
| 475 | 491 | |
| 492 | + $cartProfile = CustomerProfileService::canView() | |
| 493 | + ? CustomerProfileService::getProfileData($booking->email) | |
| 494 | + : null; | |
| 495 | + if ($cartProfile) { | |
| 496 | + $sidebarContents[] = [ | |
| 497 | + 'id' => 'fluent_cart_profile', | |
| 498 | + 'title' => __('Cart Profile', 'fluent-booking'), | |
| 499 | + 'type' => 'cart_profile', | |
| 500 | + 'profile' => $cartProfile, | |
| 501 | + ]; | |
| 502 | + } | |
| 503 | + | |
| 476 | 504 | $order = null; |
| 477 | 505 | if ($booking->payment_status && $booking->payment_order) { |
| 478 | 506 | $order = $booking->payment_order; |
| 479 | 507 | $relations = ['items', 'transaction']; |
| @@ -492,8 +520,172 @@ | ||
| 492 | 520 | 'sidebar_contents' => $sidebarContents, |
| 493 | 521 | 'payment_order' => $order, |
| 494 | 522 | 'main_body_contents' => $mainBodyContents |
| 495 | 523 | ]; |
| 524 | + } | |
| 525 | + | |
| 526 | + /** | |
| 527 | + * Return the CRM contact state for a booking plus the full tag/list option | |
| 528 | + * sets, so the admin can manage tags/lists inline from the CRM Profile card. | |
| 529 | + */ | |
| 530 | + public function getCrmContact(Request $request, $bookingId) | |
| 531 | + { | |
| 532 | + $booking = $this->resolveOwnedBookingOrFail($bookingId); | |
| 533 | + | |
| 534 | + if (!CrmContactService::isActive()) { | |
| 535 | + return $this->sendError([ | |
| 536 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 537 | + ]); | |
| 538 | + } | |
| 539 | + | |
| 540 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 541 | + return $this->sendError([ | |
| 542 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 543 | + ], 403); | |
| 544 | + } | |
| 545 | + | |
| 546 | + $state = CrmContactService::getContactState($booking->email); | |
| 547 | + | |
| 548 | + if (!$state) { | |
| 549 | + return $this->sendError([ | |
| 550 | + 'message' => __('No CRM contact found for this booking.', 'fluent-booking') | |
| 551 | + ], 404); | |
| 552 | + } | |
| 553 | + | |
| 554 | + return $state; | |
| 555 | + } | |
| 556 | + | |
| 557 | + /** | |
| 558 | + * Guest-field prefill for the CRM "Book Appointment" action. | |
| 559 | + * Keyed by contact id, so it carries its own CRM-read gate. | |
| 560 | + */ | |
| 561 | + public function getCrmContactPrefill(Request $request) | |
| 562 | + { | |
| 563 | + if (!CrmContactService::isActive()) { | |
| 564 | + return $this->sendError([ | |
| 565 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 566 | + ]); | |
| 567 | + } | |
| 568 | + | |
| 569 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 570 | + return $this->sendError([ | |
| 571 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 572 | + ], 403); | |
| 573 | + } | |
| 574 | + | |
| 575 | + $contactId = absint($request->get('contact_id')); | |
| 576 | + | |
| 577 | + if (!$contactId) { | |
| 578 | + return $this->sendError([ | |
| 579 | + 'message' => __('Invalid contact.', 'fluent-booking') | |
| 580 | + ], 422); | |
| 581 | + } | |
| 582 | + | |
| 583 | + $prefill = CrmContactService::getBookingPrefillData($contactId); | |
| 584 | + | |
| 585 | + if (!$prefill) { | |
| 586 | + return $this->sendError([ | |
| 587 | + 'message' => __('No CRM contact found.', 'fluent-booking') | |
| 588 | + ], 404); | |
| 589 | + } | |
| 590 | + | |
| 591 | + return [ | |
| 592 | + 'prefill' => $prefill, | |
| 593 | + ]; | |
| 594 | + } | |
| 595 | + | |
| 596 | + /** | |
| 597 | + * Typeahead search for the admin booking modal's CRM contact picker. | |
| 598 | + * Same intersection gate as getCrmContactPrefill: booking access (policy) AND fcrm_read_contacts. | |
| 599 | + */ | |
| 600 | + public function searchCrmContacts(Request $request) | |
| 601 | + { | |
| 602 | + if (!CrmContactService::isActive()) { | |
| 603 | + return $this->sendError([ | |
| 604 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 605 | + ]); | |
| 606 | + } | |
| 607 | + | |
| 608 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 609 | + return $this->sendError([ | |
| 610 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 611 | + ], 403); | |
| 612 | + } | |
| 613 | + | |
| 614 | + $search = sanitize_text_field($request->get('search', '')); | |
| 615 | + | |
| 616 | + return [ | |
| 617 | + 'contacts' => CrmContactService::searchContacts($search), | |
| 618 | + ]; | |
| 619 | + } | |
| 620 | + | |
| 621 | + /** | |
| 622 | + * Bounded, searchable tag/list options for the CRM Profile picker. | |
| 623 | + */ | |
| 624 | + public function getCrmOptions(Request $request, $bookingId) | |
| 625 | + { | |
| 626 | + $this->resolveOwnedBookingOrFail($bookingId); | |
| 627 | + | |
| 628 | + if (!CrmContactService::isActive()) { | |
| 629 | + return $this->sendError([ | |
| 630 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 631 | + ]); | |
| 632 | + } | |
| 633 | + | |
| 634 | + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) { | |
| 635 | + return $this->sendError([ | |
| 636 | + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking') | |
| 637 | + ], 403); | |
| 638 | + } | |
| 639 | + | |
| 640 | + $type = $request->get('type') === 'lists' ? 'lists' : 'tags'; | |
| 641 | + $search = sanitize_text_field($request->get('search', '')); | |
| 642 | + | |
| 643 | + return [ | |
| 644 | + 'options' => CrmContactService::getOptions($type, $search), | |
| 645 | + ]; | |
| 646 | + } | |
| 647 | + | |
| 648 | + public function updateCrmTags(Request $request, $bookingId) | |
| 649 | + { | |
| 650 | + return $this->updateCrmTaxonomy($request, $bookingId, 'tags'); | |
| 651 | + } | |
| 652 | + | |
| 653 | + public function updateCrmLists(Request $request, $bookingId) | |
| 654 | + { | |
| 655 | + return $this->updateCrmTaxonomy($request, $bookingId, 'lists'); | |
| 656 | + } | |
| 657 | + | |
| 658 | + private function updateCrmTaxonomy(Request $request, $bookingId, $type) | |
| 659 | + { | |
| 660 | + $booking = $this->resolveOwnedBookingOrFail($bookingId); | |
| 661 | + | |
| 662 | + if (!CrmContactService::isActive()) { | |
| 663 | + return $this->sendError([ | |
| 664 | + 'message' => __('FluentCRM is not active.', 'fluent-booking') | |
| 665 | + ]); | |
| 666 | + } | |
| 667 | + | |
| 668 | + if (!CrmPermissionManager::currentUserCan('fcrm_manage_contacts')) { | |
| 669 | + return $this->sendError([ | |
| 670 | + 'message' => __('You do not have permission to manage CRM contacts.', 'fluent-booking') | |
| 671 | + ], 403); | |
| 672 | + } | |
| 673 | + | |
| 674 | + $requestKey = $type === 'tags' ? 'tag_ids' : 'list_ids'; | |
| 675 | + $desired = (array) $request->get($requestKey, []); | |
| 676 | + | |
| 677 | + $result = CrmContactService::syncTaxonomy($booking->email, $type, $desired); | |
| 678 | + | |
| 679 | + if ($result === null) { | |
| 680 | + return $this->sendError([ | |
| 681 | + 'message' => __('No CRM contact found for this booking.', 'fluent-booking') | |
| 682 | + ], 404); | |
| 683 | + } | |
| 684 | + | |
| 685 | + return $this->sendSuccess(array_merge([ | |
| 686 | + 'message' => __('CRM contact updated successfully.', 'fluent-booking'), | |
| 687 | + ], $result)); | |
| 496 | 688 | } |
| 497 | 689 | |
| 498 | 690 | private function resolveOwnedBookingOrFail($bookingId) |
| 499 | 691 | { |