| @@ -104,8 +104,15 @@ | ||
| 104 | 104 | ], |
| 105 | 105 | ], |
| 106 | 106 | ]); |
| 107 | 107 | |
| 108 | + // Bulk operations | |
| 109 | + register_rest_route($this->namespace, "/{$this->rest_base}/bulk", [ | |
| 110 | + 'methods' => \WP_REST_Server::CREATABLE, | |
| 111 | + 'callback' => [$this, 'bulkAction'], | |
| 112 | + 'permission_callback' => [$this, 'check_permission'], | |
| 113 | + ]); | |
| 114 | + | |
| 108 | 115 | register_rest_route($this->namespace, "/{$this->rest_base}/orders", [ |
| 109 | 116 | 'methods' => 'POST', |
| 110 | 117 | 'callback' => [$this, 'update_display_orders'], |
| 111 | 118 | 'permission_callback' => [$this, 'check_permission'], |
| @@ -353,8 +360,10 @@ | ||
| 353 | 360 | $attribute->field_options = $metadata['field_options'] ?? null; |
| 354 | 361 | $attribute->validation_rules = $metadata['validation_rules'] ?? null; |
| 355 | 362 | } |
| 356 | 363 | } |
| 364 | + | |
| 365 | + $this->attachDecodedIconToAttribute($attribute); | |
| 357 | 366 | |
| 358 | 367 | return $this->success_response($attribute, 201); |
| 359 | 368 | } |
| 360 | 369 | |
| @@ -397,39 +406,9 @@ | ||
| 397 | 406 | $attribute->validation_rules = $metadata['validation_rules'] ?? null; |
| 398 | 407 | } |
| 399 | 408 | } |
| 400 | 409 | |
| 401 | - // Process icon field - unserialize if it's serialized | |
| 402 | - if (!empty($attribute->icon)) { | |
| 403 | - $icon_data = maybe_unserialize($attribute->icon); | |
| 404 | - if (is_array($icon_data)) { | |
| 405 | - // Resolve image URLs for image type icons | |
| 406 | - if ($icon_data['type'] === 'image' && !empty($icon_data['value'])) { | |
| 407 | - $value = $icon_data['value']; | |
| 408 | - $image_url = ''; | |
| 409 | - | |
| 410 | - if (is_numeric($value)) { | |
| 411 | - $maybe_url = wp_get_attachment_image_url((int) $value, 'large'); | |
| 412 | - if (!empty($maybe_url)) { | |
| 413 | - $image_url = $maybe_url; | |
| 414 | - } | |
| 415 | - } elseif (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) { | |
| 416 | - $image_url = $value; | |
| 417 | - } | |
| 418 | - | |
| 419 | - $icon_data['value'] = $image_url; | |
| 420 | - } | |
| 421 | - $attribute->icon = $icon_data; | |
| 422 | - } else { | |
| 423 | - // Handle legacy string format | |
| 424 | - $attribute->icon = [ | |
| 425 | - 'type' => 'icon', | |
| 426 | - 'value' => $attribute->icon | |
| 427 | - ]; | |
| 428 | - } | |
| 429 | - } else { | |
| 430 | - $attribute->icon = null; | |
| 431 | - } | |
| 410 | + $this->attachDecodedIconToAttribute($attribute); | |
| 432 | 411 | |
| 433 | 412 | return $this->success_response($attribute); |
| 434 | 413 | |
| 435 | 414 | } catch (\Exception $e) { |
| @@ -501,8 +480,52 @@ | ||
| 501 | 480 | } |
| 502 | 481 | } |
| 503 | 482 | |
| 504 | 483 | /** |
| 484 | + * Handle bulk operations | |
| 485 | + */ | |
| 486 | + public function bulkAction(WP_REST_Request $request): WP_REST_Response | |
| 487 | + { | |
| 488 | + try { | |
| 489 | + $action = sanitize_text_field((string) $request->get_param('action')); | |
| 490 | + $ids = $request->get_param('ids'); | |
| 491 | + | |
| 492 | + if (empty($action)) { | |
| 493 | + return $this->error_response(__('Action is required', 'yatra'), 400); | |
| 494 | + } | |
| 495 | + | |
| 496 | + if (empty($ids) || !is_array($ids)) { | |
| 497 | + return $this->error_response(__('No attributes selected', 'yatra'), 400); | |
| 498 | + } | |
| 499 | + | |
| 500 | + switch ($action) { | |
| 501 | + case 'trash': | |
| 502 | + $result = $this->attributeService->bulkUpdateStatus($ids, 'trash'); | |
| 503 | + break; | |
| 504 | + case 'publish': | |
| 505 | + case 'restore': | |
| 506 | + $result = $this->attributeService->bulkUpdateStatus($ids, 'publish'); | |
| 507 | + break; | |
| 508 | + case 'draft': | |
| 509 | + $result = $this->attributeService->bulkUpdateStatus($ids, 'draft'); | |
| 510 | + break; | |
| 511 | + case 'delete': | |
| 512 | + $result = $this->attributeService->bulkDelete($ids); | |
| 513 | + break; | |
| 514 | + default: | |
| 515 | + return $this->error_response(__('Invalid action', 'yatra'), 400); | |
| 516 | + } | |
| 517 | + | |
| 518 | + return $this->success_response($result); | |
| 519 | + | |
| 520 | + } catch (\InvalidArgumentException $e) { | |
| 521 | + return $this->error_response($e->getMessage(), 400); | |
| 522 | + } catch (\Exception $e) { | |
| 523 | + return $this->error_response($e->getMessage(), 500); | |
| 524 | + } | |
| 525 | + } | |
| 526 | + | |
| 527 | + /** | |
| 505 | 528 | * Get attribute values |
| 506 | 529 | */ |
| 507 | 530 | public function get_attribute_values(WP_REST_Request $request) |
| 508 | 531 | { |
| @@ -692,17 +715,18 @@ | ||
| 692 | 715 | // Handle icon field |
| 693 | 716 | if ($request->has_param('icon')) { |
| 694 | 717 | $icon = $request->get_param('icon'); |
| 695 | 718 | if (is_array($icon)) { |
| 696 | - // Sanitize icon array | |
| 697 | - $data['icon'] = [ | |
| 698 | - 'type' => isset($icon['type']) && in_array($icon['type'], ['icon', 'image'], true) | |
| 699 | - ? $icon['type'] | |
| 700 | - : 'icon', | |
| 701 | - 'value' => isset($icon['value']) | |
| 702 | - ? sanitize_text_field($icon['value']) | |
| 703 | - : '', | |
| 704 | - ]; | |
| 719 | + $data['icon'] = function_exists('yatra_normalize_icon_picker_for_storage') | |
| 720 | + ? yatra_normalize_icon_picker_for_storage($icon) | |
| 721 | + : [ | |
| 722 | + 'type' => isset($icon['type']) && in_array($icon['type'], ['icon', 'image'], true) | |
| 723 | + ? $icon['type'] | |
| 724 | + : 'icon', | |
| 725 | + 'value' => isset($icon['value']) | |
| 726 | + ? sanitize_text_field((string) $icon['value']) | |
| 727 | + : '', | |
| 728 | + ]; | |
| 705 | 729 | } elseif (is_string($icon)) { |
| 706 | 730 | // Handle legacy string format |
| 707 | 731 | $data['icon'] = sanitize_text_field($icon); |
| 708 | 732 | } |
| @@ -786,42 +810,33 @@ | ||
| 786 | 810 | return $data; |
| 787 | 811 | } |
| 788 | 812 | |
| 789 | 813 | /** |
| 790 | - * Check permissions for read operations | |
| 814 | + * Granular permission checks. Trip attributes are a trip-taxonomy | |
| 815 | + * concept — they classify trips for filtering / display — so the | |
| 816 | + * Team module's `yatra_manage_trip_taxonomies` cap is the right | |
| 817 | + * gate for write operations, and `yatra_view_trips` for reads. | |
| 818 | + * WP administrators pass every cap via the Team module's admin- | |
| 819 | + * fallback filter so no explicit `manage_options` check is needed. | |
| 791 | 820 | */ |
| 792 | 821 | public function get_permissions_check(): bool |
| 793 | 822 | { |
| 794 | - return current_user_can('manage_options'); | |
| 823 | + return current_user_can('yatra_view_trips'); | |
| 795 | 824 | } |
| 796 | 825 | |
| 797 | - /** | |
| 798 | - * Check permissions for create/update/delete operations | |
| 799 | - */ | |
| 800 | 826 | public function check_permission(?WP_REST_Request $request = null): bool |
| 801 | 827 | { |
| 802 | - $hasPermission = current_user_can('manage_options'); | |
| 803 | - | |
| 804 | - if (defined('WP_DEBUG') && WP_DEBUG) { | |
| 805 | - } | |
| 806 | - | |
| 807 | - return $hasPermission; | |
| 828 | + return current_user_can('yatra_manage_trip_taxonomies'); | |
| 808 | 829 | } |
| 809 | 830 | |
| 810 | - /** | |
| 811 | - * Check permissions for search operations | |
| 812 | - */ | |
| 813 | 831 | public function search_permissions_check(): bool |
| 814 | 832 | { |
| 815 | - return current_user_can('manage_options'); | |
| 833 | + return current_user_can('yatra_view_trips'); | |
| 816 | 834 | } |
| 817 | 835 | |
| 818 | - /** | |
| 819 | - * Check permissions for update operations | |
| 820 | - */ | |
| 821 | 836 | public function update_permissions_check(): bool |
| 822 | 837 | { |
| 823 | - return current_user_can('manage_options'); | |
| 838 | + return current_user_can('yatra_manage_trip_taxonomies'); | |
| 824 | 839 | } |
| 825 | 840 | |
| 826 | 841 | /** |
| 827 | 842 | * Get item schema |
| @@ -936,7 +951,44 @@ | ||
| 936 | 951 | $stats = $this->attributeService->getStatusCounts(); |
| 937 | 952 | return $this->success_response($stats); |
| 938 | 953 | } catch (\Exception $e) { |
| 939 | 954 | return $this->error_response($e->getMessage(), 500); |
| 955 | + } | |
| 956 | + } | |
| 957 | + | |
| 958 | + /** | |
| 959 | + * Replace raw DB icon (serialized array or legacy string) with REST JSON (preserves Font Awesome provider). | |
| 960 | + * | |
| 961 | + * @param object $attribute Row from AttributeService::getById() | |
| 962 | + */ | |
| 963 | + private function attachDecodedIconToAttribute(object $attribute): void | |
| 964 | + { | |
| 965 | + if (!empty($attribute->icon)) { | |
| 966 | + $icon_data = maybe_unserialize($attribute->icon); | |
| 967 | + if (is_array($icon_data)) { | |
| 968 | + if ($icon_data['type'] === 'image' && !empty($icon_data['value'])) { | |
| 969 | + $value = $icon_data['value']; | |
| 970 | + $image_url = ''; | |
| 971 | + | |
| 972 | + if (is_numeric($value)) { | |
| 973 | + $maybe_url = wp_get_attachment_image_url((int) $value, 'large'); | |
| 974 | + if (!empty($maybe_url)) { | |
| 975 | + $image_url = $maybe_url; | |
| 976 | + } | |
| 977 | + } elseif (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) { | |
| 978 | + $image_url = $value; | |
| 979 | + } | |
| 980 | + | |
| 981 | + $icon_data['value'] = $image_url; | |
| 982 | + } | |
| 983 | + $attribute->icon = $icon_data; | |
| 984 | + } else { | |
| 985 | + $attribute->icon = [ | |
| 986 | + 'type' => 'icon', | |
| 987 | + 'value' => (string) $attribute->icon, | |
| 988 | + ]; | |
| 989 | + } | |
| 990 | + } else { | |
| 991 | + $attribute->icon = null; | |
| 940 | 992 | } |
| 941 | 993 | } |
| 942 | 994 | } |