Changeset 3420035
- Timestamp:
- 12/15/2025 11:27:02 AM (3 months ago)
- Location:
- easy-invoice
- Files:
-
- 6 edited
- 1 copied
-
tags/2.1.7 (copied) (copied from easy-invoice/trunk)
-
tags/2.1.7/easy-invoice.php (modified) (2 diffs)
-
tags/2.1.7/includes/Controllers/QuoteController.php (modified) (117 diffs)
-
tags/2.1.7/readme.txt (modified) (2 diffs)
-
trunk/easy-invoice.php (modified) (2 diffs)
-
trunk/includes/Controllers/QuoteController.php (modified) (117 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
easy-invoice/tags/2.1.7/easy-invoice.php
r3417137 r3420035 4 4 * Plugin URI: https://matrixaddons.com/plugins/easy-invoice 5 5 * Description: A beautiful, full-featured invoicing solution for WordPress. Create professional invoices, quotes, and manage payments with ease. 6 * Version: 2.1. 66 * Version: 2.1.7 7 7 * Author: MatrixAddons 8 8 * Author URI: https://matrixaddons.com … … 25 25 26 26 // Define plugin constants. 27 define( 'EASY_INVOICE_VERSION', '2.1. 6' );27 define( 'EASY_INVOICE_VERSION', '2.1.7' ); 28 28 define( 'EASY_INVOICE_PLUGIN_FILE', __FILE__ ); 29 29 define( 'EASY_INVOICE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
easy-invoice/tags/2.1.7/includes/Controllers/QuoteController.php
r3399346 r3420035 27 27 */ 28 28 class QuoteController { 29 29 30 30 /** 31 31 * Quote repository … … 34 34 */ 35 35 private $quote_repository; 36 36 37 37 /** 38 38 * Client repository … … 41 41 */ 42 42 private $client_repository; 43 43 44 44 /** 45 45 * Form processor … … 48 48 */ 49 49 private $form_processor; 50 50 51 51 /** 52 52 * Quote log service … … 55 55 */ 56 56 private $quote_log_service; 57 57 58 58 /** 59 59 * Constructor … … 67 67 $this->quote_log_service = new QuoteLogService(); 68 68 } 69 69 70 70 /** 71 71 * Initialize the controller … … 76 76 // Allow plugins to extend the controller initialization 77 77 do_action('easy_invoice_quote_controller_before_init', $this); 78 78 79 79 // Add AJAX handlers 80 80 add_action('wp_ajax_easy_invoice_delete_quote', [$this, 'handleDeleteQuote']); … … 89 89 add_action('wp_ajax_nopriv_easy_invoice_decline_quote', [$this, 'handleDeclineQuote']); 90 90 add_action('wp_ajax_easy_invoice_update_existing_quotes', [$this, 'handleUpdateExistingQuotes']); 91 91 92 92 // Add missing AJAX handlers for quote listing actions 93 93 add_action('wp_ajax_easy_invoice_bulk_quote_action', [$this, 'handleBulkQuoteAction']); 94 94 add_action('wp_ajax_easy_invoice_trash_quote', [$this, 'handleTrashQuote']); 95 95 add_action('wp_ajax_easy_invoice_draft_quote', [$this, 'handleDraftQuote']); 96 96 97 97 // Add regular POST form handlers for quote actions 98 98 add_action('init', [$this, 'handleQuoteFormActions']); 99 99 100 100 // Add new AJAX handler for restoring a trashed quote 101 101 add_action('wp_ajax_easy_invoice_restore_quote', [ $this, 'handleRestoreQuote' ]); 102 102 103 103 // Add new AJAX handler for emptying trash 104 104 add_action('wp_ajax_easy_invoice_empty_trash', [ $this, 'handleEmptyTrash' ]); 105 105 106 106 // Add new AJAX handler for getting quote logs 107 107 add_action('wp_ajax_easy_invoice_get_quote_logs', [ $this, 'handleGetQuoteLogs' ]); 108 108 109 109 // Allow plugins to extend the controller initialization 110 110 do_action('easy_invoice_quote_controller_after_init', $this); 111 111 } 112 112 113 113 /** 114 114 * Display quote pages … … 120 120 // Allow plugins to modify display arguments 121 121 $args = apply_filters('easy_invoice_quote_controller_display_args', $args); 122 122 123 123 $page = $args['page'] ?? ''; 124 124 125 125 // Allow plugins to modify the page before processing 126 126 $page = apply_filters('easy_invoice_quote_controller_display_page', $page, $args); 127 127 128 128 switch ($page) { 129 129 case PagesSlugs::ALL_QUOTES: 130 130 $this->displayListing(); 131 131 break; 132 132 133 133 case PagesSlugs::QUOTE_NEW: 134 134 $this->displayBuilder(); 135 135 break; 136 136 137 137 case PagesSlugs::QUOTE_PREVIEW: 138 138 $this->displayPreview($args); 139 139 break; 140 140 141 141 default: 142 142 $this->displayListing(); 143 143 break; 144 144 } 145 145 146 146 // Allow plugins to perform actions after display 147 147 do_action('easy_invoice_quote_controller_after_display', $page, $args); 148 148 } 149 149 150 150 /** 151 151 * Display quote listing page … … 159 159 // Get trash count first (based on post_status) 160 160 $trash_count = (int)$wpdb->get_var($wpdb->prepare( 161 "SELECT COUNT(*) FROM {$wpdb->posts} 161 "SELECT COUNT(*) FROM {$wpdb->posts} 162 162 WHERE post_type = %s AND post_status = 'trash'", 163 163 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE 164 164 )); 165 165 166 166 // Get counts for each meta status (excluding trashed posts) 167 167 $status_counts = $wpdb->get_results($wpdb->prepare( 168 "SELECT COALESCE(pm.meta_value, 'draft') as status, COUNT(*) as count 169 FROM {$wpdb->posts} p 168 "SELECT COALESCE(pm.meta_value, 'draft') as status, COUNT(*) as count 169 FROM {$wpdb->posts} p 170 170 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_easy_invoice_quote_status' 171 WHERE p.post_type = %s 171 WHERE p.post_type = %s 172 172 AND p.post_status != 'trash' 173 173 GROUP BY COALESCE(pm.meta_value, 'draft')", … … 189 189 $count = (int)$status->count; 190 190 $all_count += $count; // Add to total (excluding trash) 191 191 192 192 switch ($status->status) { 193 193 case 'draft': … … 218 218 // Allow plugins to perform actions before displaying listing 219 219 do_action('easy_invoice_quote_controller_before_display_listing'); 220 220 221 221 // Get filter parameters 222 222 $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : ''; … … 242 242 // For trash and cancelled views, look at post_status = 'trash' 243 243 $query_args['post_status'] = 'trash'; 244 244 245 245 // For cancelled view, also filter by meta status 246 246 if ($current_view === 'cancelled') { … … 256 256 // For all other views, exclude trashed posts 257 257 $query_args['post_status'] = ['publish', 'draft', 'private', 'pending']; 258 258 259 259 if ($current_view !== 'all') { 260 260 // For specific status views, add meta query … … 272 272 if (!empty($search_query)) { 273 273 $search_ids = []; 274 274 275 275 // Build base query args for search 276 276 $search_query_args = [ … … 310 310 ]); 311 311 $meta_search = new \WP_Query($meta_search_args); 312 312 313 313 if ($meta_search->have_posts()) { 314 314 $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID')); 315 315 } 316 316 317 317 $search_ids = array_unique($search_ids); 318 318 319 319 if (!empty($search_ids)) { 320 320 $query_args['post__in'] = $search_ids; … … 329 329 $wp_query = new \WP_Query($query_args); 330 330 $quotes = []; 331 331 332 332 if ($wp_query->have_posts()) { 333 333 foreach ($wp_query->posts as $post) { … … 341 341 // Allow plugins to modify the quotes array 342 342 $quotes = apply_filters('easy_invoice_quote_controller_quotes_list', $quotes, $wp_query); 343 343 344 344 // Get pagination info from WordPress query 345 345 $total_quotes = $wp_query->found_posts; … … 381 381 } 382 382 } 383 383 384 384 // Prepare template data 385 385 $template_data = [ … … 404 404 'wp_query' => $wp_query 405 405 ]; 406 406 407 407 // Allow plugins to modify template data 408 408 $template_data = apply_filters('easy_invoice_quote_controller_template_data', $template_data); 409 409 410 410 // Display the template 411 411 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/listing.php'; 412 412 413 413 // Allow plugins to perform actions after displaying listing 414 414 do_action('easy_invoice_quote_controller_after_display_listing', $template_data); 415 415 } 416 416 417 417 /** 418 418 * Display quote builder page … … 423 423 // Allow plugins to perform actions before displaying builder 424 424 do_action('easy_invoice_quote_controller_before_display_builder'); 425 425 426 426 $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0; 427 427 $quote = null; 428 428 429 429 if ($quote_id > 0) { 430 430 $quote = $this->quote_repository->find($quote_id); 431 431 } 432 432 433 433 $clients = $this->client_repository->all(); 434 434 435 435 // Allow plugins to modify the data 436 436 $quote = apply_filters('easy_invoice_quote_controller_builder_quote', $quote, $quote_id); … … 439 439 // Include the builder template 440 440 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/builder.php'; 441 441 442 442 // Allow plugins to perform actions after displaying builder 443 443 do_action('easy_invoice_quote_controller_after_display_builder', $quote, $clients); 444 444 } 445 445 446 446 /** 447 447 * Display quote preview page … … 453 453 // Allow plugins to perform actions before displaying preview 454 454 do_action('easy_invoice_quote_controller_before_display_preview', $args); 455 455 456 456 $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0; 457 457 458 458 if ($quote_id <= 0) { 459 459 wp_die(__('Quote not found.', 'easy-invoice')); 460 460 } 461 461 462 462 $quote = $this->quote_repository->find($quote_id); 463 463 if (!$quote) { 464 464 wp_die(__('Quote not found.', 'easy-invoice')); 465 465 } 466 466 467 467 // Allow plugins to modify the quote 468 468 $quote = apply_filters('easy_invoice_quote_controller_preview_quote', $quote, $quote_id); 469 469 470 470 // Include the preview template 471 471 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/preview.php'; 472 472 473 473 // Allow plugins to perform actions after displaying preview 474 474 do_action('easy_invoice_quote_controller_after_display_preview', $quote, $args); 475 475 } 476 476 477 477 /** 478 478 * Handle delete quote AJAX request … … 485 485 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 486 486 } 487 487 488 488 // Check permissions 489 489 if (!current_user_can('manage_options')) { 490 490 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 491 491 } 492 492 493 493 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 494 494 495 495 if ($quote_id <= 0) { 496 496 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 497 497 } 498 498 499 499 if ($this->quote_repository->delete($quote_id)) { 500 500 // Log the quote deletion 501 501 $this->quote_log_service->logDeletion($quote_id); 502 502 503 503 wp_send_json_success([ 504 504 'message' => __('Quote deleted successfully.', 'easy-invoice'), … … 512 512 } 513 513 } 514 514 515 515 /** 516 516 * Handle get quote AJAX request … … 523 523 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 524 524 } 525 525 526 526 // Check permissions 527 527 if (!current_user_can('manage_options')) { 528 528 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 529 529 } 530 530 531 531 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 532 532 533 533 if ($quote_id <= 0) { 534 534 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 535 535 } 536 536 537 537 $quote = $this->quote_repository->find($quote_id); 538 538 539 539 if (!$quote) { 540 540 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 541 541 } 542 542 543 543 wp_send_json_success(['quote' => $quote->toArray()]); 544 544 } 545 545 546 546 /** 547 547 * Handle AJAX request to load quote template … … 554 554 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 555 555 } 556 556 557 557 // Check permissions 558 558 if (!current_user_can('manage_options')) { 559 559 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 560 560 } 561 561 562 562 $template_id = sanitize_text_field($_POST['template'] ?? ''); 563 563 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 564 564 565 565 if (empty($template_id)) { 566 566 wp_send_json_error(['message' => __('Template ID is required.', 'easy-invoice')]); 567 567 } 568 568 569 569 // Validate template name securely 570 570 $template_id = $this->validateTemplateName($template_id, 'quote'); 571 571 572 572 // Get secure template file path 573 573 $template_file = $this->getSecureTemplatePath($template_id, 'quote'); 574 574 575 575 if (!$template_file) { 576 576 wp_send_json_error(['message' => __('Template not found.', 'easy-invoice')]); 577 577 } 578 578 579 579 // Load quote if provided 580 580 $quote = null; … … 582 582 $quote = $this->quote_repository->find($quote_id); 583 583 } 584 584 585 585 // Start output buffering to capture template HTML 586 586 ob_start(); 587 587 588 588 // Include the template file 589 589 include $template_file; 590 590 591 591 // Get the captured HTML 592 592 $html = ob_get_clean(); 593 593 594 594 wp_send_json_success(['html' => $html]); 595 595 } … … 597 597 /** 598 598 * Validate and sanitize template name to prevent directory traversal attacks 599 * 599 * 600 600 * @param string $template The template name to validate 601 601 * @param string $type Either 'invoice' or 'quote' … … 611 611 // Strip any directory components using basename 612 612 $template = basename($template); 613 613 614 614 // Remove any file extension 615 615 $template = preg_replace('/\.(php|html|htm)$/i', '', $template); 616 616 617 617 // Remove any non-alphanumeric characters except hyphens and underscores 618 618 $template = preg_replace('/[^a-z0-9_-]/i', '', $template); 619 619 620 620 // Check if template is in whitelist 621 621 if (isset($allowed_templates[$type]) && in_array($template, $allowed_templates[$type], true)) { 622 622 return $template; 623 623 } 624 624 625 625 // Return default template if not in whitelist 626 626 return 'standard'; … … 629 629 /** 630 630 * Get secure template file path with directory traversal protection 631 * 631 * 632 632 * @param string $template The validated template name 633 633 * @param string $type Either 'invoice' or 'quote' … … 646 646 647 647 $template_dir = $template_dirs[$type]; 648 648 649 649 // Ensure template directory exists and is a directory 650 650 if (!is_dir($template_dir)) { … … 660 660 // Construct the template file path 661 661 $template_file = $real_template_dir . DIRECTORY_SEPARATOR . $template . '.php'; 662 662 663 663 // Get the real path of the template file (resolves any .. or . components) 664 664 $real_template_file = realpath($template_file); 665 665 666 666 // Verify that the resolved path is within the template directory 667 667 // This prevents directory traversal attacks … … 670 670 $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php'; 671 671 $real_default_file = realpath($default_file); 672 672 673 673 if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0) { 674 674 return $real_default_file; 675 675 } 676 676 677 677 return false; 678 678 } … … 683 683 $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php'; 684 684 $real_default_file = realpath($default_file); 685 685 686 686 if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0 && is_file($real_default_file) && is_readable($real_default_file)) { 687 687 return $real_default_file; 688 688 } 689 689 690 690 return false; 691 691 } … … 693 693 return $real_template_file; 694 694 } 695 695 696 696 /** 697 697 * Handle AJAX request to create a new quote with just the title … … 712 712 wp_send_json_error(['message' => __('Quote title is required.', 'easy-invoice')]); 713 713 } 714 714 715 715 // Generate a unique quote number 716 716 $quote_number = ''; … … 722 722 $quote_number = 'QT-' . str_pad(time(), 6, '0', STR_PAD_LEFT); 723 723 } 724 724 725 725 // Get global quote settings 726 726 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 732 732 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice')); 733 733 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice')); 734 734 735 735 // Create the quote with just the title and default values 736 736 $data = [ … … 756 756 wp_send_json_success(['quote_id' => $quote->getId()]); 757 757 } 758 758 759 759 /** 760 760 * Handle AJAX request to load quote form for modal … … 767 767 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 768 768 } 769 769 770 770 // Check permissions 771 771 if (!current_user_can('manage_options')) { 772 772 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 773 773 } 774 774 775 775 // Get global quote settings 776 776 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 782 782 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice')); 783 783 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice')); 784 784 785 785 // Create a new quote object for the form 786 786 $quote_number_service = function_exists('easy_invoice_get_quote_number_service') ? easy_invoice_get_quote_number_service() : null; … … 815 815 'declined_message' => $quote_declined_message, // Use global declined message setting 816 816 ); 817 817 818 818 // Create a temporary WP_Post object for new quote 819 819 $empty_post = new \WP_Post((object) array( … … 837 837 'filter' => 'raw', 838 838 )); 839 839 840 840 $quote = new \EasyInvoice\Models\Quote($empty_post); 841 841 842 842 // Set default values on the quote object 843 843 foreach ($quote_data as $key => $value) { … … 868 868 } 869 869 } 870 870 871 871 // Initialize empty items array 872 872 $quote->setItems([]); 873 873 874 874 // Set variables needed by the form template 875 875 $quote_id = 0; … … 879 879 $admin_nonce = wp_create_nonce('easy_invoice_admin_nonce'); 880 880 $quote_field_config = $quote_form_manager->getFieldConfigForJavaScript(); 881 881 882 882 // Start output buffering to capture form HTML 883 883 ob_start(); 884 884 885 885 // Include the quote form template 886 886 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/form.php'; 887 887 888 888 // Get the captured HTML 889 889 $html = ob_get_clean(); 890 890 891 891 wp_send_json_success(['html' => $html]); 892 892 } 893 893 894 894 /** 895 895 * Handle search clients AJAX request … … 902 902 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 903 903 } 904 904 905 905 // Check permissions 906 906 if (!current_user_can('manage_options')) { 907 907 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 908 908 } 909 909 910 910 $query = sanitize_text_field($_POST['query'] ?? ''); 911 911 912 912 // If query is empty, get all clients 913 913 if (empty($query)) { … … 917 917 $clients = $this->client_repository->search($query); 918 918 } 919 919 920 920 $results = []; 921 921 foreach ($clients as $client) { … … 930 930 ]; 931 931 } 932 932 933 933 wp_send_json_success($results); 934 934 } 935 935 936 936 /** 937 937 * Handle AJAX request to accept a quote … … 944 944 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 945 945 } 946 946 947 947 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 948 948 949 949 if ($quote_id <= 0) { 950 950 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 951 951 } 952 952 953 953 // Get the quote 954 954 $quote = $this->quote_repository->find($quote_id); 955 955 956 956 if (!$quote) { 957 957 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 958 958 } 959 959 960 960 // Check if user has permission to accept this quote 961 961 $current_user = wp_get_current_user(); 962 962 $is_admin = current_user_can('manage_options'); 963 964 if (!$is_admin) { 965 // For non-admins, check if they are the client 963 964 $restrict = get_option('easy_invoice_pro_restrict_quote_to_client', 'no'); 965 966 if (!$is_admin && $restrict === 'yes') { 967 // For non-admins, check if they are the client 966 968 if ($quote->getClientId()) { 967 969 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 968 970 $client = $client_repository->find($quote->getClientId()); 969 971 970 972 if (!$client || $client->getEmail() !== $current_user->user_email) { 971 973 wp_send_json_error(['message' => __('You do not have permission to accept this quote.', 'easy-invoice')]); … … 975 977 } 976 978 } 977 979 978 980 // Get global accept action setting 979 981 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); 980 982 $accept_action = $settings_controller::getQuoteAcceptAction(); 981 983 982 984 // Update quote status to accepted 983 985 $quote->setStatus('accepted'); 984 986 $quote->setAcceptedDate(date('Y-m-d H:i:s')); 985 987 $quote->setAcceptedBy($current_user->ID); 986 988 987 989 // Save the quote 988 990 $saved = $quote->save(); 989 991 990 992 if (!$saved) { 991 993 wp_send_json_error(['message' => __('Failed to accept quote.', 'easy-invoice')]); 992 994 } 993 995 994 996 // Log the quote acceptance 995 997 $this->quote_log_service->logAcceptance($quote_id, [ … … 997 999 'user_type' => $is_admin ? 'admin' : 'client' 998 1000 ]); 999 1001 1000 1002 // Perform the configured accept action 1001 1003 $invoice_id = null; 1002 1004 $action_message = ''; 1003 1005 1004 1006 switch ($accept_action) { 1005 1007 case 'convert': … … 1011 1013 $action_message = __('Quote converted to invoice successfully.', 'easy-invoice'); 1012 1014 break; 1013 1015 1014 1016 case 'convert_available': 1015 1017 // Convert quote to invoice (Available status) … … 1020 1022 $action_message = __('Quote converted to invoice successfully.', 'easy-invoice'); 1021 1023 break; 1022 1024 1023 1025 case 'convert_send': 1024 1026 // Convert quote to invoice and send to client (Available status) … … 1029 1031 $action_message = __('Quote converted to invoice and sent to client successfully.', 'easy-invoice'); 1030 1032 break; 1031 1033 1032 1034 case 'duplicate': 1033 1035 // Create new invoice, keep quote as-is (Draft status) … … 1038 1040 $action_message = __('New invoice created from quote successfully.', 'easy-invoice'); 1039 1041 break; 1040 1042 1041 1043 case 'duplicate_send': 1042 1044 // Create new invoice and send to client, keep quote as-is (Available status) … … 1047 1049 $action_message = __('New invoice created and sent to client successfully.', 'easy-invoice'); 1048 1050 break; 1049 1051 1050 1052 case 'do_nothing': 1051 1053 default: … … 1054 1056 break; 1055 1057 } 1056 1058 1057 1059 // Send notification email to admin 1058 1060 if (!$is_admin) { 1059 1061 $this->sendQuoteAcceptanceNotification($quote); 1060 1062 } 1061 1063 1062 1064 // Get URLs for the new invoice 1063 1065 $invoice_url = null; 1064 1066 $secure_url = null; 1065 1067 1066 1068 if ($invoice_id) { 1067 1069 // Always use WordPress permalink … … 1075 1077 } 1076 1078 } 1077 1079 1078 1080 wp_send_json_success([ 1079 1081 'message' => $action_message, … … 1087 1089 ]); 1088 1090 } 1089 1091 1090 1092 /** 1091 1093 * Convert quote to invoice … … 1099 1101 // Get invoice repository 1100 1102 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository(); 1101 1103 1102 1104 // Create invoice data from quote - convert ALL fields 1103 1105 $invoice_data = [ … … 1135 1137 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields 1136 1138 ]; 1137 1139 1138 1140 // Create the invoice 1139 1141 $invoice = $invoice_repository->create($invoice_data); 1140 1142 1141 1143 if ($invoice) { 1142 1144 // Store the quote ID in the invoice's meta for tracking 1143 1145 update_post_meta($invoice->getId(), '_converted_from_quote', $quote->getId()); 1144 1146 1145 1147 // Update quote to reference the created invoice 1146 1148 $quote->setCustomField('converted_invoice_id', $invoice->getId()); 1147 1149 $quote->save(); 1148 1150 1149 1151 // Ensure secure link is generated for the new invoice (Pro version) 1150 1152 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) { … … 1152 1154 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId())); 1153 1155 } 1154 1156 1155 1157 return $invoice->getId(); 1156 1158 } 1157 1159 1158 1160 return null; 1159 1161 } catch (\Exception $e) { … … 1162 1164 } 1163 1165 } 1164 1166 1165 1167 /** 1166 1168 * Create new invoice from quote (duplicate) … … 1174 1176 // Get invoice repository 1175 1177 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository(); 1176 1178 1177 1179 // Create invoice data from quote - convert ALL fields 1178 1180 $invoice_data = [ … … 1210 1212 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields 1211 1213 ]; 1212 1214 1213 1215 // Create the invoice 1214 1216 $invoice = $invoice_repository->create($invoice_data); 1215 1217 1216 1218 if ($invoice) { 1217 1219 // Link the invoice to the quote 1218 1220 $quote->setCustomField('related_invoice_id', $invoice->getId()); 1219 1221 $quote->save(); 1220 1222 1221 1223 // Ensure secure link is generated for the new invoice (Pro version) 1222 1224 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) { … … 1224 1226 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId())); 1225 1227 } 1226 1228 1227 1229 return $invoice->getId(); 1228 1230 } 1229 1231 1230 1232 return null; 1231 1233 } catch (\Exception $e) { … … 1234 1236 } 1235 1237 } 1236 1238 1237 1239 /** 1238 1240 * Send invoice to client … … 1246 1248 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository(); 1247 1249 $invoice = $invoice_repository->find($invoice_id); 1248 1250 1249 1251 if (!$invoice) { 1250 1252 return false; 1251 1253 } 1252 1254 1253 1255 // Get email manager 1254 1256 $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); 1255 1257 1256 1258 // Send invoice email 1257 1259 $result = $email_manager->sendInvoiceEmail($invoice, 'new'); 1258 1260 1259 1261 return $result['success']; 1260 1262 } catch (\Exception $e) { … … 1263 1265 } 1264 1266 } 1265 1267 1266 1268 /** 1267 1269 * Convert quote items to invoice items … … 1272 1274 private function convertQuoteItemsToInvoiceItems(array $quote_items): array { 1273 1275 $invoice_items = []; 1274 1276 1275 1277 foreach ($quote_items as $quote_item) { 1276 1278 if (is_object($quote_item) && method_exists($quote_item, 'toArray')) { … … 1300 1302 } 1301 1303 } 1302 1304 1303 1305 return $invoice_items; 1304 1306 } 1305 1307 1306 1308 /** 1307 1309 * Generate unique invoice number … … 1315 1317 return $invoice_number_service->generateUniqueNumber(); 1316 1318 } 1317 1319 1318 1320 // Fallback to timestamp-based number 1319 1321 return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT); 1320 1322 } 1321 1323 1322 1324 /** 1323 1325 * Get changes between two quote versions … … 1329 1331 private function getQuoteChanges($old_quote, $new_quote): array { 1330 1332 $changes = []; 1331 1333 1332 1334 // Compare key fields 1333 1335 $fields_to_compare = [ … … 1343 1345 'terms' => 'Terms', 1344 1346 ]; 1345 1347 1346 1348 foreach ($fields_to_compare as $field => $label) { 1347 1349 $method_name = 'get' . easy_invoice_str_replace('_', '', ucwords($field, '_')); 1348 1350 1349 1351 if (method_exists($old_quote, $method_name) && method_exists($new_quote, $method_name)) { 1350 1352 $old_value = $old_quote->$method_name(); 1351 1353 $new_value = $new_quote->$method_name(); 1352 1354 1353 1355 if ($old_value !== $new_value) { 1354 1356 $changes[$field] = $new_value; … … 1356 1358 } 1357 1359 } 1358 1360 1359 1361 return $changes; 1360 1362 } 1361 1363 1362 1364 /** 1363 1365 * Handle AJAX request to decline a quote … … 1370 1372 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1371 1373 } 1372 1374 1373 1375 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1374 1376 $decline_reason = isset($_POST['decline_reason']) ? sanitize_textarea_field($_POST['decline_reason']) : ''; 1375 1377 1376 1378 if ($quote_id <= 0) { 1377 1379 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1378 1380 } 1379 1381 1380 1382 // Get the quote 1381 1383 $quote = $this->quote_repository->find($quote_id); 1382 1384 1383 1385 if (!$quote) { 1384 1386 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1385 1387 } 1386 1388 1387 1389 // Check if decline reason is required by global settings 1388 1390 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 1390 1392 wp_send_json_error(['message' => __('Reason for declining is required.', 'easy-invoice')]); 1391 1393 } 1392 1394 1393 1395 // Check if user has permission to decline this quote 1394 1396 $current_user = wp_get_current_user(); 1395 1397 $is_admin = current_user_can('manage_options'); 1396 1397 if (!$is_admin) { 1398 1399 $restrict = get_option('easy_invoice_pro_restrict_quote_to_client', 'no'); 1400 1401 if (!$is_admin && $restrict === 'yes') { 1398 1402 // For non-admins, check if they are the client 1399 1403 if ($quote->getClientId()) { 1400 1404 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1401 1405 $client = $client_repository->find($quote->getClientId()); 1402 1406 1403 1407 if (!$client || $client->getEmail() !== $current_user->user_email) { 1404 1408 wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]); … … 1408 1412 } 1409 1413 } 1410 1414 1411 1415 // Update quote status to declined 1412 1416 $quote->setStatus('declined'); 1413 1417 $quote->setDeclinedDate(date('Y-m-d H:i:s')); 1414 1418 $quote->setDeclinedBy($current_user->ID); 1415 1419 1416 1420 // Save decline reason if provided 1417 1421 if (!empty($decline_reason)) { 1418 1422 $quote->setDeclineReason($decline_reason); 1419 1423 } 1420 1424 1421 1425 // Save the quote 1422 1426 $saved = $quote->save(); 1423 1427 1424 1428 if (!$saved) { 1425 1429 wp_send_json_error(['message' => __('Failed to decline quote.', 'easy-invoice')]); 1426 1430 } 1427 1431 1428 1432 // Log the quote decline 1429 1433 $this->quote_log_service->logDecline($quote_id, $decline_reason, [ 1430 1434 'user_type' => $is_admin ? 'admin' : 'client' 1431 1435 ]); 1432 1436 1433 1437 // Send notification email to admin 1434 1438 if (!$is_admin) { 1435 1439 $this->sendQuoteDeclineNotification($quote); 1436 1440 } 1437 1441 1438 1442 wp_send_json_success([ 1439 1443 'message' => __('Quote declined successfully.', 'easy-invoice'), … … 1444 1448 ]); 1445 1449 } 1446 1450 1447 1451 /** 1448 1452 * Send quote acceptance notification to admin … … 1455 1459 $email_manager->sendAdminQuoteNotification($quote, 'accepted'); 1456 1460 } 1457 1461 1458 1462 /** 1459 1463 * Send quote decline notification to admin … … 1466 1470 $email_manager->sendAdminQuoteNotification($quote, 'declined'); 1467 1471 } 1468 1472 1469 1473 /** 1470 1474 * Handle AJAX request to update existing quotes with missing data … … 1477 1481 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1478 1482 } 1479 1483 1480 1484 // Check permissions 1481 1485 if (!current_user_can('manage_options')) { 1482 1486 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1483 1487 } 1484 1488 1485 1489 $updated_count = 0; 1486 1490 $quotes = $this->quote_repository->findAll(); 1487 1491 1488 1492 foreach ($quotes as $quote) { 1489 1493 $post = get_post($quote->getId()); … … 1492 1496 $post_title = $quote->getTitle() ?: $quote->getNumber() ?: 'Untitled Quote'; 1493 1497 $post_name = sanitize_title($post_title); 1494 1498 1495 1499 // Ensure uniqueness 1496 1500 $original_slug = $post_name; … … 1500 1504 $counter++; 1501 1505 } 1502 1506 1503 1507 // Update the post with the new slug 1504 1508 wp_update_post([ … … 1506 1510 'post_name' => $post_name 1507 1511 ]); 1508 1512 1509 1513 $updated_count++; 1510 1514 } 1511 1515 } 1512 1516 1513 1517 wp_send_json_success([ 1514 1518 'message' => sprintf(__('Updated %d quotes with proper URLs.', 'easy-invoice'), $updated_count) 1515 1519 ]); 1516 1520 } 1517 1521 1518 1522 /** 1519 1523 * Handle AJAX request to duplicate a quote … … 1526 1530 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1527 1531 } 1528 1532 1529 1533 // Check permissions 1530 1534 if (!current_user_can('manage_options')) { 1531 1535 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1532 1536 } 1533 1537 1534 1538 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1535 1539 1536 1540 if ($quote_id <= 0) { 1537 1541 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1538 1542 } 1539 1543 1540 1544 $quote = $this->quote_repository->find($quote_id); 1541 1545 1542 1546 if (!$quote) { 1543 1547 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1544 1548 } 1545 1549 1546 1550 // Get global quote settings 1547 1551 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 1553 1557 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice')); 1554 1558 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice')); 1555 1559 1556 1560 // Create the duplicate quote 1557 1561 $duplicate_data = [ … … 1572 1576 'declined_message' => $quote_declined_message, 1573 1577 ]; 1574 1578 1575 1579 // Set client ID to 0 for a new quote 1576 1580 $duplicate_data['client_id'] = 0; 1577 1581 1578 1582 $duplicate_quote = $this->quote_repository->create($duplicate_data); 1579 1583 1580 1584 if ($duplicate_quote) { 1581 1585 $this->quote_log_service->logActivity($quote_id, 'duplicate', 'Quote duplicated', ['duplicate_id' => $duplicate_quote->getId()]); … … 1592 1596 } 1593 1597 } 1594 1598 1595 1599 /** 1596 1600 * Handle regular POST form actions for quote accept/decline … … 1603 1607 return; 1604 1608 } 1605 1609 1606 1610 // Handle accept quote 1607 1611 if (isset($_POST['accept_quote']) && isset($_POST['quote_id'])) { 1608 1612 $this->handleAcceptQuoteForm(); 1609 1613 } 1610 1614 1611 1615 // Handle decline quote 1612 1616 if (isset($_POST['decline_quote']) && isset($_POST['quote_id'])) { … … 1614 1618 } 1615 1619 } 1616 1620 1617 1621 /** 1618 1622 * Handle accept quote form submission … … 1625 1629 wp_die(__('Security check failed.', 'easy-invoice')); 1626 1630 } 1627 1631 1628 1632 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1629 1633 1630 1634 if ($quote_id <= 0) { 1631 1635 wp_die(__('Invalid quote ID.', 'easy-invoice')); 1632 1636 } 1633 1637 1634 1638 // Get the quote 1635 1639 $quote = $this->quote_repository->find($quote_id); 1636 1640 1637 1641 if (!$quote) { 1638 1642 wp_die(__('Quote not found.', 'easy-invoice')); 1639 1643 } 1640 1644 1641 1645 // Check if user has permission to accept this quote 1642 1646 $current_user = wp_get_current_user(); 1643 1647 $is_admin = current_user_can('manage_options'); 1644 1648 1649 $restrict = get_option('easy_invoice_pro_restrict_quote_to_client', 'no'); 1650 1651 if (!$is_admin && $restrict === 'yes') { // For non-admins, check if they are the client 1652 if ($quote->getClientId()) { 1653 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1654 $client = $client_repository->find($quote->getClientId()); 1655 1656 if (!$client || $client->getEmail() !== $current_user->user_email) { 1657 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1658 } 1659 } else { 1660 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1661 } 1662 } 1663 1664 // Update quote status to accepted 1665 $quote->setStatus('accepted'); 1666 $quote->setAcceptedDate(date('Y-m-d H:i:s')); 1667 $quote->setAcceptedBy($current_user->ID); 1668 1669 // Save the quote 1670 $saved = $quote->save(); 1671 1672 if (!$saved) { 1673 wp_die(__('Failed to accept quote.', 'easy-invoice')); 1674 } 1675 1676 // Send notification email to admin 1677 if (!$is_admin) { 1678 $this->sendQuoteAcceptanceNotification($quote); 1679 } 1680 1681 // Redirect back to the quote page with success message 1682 $redirect_url = add_query_arg('action', 'accepted', get_permalink($quote_id)); 1683 wp_redirect($redirect_url); 1684 exit; 1685 } 1686 1687 /** 1688 * Handle decline quote form submission 1689 * 1690 * @since 1.0.0 1691 */ 1692 private function handleDeclineQuoteForm(): void { 1693 // Verify nonce 1694 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) { 1695 wp_die(__('Security check failed.', 'easy-invoice')); 1696 } 1697 1698 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1699 1700 if ($quote_id <= 0) { 1701 wp_die(__('Invalid quote ID.', 'easy-invoice')); 1702 } 1703 1704 // Get the quote 1705 $quote = $this->quote_repository->find($quote_id); 1706 1707 if (!$quote) { 1708 wp_die(__('Quote not found.', 'easy-invoice')); 1709 } 1710 1711 // Check if user has permission to decline this quote 1712 $current_user = wp_get_current_user(); 1713 $is_admin = current_user_can('manage_options'); 1714 1645 1715 if (!$is_admin) { 1646 1716 // For non-admins, check if they are the client … … 1648 1718 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1649 1719 $client = $client_repository->find($quote->getClientId()); 1650 1651 if (!$client || $client->getEmail() !== $current_user->user_email) { 1652 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1653 } 1654 } else { 1655 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1656 } 1657 } 1658 1659 // Update quote status to accepted 1660 $quote->setStatus('accepted'); 1661 $quote->setAcceptedDate(date('Y-m-d H:i:s')); 1662 $quote->setAcceptedBy($current_user->ID); 1663 1664 // Save the quote 1665 $saved = $quote->save(); 1666 1667 if (!$saved) { 1668 wp_die(__('Failed to accept quote.', 'easy-invoice')); 1669 } 1670 1671 // Send notification email to admin 1672 if (!$is_admin) { 1673 $this->sendQuoteAcceptanceNotification($quote); 1674 } 1675 1676 // Redirect back to the quote page with success message 1677 $redirect_url = add_query_arg('action', 'accepted', get_permalink($quote_id)); 1678 wp_redirect($redirect_url); 1679 exit; 1680 } 1681 1682 /** 1683 * Handle decline quote form submission 1684 * 1685 * @since 1.0.0 1686 */ 1687 private function handleDeclineQuoteForm(): void { 1688 // Verify nonce 1689 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) { 1690 wp_die(__('Security check failed.', 'easy-invoice')); 1691 } 1692 1693 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1694 1695 if ($quote_id <= 0) { 1696 wp_die(__('Invalid quote ID.', 'easy-invoice')); 1697 } 1698 1699 // Get the quote 1700 $quote = $this->quote_repository->find($quote_id); 1701 1702 if (!$quote) { 1703 wp_die(__('Quote not found.', 'easy-invoice')); 1704 } 1705 1706 // Check if user has permission to decline this quote 1707 $current_user = wp_get_current_user(); 1708 $is_admin = current_user_can('manage_options'); 1709 1710 if (!$is_admin) { 1711 // For non-admins, check if they are the client 1712 if ($quote->getClientId()) { 1713 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1714 $client = $client_repository->find($quote->getClientId()); 1715 1720 1716 1721 if (!$client || $client->getEmail() !== $current_user->user_email) { 1717 1722 wp_die(__('You do not have permission to decline this quote.', 'easy-invoice')); … … 1721 1726 } 1722 1727 } 1723 1728 1724 1729 // Update quote status to declined 1725 1730 $quote->setStatus('declined'); 1726 1731 $quote->setDeclinedDate(date('Y-m-d H:i:s')); 1727 1732 $quote->setDeclinedBy($current_user->ID); 1728 1733 1729 1734 // Save the quote 1730 1735 $saved = $quote->save(); 1731 1736 1732 1737 if (!$saved) { 1733 1738 wp_die(__('Failed to decline quote.', 'easy-invoice')); 1734 1739 } 1735 1740 1736 1741 // Send notification email to admin 1737 1742 if (!$is_admin) { 1738 1743 $this->sendQuoteDeclineNotification($quote); 1739 1744 } 1740 1745 1741 1746 // Redirect back to the quote page with success message 1742 1747 $redirect_url = add_query_arg('action', 'declined', get_permalink($quote_id)); … … 1744 1749 exit; 1745 1750 } 1746 1751 1747 1752 /** 1748 1753 * Handle AJAX request for bulk quote actions … … 1755 1760 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1756 1761 } 1757 1762 1758 1763 // Check permissions 1759 1764 if (!current_user_can('manage_options')) { 1760 1765 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1761 1766 } 1762 1767 1763 1768 $quote_ids = isset($_POST['quote_ids']) ? array_map('intval', $_POST['quote_ids']) : []; 1764 1769 $bulk_action = sanitize_text_field($_POST['bulk_action'] ?? ''); 1765 1770 1766 1771 if (empty($quote_ids)) { 1767 1772 wp_send_json_error(['message' => __('No quotes selected.', 'easy-invoice')]); 1768 1773 } 1769 1774 1770 1775 if (empty($bulk_action)) { 1771 1776 wp_send_json_error(['message' => __('No action selected.', 'easy-invoice')]); 1772 1777 } 1773 1778 1774 1779 $success_count = 0; 1775 1780 $error_count = 0; 1776 1781 1777 1782 foreach ($quote_ids as $quote_id) { 1778 1783 $quote = $this->quote_repository->find($quote_id); 1779 1784 1780 1785 if (!$quote) { 1781 1786 $error_count++; 1782 1787 continue; 1783 1788 } 1784 1789 1785 1790 try { 1786 1791 switch ($bulk_action) { … … 1793 1798 } 1794 1799 break; 1795 1800 1796 1801 case 'trash': 1797 1802 $old_status = $quote->getStatus(); … … 1804 1809 } 1805 1810 break; 1806 1811 1807 1812 case 'draft': 1808 1813 $old_status = $quote->getStatus(); … … 1815 1820 } 1816 1821 break; 1817 1822 1818 1823 case 'restore': 1819 1824 $old_status = $quote->getStatus(); … … 1826 1831 } 1827 1832 break; 1828 1833 1829 1834 default: 1830 1835 $error_count++; … … 1836 1841 } 1837 1842 } 1838 1843 1839 1844 if ($error_count > 0) { 1840 1845 wp_send_json_success([ … … 1855 1860 } 1856 1861 } 1857 1862 1858 1863 /** 1859 1864 * Handle AJAX request to trash a quote … … 1866 1871 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1867 1872 } 1868 1873 1869 1874 // Check permissions 1870 1875 if (!current_user_can('manage_options')) { 1871 1876 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1872 1877 } 1873 1878 1874 1879 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1875 1880 1876 1881 if ($quote_id <= 0) { 1877 1882 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1878 1883 } 1879 1884 1880 1885 $quote = $this->quote_repository->find($quote_id); 1881 1886 1882 1887 if (!$quote) { 1883 1888 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1884 1889 } 1885 1890 1886 1891 // Set status to cancelled before moving to trash 1887 1892 $old_status = $quote->getStatus(); 1888 1893 $quote->setStatus('cancelled'); 1889 1894 $quote->save(); 1890 1895 1891 1896 // Move the post to trash status 1892 1897 $result = wp_trash_post($quote_id); 1893 1898 1894 1899 if ($result) { 1895 1900 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled'); … … 1905 1910 } 1906 1911 } 1907 1912 1908 1913 /** 1909 1914 * Handle AJAX request to move a quote to draft … … 1916 1921 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1917 1922 } 1918 1923 1919 1924 // Check permissions 1920 1925 if (!current_user_can('manage_options')) { 1921 1926 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1922 1927 } 1923 1928 1924 1929 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1925 1930 1926 1931 if ($quote_id <= 0) { 1927 1932 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1928 1933 } 1929 1934 1930 1935 $quote = $this->quote_repository->find($quote_id); 1931 1936 1932 1937 if (!$quote) { 1933 1938 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1934 1939 } 1935 1940 1936 1941 // Set status to draft 1937 1942 $old_status = $quote->getStatus(); 1938 1943 $quote->setStatus('draft'); 1939 1944 1940 1945 if ($quote->save()) { 1941 1946 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft'); … … 1951 1956 } 1952 1957 } 1953 1958 1954 1959 /** 1955 1960 * Handle AJAX request to restore a trashed quote … … 1962 1967 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1963 1968 } 1964 1969 1965 1970 // Check permissions 1966 1971 if (!current_user_can('manage_options')) { 1967 1972 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1968 1973 } 1969 1974 1970 1975 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1971 1976 1972 1977 if ($quote_id <= 0) { 1973 1978 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1974 1979 } 1975 1980 1976 1981 $quote = $this->quote_repository->find($quote_id); 1977 1982 1978 1983 if (!$quote) { 1979 1984 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1980 1985 } 1981 1986 1982 1987 // Restore the post from trash 1983 1988 $result = wp_untrash_post($quote_id); 1984 1989 1985 1990 if ($result) { 1986 1991 // After restoring from trash, set the meta status to available 1987 1992 $quote->setStatus('available'); 1988 1993 $quote->save(); 1989 1994 1990 1995 $this->quote_log_service->logRestoration($quote_id); 1991 1996 wp_send_json_success([ … … 2000 2005 } 2001 2006 } 2002 2007 2003 2008 /** 2004 2009 * Handle AJAX request to empty trash … … 2012 2017 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 2013 2018 } 2014 2019 2015 2020 // Check permissions 2016 2021 if (!current_user_can('manage_options')) { 2017 2022 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 2018 2023 } 2019 2024 2020 2025 // Get all quotes in trash (post_status = 'trash') 2021 2026 global $wpdb; 2022 2027 $quote_ids = $wpdb->get_col($wpdb->prepare( 2023 "SELECT ID FROM {$wpdb->posts} 2024 WHERE post_type = %s 2028 "SELECT ID FROM {$wpdb->posts} 2029 WHERE post_type = %s 2025 2030 AND post_status = 'trash'", 2026 2031 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE 2027 2032 )); 2028 2033 2029 2034 if (empty($quote_ids)) { 2030 2035 wp_send_json_error(['message' => __('No quotes found in trash.', 'easy-invoice')]); 2031 2036 } 2032 2037 2033 2038 $success_count = 0; 2034 2039 $error_count = 0; 2035 2040 2036 2041 foreach ($quote_ids as $quote_id) { 2037 2042 if (wp_delete_post($quote_id, true)) { … … 2042 2047 } 2043 2048 } 2044 2049 2045 2050 if ($error_count > 0) { 2046 2051 wp_send_json_success([ … … 2064 2069 ]); 2065 2070 } 2066 2071 2067 2072 } catch (\Exception $e) { 2068 2073 error_log('Error emptying quote trash: ' . $e->getMessage()); … … 2073 2078 } 2074 2079 } 2075 2080 2076 2081 /** 2077 2082 * Handle AJAX request to get quote logs … … 2084 2089 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 2085 2090 } 2086 2091 2087 2092 // Check permissions 2088 2093 if (!current_user_can('manage_options')) { 2089 2094 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 2090 2095 } 2091 2096 2092 2097 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 2093 2098 2094 2099 if ($quote_id <= 0) { 2095 2100 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 2096 2101 } 2097 2102 2098 2103 try { 2099 2104 $logs = $this->quote_log_service->getLogs($quote_id); 2100 2105 2101 2106 // Convert QuoteLog objects to arrays for JSON response 2102 2107 $logs_data = []; … … 2113 2118 ]; 2114 2119 } 2115 2120 2116 2121 wp_send_json_success([ 2117 2122 'logs' => $logs_data, 2118 2123 'count' => count($logs_data) 2119 2124 ]); 2120 2125 2121 2126 } catch (\Exception $e) { 2122 2127 wp_send_json_error([ … … 2126 2131 } 2127 2132 } 2128 2133 2129 2134 /** 2130 2135 * Format currency amount using QuoteFormatter … … 2138 2143 return $formatter->format($amount); 2139 2144 } 2140 } 2145 } -
easy-invoice/tags/2.1.7/readme.txt
r3417137 r3420035 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2.1. 67 Stable tag: 2.1.7 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 137 137 138 138 == Changelog == 139 139 = 2.1.7 - 2025-12-15 = 140 * Checked - fix quote accept and decline issue 140 141 = 2.1.6 - 2025-12-11 = 141 142 * Checked - WordPress 6.9 compatibility tested -
easy-invoice/trunk/easy-invoice.php
r3417137 r3420035 4 4 * Plugin URI: https://matrixaddons.com/plugins/easy-invoice 5 5 * Description: A beautiful, full-featured invoicing solution for WordPress. Create professional invoices, quotes, and manage payments with ease. 6 * Version: 2.1. 66 * Version: 2.1.7 7 7 * Author: MatrixAddons 8 8 * Author URI: https://matrixaddons.com … … 25 25 26 26 // Define plugin constants. 27 define( 'EASY_INVOICE_VERSION', '2.1. 6' );27 define( 'EASY_INVOICE_VERSION', '2.1.7' ); 28 28 define( 'EASY_INVOICE_PLUGIN_FILE', __FILE__ ); 29 29 define( 'EASY_INVOICE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
easy-invoice/trunk/includes/Controllers/QuoteController.php
r3399346 r3420035 27 27 */ 28 28 class QuoteController { 29 29 30 30 /** 31 31 * Quote repository … … 34 34 */ 35 35 private $quote_repository; 36 36 37 37 /** 38 38 * Client repository … … 41 41 */ 42 42 private $client_repository; 43 43 44 44 /** 45 45 * Form processor … … 48 48 */ 49 49 private $form_processor; 50 50 51 51 /** 52 52 * Quote log service … … 55 55 */ 56 56 private $quote_log_service; 57 57 58 58 /** 59 59 * Constructor … … 67 67 $this->quote_log_service = new QuoteLogService(); 68 68 } 69 69 70 70 /** 71 71 * Initialize the controller … … 76 76 // Allow plugins to extend the controller initialization 77 77 do_action('easy_invoice_quote_controller_before_init', $this); 78 78 79 79 // Add AJAX handlers 80 80 add_action('wp_ajax_easy_invoice_delete_quote', [$this, 'handleDeleteQuote']); … … 89 89 add_action('wp_ajax_nopriv_easy_invoice_decline_quote', [$this, 'handleDeclineQuote']); 90 90 add_action('wp_ajax_easy_invoice_update_existing_quotes', [$this, 'handleUpdateExistingQuotes']); 91 91 92 92 // Add missing AJAX handlers for quote listing actions 93 93 add_action('wp_ajax_easy_invoice_bulk_quote_action', [$this, 'handleBulkQuoteAction']); 94 94 add_action('wp_ajax_easy_invoice_trash_quote', [$this, 'handleTrashQuote']); 95 95 add_action('wp_ajax_easy_invoice_draft_quote', [$this, 'handleDraftQuote']); 96 96 97 97 // Add regular POST form handlers for quote actions 98 98 add_action('init', [$this, 'handleQuoteFormActions']); 99 99 100 100 // Add new AJAX handler for restoring a trashed quote 101 101 add_action('wp_ajax_easy_invoice_restore_quote', [ $this, 'handleRestoreQuote' ]); 102 102 103 103 // Add new AJAX handler for emptying trash 104 104 add_action('wp_ajax_easy_invoice_empty_trash', [ $this, 'handleEmptyTrash' ]); 105 105 106 106 // Add new AJAX handler for getting quote logs 107 107 add_action('wp_ajax_easy_invoice_get_quote_logs', [ $this, 'handleGetQuoteLogs' ]); 108 108 109 109 // Allow plugins to extend the controller initialization 110 110 do_action('easy_invoice_quote_controller_after_init', $this); 111 111 } 112 112 113 113 /** 114 114 * Display quote pages … … 120 120 // Allow plugins to modify display arguments 121 121 $args = apply_filters('easy_invoice_quote_controller_display_args', $args); 122 122 123 123 $page = $args['page'] ?? ''; 124 124 125 125 // Allow plugins to modify the page before processing 126 126 $page = apply_filters('easy_invoice_quote_controller_display_page', $page, $args); 127 127 128 128 switch ($page) { 129 129 case PagesSlugs::ALL_QUOTES: 130 130 $this->displayListing(); 131 131 break; 132 132 133 133 case PagesSlugs::QUOTE_NEW: 134 134 $this->displayBuilder(); 135 135 break; 136 136 137 137 case PagesSlugs::QUOTE_PREVIEW: 138 138 $this->displayPreview($args); 139 139 break; 140 140 141 141 default: 142 142 $this->displayListing(); 143 143 break; 144 144 } 145 145 146 146 // Allow plugins to perform actions after display 147 147 do_action('easy_invoice_quote_controller_after_display', $page, $args); 148 148 } 149 149 150 150 /** 151 151 * Display quote listing page … … 159 159 // Get trash count first (based on post_status) 160 160 $trash_count = (int)$wpdb->get_var($wpdb->prepare( 161 "SELECT COUNT(*) FROM {$wpdb->posts} 161 "SELECT COUNT(*) FROM {$wpdb->posts} 162 162 WHERE post_type = %s AND post_status = 'trash'", 163 163 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE 164 164 )); 165 165 166 166 // Get counts for each meta status (excluding trashed posts) 167 167 $status_counts = $wpdb->get_results($wpdb->prepare( 168 "SELECT COALESCE(pm.meta_value, 'draft') as status, COUNT(*) as count 169 FROM {$wpdb->posts} p 168 "SELECT COALESCE(pm.meta_value, 'draft') as status, COUNT(*) as count 169 FROM {$wpdb->posts} p 170 170 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_easy_invoice_quote_status' 171 WHERE p.post_type = %s 171 WHERE p.post_type = %s 172 172 AND p.post_status != 'trash' 173 173 GROUP BY COALESCE(pm.meta_value, 'draft')", … … 189 189 $count = (int)$status->count; 190 190 $all_count += $count; // Add to total (excluding trash) 191 191 192 192 switch ($status->status) { 193 193 case 'draft': … … 218 218 // Allow plugins to perform actions before displaying listing 219 219 do_action('easy_invoice_quote_controller_before_display_listing'); 220 220 221 221 // Get filter parameters 222 222 $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : ''; … … 242 242 // For trash and cancelled views, look at post_status = 'trash' 243 243 $query_args['post_status'] = 'trash'; 244 244 245 245 // For cancelled view, also filter by meta status 246 246 if ($current_view === 'cancelled') { … … 256 256 // For all other views, exclude trashed posts 257 257 $query_args['post_status'] = ['publish', 'draft', 'private', 'pending']; 258 258 259 259 if ($current_view !== 'all') { 260 260 // For specific status views, add meta query … … 272 272 if (!empty($search_query)) { 273 273 $search_ids = []; 274 274 275 275 // Build base query args for search 276 276 $search_query_args = [ … … 310 310 ]); 311 311 $meta_search = new \WP_Query($meta_search_args); 312 312 313 313 if ($meta_search->have_posts()) { 314 314 $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID')); 315 315 } 316 316 317 317 $search_ids = array_unique($search_ids); 318 318 319 319 if (!empty($search_ids)) { 320 320 $query_args['post__in'] = $search_ids; … … 329 329 $wp_query = new \WP_Query($query_args); 330 330 $quotes = []; 331 331 332 332 if ($wp_query->have_posts()) { 333 333 foreach ($wp_query->posts as $post) { … … 341 341 // Allow plugins to modify the quotes array 342 342 $quotes = apply_filters('easy_invoice_quote_controller_quotes_list', $quotes, $wp_query); 343 343 344 344 // Get pagination info from WordPress query 345 345 $total_quotes = $wp_query->found_posts; … … 381 381 } 382 382 } 383 383 384 384 // Prepare template data 385 385 $template_data = [ … … 404 404 'wp_query' => $wp_query 405 405 ]; 406 406 407 407 // Allow plugins to modify template data 408 408 $template_data = apply_filters('easy_invoice_quote_controller_template_data', $template_data); 409 409 410 410 // Display the template 411 411 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/listing.php'; 412 412 413 413 // Allow plugins to perform actions after displaying listing 414 414 do_action('easy_invoice_quote_controller_after_display_listing', $template_data); 415 415 } 416 416 417 417 /** 418 418 * Display quote builder page … … 423 423 // Allow plugins to perform actions before displaying builder 424 424 do_action('easy_invoice_quote_controller_before_display_builder'); 425 425 426 426 $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0; 427 427 $quote = null; 428 428 429 429 if ($quote_id > 0) { 430 430 $quote = $this->quote_repository->find($quote_id); 431 431 } 432 432 433 433 $clients = $this->client_repository->all(); 434 434 435 435 // Allow plugins to modify the data 436 436 $quote = apply_filters('easy_invoice_quote_controller_builder_quote', $quote, $quote_id); … … 439 439 // Include the builder template 440 440 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/builder.php'; 441 441 442 442 // Allow plugins to perform actions after displaying builder 443 443 do_action('easy_invoice_quote_controller_after_display_builder', $quote, $clients); 444 444 } 445 445 446 446 /** 447 447 * Display quote preview page … … 453 453 // Allow plugins to perform actions before displaying preview 454 454 do_action('easy_invoice_quote_controller_before_display_preview', $args); 455 455 456 456 $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0; 457 457 458 458 if ($quote_id <= 0) { 459 459 wp_die(__('Quote not found.', 'easy-invoice')); 460 460 } 461 461 462 462 $quote = $this->quote_repository->find($quote_id); 463 463 if (!$quote) { 464 464 wp_die(__('Quote not found.', 'easy-invoice')); 465 465 } 466 466 467 467 // Allow plugins to modify the quote 468 468 $quote = apply_filters('easy_invoice_quote_controller_preview_quote', $quote, $quote_id); 469 469 470 470 // Include the preview template 471 471 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/preview.php'; 472 472 473 473 // Allow plugins to perform actions after displaying preview 474 474 do_action('easy_invoice_quote_controller_after_display_preview', $quote, $args); 475 475 } 476 476 477 477 /** 478 478 * Handle delete quote AJAX request … … 485 485 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 486 486 } 487 487 488 488 // Check permissions 489 489 if (!current_user_can('manage_options')) { 490 490 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 491 491 } 492 492 493 493 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 494 494 495 495 if ($quote_id <= 0) { 496 496 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 497 497 } 498 498 499 499 if ($this->quote_repository->delete($quote_id)) { 500 500 // Log the quote deletion 501 501 $this->quote_log_service->logDeletion($quote_id); 502 502 503 503 wp_send_json_success([ 504 504 'message' => __('Quote deleted successfully.', 'easy-invoice'), … … 512 512 } 513 513 } 514 514 515 515 /** 516 516 * Handle get quote AJAX request … … 523 523 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 524 524 } 525 525 526 526 // Check permissions 527 527 if (!current_user_can('manage_options')) { 528 528 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 529 529 } 530 530 531 531 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 532 532 533 533 if ($quote_id <= 0) { 534 534 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 535 535 } 536 536 537 537 $quote = $this->quote_repository->find($quote_id); 538 538 539 539 if (!$quote) { 540 540 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 541 541 } 542 542 543 543 wp_send_json_success(['quote' => $quote->toArray()]); 544 544 } 545 545 546 546 /** 547 547 * Handle AJAX request to load quote template … … 554 554 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 555 555 } 556 556 557 557 // Check permissions 558 558 if (!current_user_can('manage_options')) { 559 559 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 560 560 } 561 561 562 562 $template_id = sanitize_text_field($_POST['template'] ?? ''); 563 563 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 564 564 565 565 if (empty($template_id)) { 566 566 wp_send_json_error(['message' => __('Template ID is required.', 'easy-invoice')]); 567 567 } 568 568 569 569 // Validate template name securely 570 570 $template_id = $this->validateTemplateName($template_id, 'quote'); 571 571 572 572 // Get secure template file path 573 573 $template_file = $this->getSecureTemplatePath($template_id, 'quote'); 574 574 575 575 if (!$template_file) { 576 576 wp_send_json_error(['message' => __('Template not found.', 'easy-invoice')]); 577 577 } 578 578 579 579 // Load quote if provided 580 580 $quote = null; … … 582 582 $quote = $this->quote_repository->find($quote_id); 583 583 } 584 584 585 585 // Start output buffering to capture template HTML 586 586 ob_start(); 587 587 588 588 // Include the template file 589 589 include $template_file; 590 590 591 591 // Get the captured HTML 592 592 $html = ob_get_clean(); 593 593 594 594 wp_send_json_success(['html' => $html]); 595 595 } … … 597 597 /** 598 598 * Validate and sanitize template name to prevent directory traversal attacks 599 * 599 * 600 600 * @param string $template The template name to validate 601 601 * @param string $type Either 'invoice' or 'quote' … … 611 611 // Strip any directory components using basename 612 612 $template = basename($template); 613 613 614 614 // Remove any file extension 615 615 $template = preg_replace('/\.(php|html|htm)$/i', '', $template); 616 616 617 617 // Remove any non-alphanumeric characters except hyphens and underscores 618 618 $template = preg_replace('/[^a-z0-9_-]/i', '', $template); 619 619 620 620 // Check if template is in whitelist 621 621 if (isset($allowed_templates[$type]) && in_array($template, $allowed_templates[$type], true)) { 622 622 return $template; 623 623 } 624 624 625 625 // Return default template if not in whitelist 626 626 return 'standard'; … … 629 629 /** 630 630 * Get secure template file path with directory traversal protection 631 * 631 * 632 632 * @param string $template The validated template name 633 633 * @param string $type Either 'invoice' or 'quote' … … 646 646 647 647 $template_dir = $template_dirs[$type]; 648 648 649 649 // Ensure template directory exists and is a directory 650 650 if (!is_dir($template_dir)) { … … 660 660 // Construct the template file path 661 661 $template_file = $real_template_dir . DIRECTORY_SEPARATOR . $template . '.php'; 662 662 663 663 // Get the real path of the template file (resolves any .. or . components) 664 664 $real_template_file = realpath($template_file); 665 665 666 666 // Verify that the resolved path is within the template directory 667 667 // This prevents directory traversal attacks … … 670 670 $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php'; 671 671 $real_default_file = realpath($default_file); 672 672 673 673 if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0) { 674 674 return $real_default_file; 675 675 } 676 676 677 677 return false; 678 678 } … … 683 683 $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php'; 684 684 $real_default_file = realpath($default_file); 685 685 686 686 if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0 && is_file($real_default_file) && is_readable($real_default_file)) { 687 687 return $real_default_file; 688 688 } 689 689 690 690 return false; 691 691 } … … 693 693 return $real_template_file; 694 694 } 695 695 696 696 /** 697 697 * Handle AJAX request to create a new quote with just the title … … 712 712 wp_send_json_error(['message' => __('Quote title is required.', 'easy-invoice')]); 713 713 } 714 714 715 715 // Generate a unique quote number 716 716 $quote_number = ''; … … 722 722 $quote_number = 'QT-' . str_pad(time(), 6, '0', STR_PAD_LEFT); 723 723 } 724 724 725 725 // Get global quote settings 726 726 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 732 732 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice')); 733 733 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice')); 734 734 735 735 // Create the quote with just the title and default values 736 736 $data = [ … … 756 756 wp_send_json_success(['quote_id' => $quote->getId()]); 757 757 } 758 758 759 759 /** 760 760 * Handle AJAX request to load quote form for modal … … 767 767 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 768 768 } 769 769 770 770 // Check permissions 771 771 if (!current_user_can('manage_options')) { 772 772 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 773 773 } 774 774 775 775 // Get global quote settings 776 776 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 782 782 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice')); 783 783 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice')); 784 784 785 785 // Create a new quote object for the form 786 786 $quote_number_service = function_exists('easy_invoice_get_quote_number_service') ? easy_invoice_get_quote_number_service() : null; … … 815 815 'declined_message' => $quote_declined_message, // Use global declined message setting 816 816 ); 817 817 818 818 // Create a temporary WP_Post object for new quote 819 819 $empty_post = new \WP_Post((object) array( … … 837 837 'filter' => 'raw', 838 838 )); 839 839 840 840 $quote = new \EasyInvoice\Models\Quote($empty_post); 841 841 842 842 // Set default values on the quote object 843 843 foreach ($quote_data as $key => $value) { … … 868 868 } 869 869 } 870 870 871 871 // Initialize empty items array 872 872 $quote->setItems([]); 873 873 874 874 // Set variables needed by the form template 875 875 $quote_id = 0; … … 879 879 $admin_nonce = wp_create_nonce('easy_invoice_admin_nonce'); 880 880 $quote_field_config = $quote_form_manager->getFieldConfigForJavaScript(); 881 881 882 882 // Start output buffering to capture form HTML 883 883 ob_start(); 884 884 885 885 // Include the quote form template 886 886 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/form.php'; 887 887 888 888 // Get the captured HTML 889 889 $html = ob_get_clean(); 890 890 891 891 wp_send_json_success(['html' => $html]); 892 892 } 893 893 894 894 /** 895 895 * Handle search clients AJAX request … … 902 902 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 903 903 } 904 904 905 905 // Check permissions 906 906 if (!current_user_can('manage_options')) { 907 907 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]); 908 908 } 909 909 910 910 $query = sanitize_text_field($_POST['query'] ?? ''); 911 911 912 912 // If query is empty, get all clients 913 913 if (empty($query)) { … … 917 917 $clients = $this->client_repository->search($query); 918 918 } 919 919 920 920 $results = []; 921 921 foreach ($clients as $client) { … … 930 930 ]; 931 931 } 932 932 933 933 wp_send_json_success($results); 934 934 } 935 935 936 936 /** 937 937 * Handle AJAX request to accept a quote … … 944 944 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 945 945 } 946 946 947 947 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 948 948 949 949 if ($quote_id <= 0) { 950 950 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 951 951 } 952 952 953 953 // Get the quote 954 954 $quote = $this->quote_repository->find($quote_id); 955 955 956 956 if (!$quote) { 957 957 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 958 958 } 959 959 960 960 // Check if user has permission to accept this quote 961 961 $current_user = wp_get_current_user(); 962 962 $is_admin = current_user_can('manage_options'); 963 964 if (!$is_admin) { 965 // For non-admins, check if they are the client 963 964 $restrict = get_option('easy_invoice_pro_restrict_quote_to_client', 'no'); 965 966 if (!$is_admin && $restrict === 'yes') { 967 // For non-admins, check if they are the client 966 968 if ($quote->getClientId()) { 967 969 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 968 970 $client = $client_repository->find($quote->getClientId()); 969 971 970 972 if (!$client || $client->getEmail() !== $current_user->user_email) { 971 973 wp_send_json_error(['message' => __('You do not have permission to accept this quote.', 'easy-invoice')]); … … 975 977 } 976 978 } 977 979 978 980 // Get global accept action setting 979 981 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); 980 982 $accept_action = $settings_controller::getQuoteAcceptAction(); 981 983 982 984 // Update quote status to accepted 983 985 $quote->setStatus('accepted'); 984 986 $quote->setAcceptedDate(date('Y-m-d H:i:s')); 985 987 $quote->setAcceptedBy($current_user->ID); 986 988 987 989 // Save the quote 988 990 $saved = $quote->save(); 989 991 990 992 if (!$saved) { 991 993 wp_send_json_error(['message' => __('Failed to accept quote.', 'easy-invoice')]); 992 994 } 993 995 994 996 // Log the quote acceptance 995 997 $this->quote_log_service->logAcceptance($quote_id, [ … … 997 999 'user_type' => $is_admin ? 'admin' : 'client' 998 1000 ]); 999 1001 1000 1002 // Perform the configured accept action 1001 1003 $invoice_id = null; 1002 1004 $action_message = ''; 1003 1005 1004 1006 switch ($accept_action) { 1005 1007 case 'convert': … … 1011 1013 $action_message = __('Quote converted to invoice successfully.', 'easy-invoice'); 1012 1014 break; 1013 1015 1014 1016 case 'convert_available': 1015 1017 // Convert quote to invoice (Available status) … … 1020 1022 $action_message = __('Quote converted to invoice successfully.', 'easy-invoice'); 1021 1023 break; 1022 1024 1023 1025 case 'convert_send': 1024 1026 // Convert quote to invoice and send to client (Available status) … … 1029 1031 $action_message = __('Quote converted to invoice and sent to client successfully.', 'easy-invoice'); 1030 1032 break; 1031 1033 1032 1034 case 'duplicate': 1033 1035 // Create new invoice, keep quote as-is (Draft status) … … 1038 1040 $action_message = __('New invoice created from quote successfully.', 'easy-invoice'); 1039 1041 break; 1040 1042 1041 1043 case 'duplicate_send': 1042 1044 // Create new invoice and send to client, keep quote as-is (Available status) … … 1047 1049 $action_message = __('New invoice created and sent to client successfully.', 'easy-invoice'); 1048 1050 break; 1049 1051 1050 1052 case 'do_nothing': 1051 1053 default: … … 1054 1056 break; 1055 1057 } 1056 1058 1057 1059 // Send notification email to admin 1058 1060 if (!$is_admin) { 1059 1061 $this->sendQuoteAcceptanceNotification($quote); 1060 1062 } 1061 1063 1062 1064 // Get URLs for the new invoice 1063 1065 $invoice_url = null; 1064 1066 $secure_url = null; 1065 1067 1066 1068 if ($invoice_id) { 1067 1069 // Always use WordPress permalink … … 1075 1077 } 1076 1078 } 1077 1079 1078 1080 wp_send_json_success([ 1079 1081 'message' => $action_message, … … 1087 1089 ]); 1088 1090 } 1089 1091 1090 1092 /** 1091 1093 * Convert quote to invoice … … 1099 1101 // Get invoice repository 1100 1102 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository(); 1101 1103 1102 1104 // Create invoice data from quote - convert ALL fields 1103 1105 $invoice_data = [ … … 1135 1137 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields 1136 1138 ]; 1137 1139 1138 1140 // Create the invoice 1139 1141 $invoice = $invoice_repository->create($invoice_data); 1140 1142 1141 1143 if ($invoice) { 1142 1144 // Store the quote ID in the invoice's meta for tracking 1143 1145 update_post_meta($invoice->getId(), '_converted_from_quote', $quote->getId()); 1144 1146 1145 1147 // Update quote to reference the created invoice 1146 1148 $quote->setCustomField('converted_invoice_id', $invoice->getId()); 1147 1149 $quote->save(); 1148 1150 1149 1151 // Ensure secure link is generated for the new invoice (Pro version) 1150 1152 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) { … … 1152 1154 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId())); 1153 1155 } 1154 1156 1155 1157 return $invoice->getId(); 1156 1158 } 1157 1159 1158 1160 return null; 1159 1161 } catch (\Exception $e) { … … 1162 1164 } 1163 1165 } 1164 1166 1165 1167 /** 1166 1168 * Create new invoice from quote (duplicate) … … 1174 1176 // Get invoice repository 1175 1177 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository(); 1176 1178 1177 1179 // Create invoice data from quote - convert ALL fields 1178 1180 $invoice_data = [ … … 1210 1212 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields 1211 1213 ]; 1212 1214 1213 1215 // Create the invoice 1214 1216 $invoice = $invoice_repository->create($invoice_data); 1215 1217 1216 1218 if ($invoice) { 1217 1219 // Link the invoice to the quote 1218 1220 $quote->setCustomField('related_invoice_id', $invoice->getId()); 1219 1221 $quote->save(); 1220 1222 1221 1223 // Ensure secure link is generated for the new invoice (Pro version) 1222 1224 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) { … … 1224 1226 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId())); 1225 1227 } 1226 1228 1227 1229 return $invoice->getId(); 1228 1230 } 1229 1231 1230 1232 return null; 1231 1233 } catch (\Exception $e) { … … 1234 1236 } 1235 1237 } 1236 1238 1237 1239 /** 1238 1240 * Send invoice to client … … 1246 1248 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository(); 1247 1249 $invoice = $invoice_repository->find($invoice_id); 1248 1250 1249 1251 if (!$invoice) { 1250 1252 return false; 1251 1253 } 1252 1254 1253 1255 // Get email manager 1254 1256 $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); 1255 1257 1256 1258 // Send invoice email 1257 1259 $result = $email_manager->sendInvoiceEmail($invoice, 'new'); 1258 1260 1259 1261 return $result['success']; 1260 1262 } catch (\Exception $e) { … … 1263 1265 } 1264 1266 } 1265 1267 1266 1268 /** 1267 1269 * Convert quote items to invoice items … … 1272 1274 private function convertQuoteItemsToInvoiceItems(array $quote_items): array { 1273 1275 $invoice_items = []; 1274 1276 1275 1277 foreach ($quote_items as $quote_item) { 1276 1278 if (is_object($quote_item) && method_exists($quote_item, 'toArray')) { … … 1300 1302 } 1301 1303 } 1302 1304 1303 1305 return $invoice_items; 1304 1306 } 1305 1307 1306 1308 /** 1307 1309 * Generate unique invoice number … … 1315 1317 return $invoice_number_service->generateUniqueNumber(); 1316 1318 } 1317 1319 1318 1320 // Fallback to timestamp-based number 1319 1321 return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT); 1320 1322 } 1321 1323 1322 1324 /** 1323 1325 * Get changes between two quote versions … … 1329 1331 private function getQuoteChanges($old_quote, $new_quote): array { 1330 1332 $changes = []; 1331 1333 1332 1334 // Compare key fields 1333 1335 $fields_to_compare = [ … … 1343 1345 'terms' => 'Terms', 1344 1346 ]; 1345 1347 1346 1348 foreach ($fields_to_compare as $field => $label) { 1347 1349 $method_name = 'get' . easy_invoice_str_replace('_', '', ucwords($field, '_')); 1348 1350 1349 1351 if (method_exists($old_quote, $method_name) && method_exists($new_quote, $method_name)) { 1350 1352 $old_value = $old_quote->$method_name(); 1351 1353 $new_value = $new_quote->$method_name(); 1352 1354 1353 1355 if ($old_value !== $new_value) { 1354 1356 $changes[$field] = $new_value; … … 1356 1358 } 1357 1359 } 1358 1360 1359 1361 return $changes; 1360 1362 } 1361 1363 1362 1364 /** 1363 1365 * Handle AJAX request to decline a quote … … 1370 1372 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1371 1373 } 1372 1374 1373 1375 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1374 1376 $decline_reason = isset($_POST['decline_reason']) ? sanitize_textarea_field($_POST['decline_reason']) : ''; 1375 1377 1376 1378 if ($quote_id <= 0) { 1377 1379 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1378 1380 } 1379 1381 1380 1382 // Get the quote 1381 1383 $quote = $this->quote_repository->find($quote_id); 1382 1384 1383 1385 if (!$quote) { 1384 1386 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1385 1387 } 1386 1388 1387 1389 // Check if decline reason is required by global settings 1388 1390 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 1390 1392 wp_send_json_error(['message' => __('Reason for declining is required.', 'easy-invoice')]); 1391 1393 } 1392 1394 1393 1395 // Check if user has permission to decline this quote 1394 1396 $current_user = wp_get_current_user(); 1395 1397 $is_admin = current_user_can('manage_options'); 1396 1397 if (!$is_admin) { 1398 1399 $restrict = get_option('easy_invoice_pro_restrict_quote_to_client', 'no'); 1400 1401 if (!$is_admin && $restrict === 'yes') { 1398 1402 // For non-admins, check if they are the client 1399 1403 if ($quote->getClientId()) { 1400 1404 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1401 1405 $client = $client_repository->find($quote->getClientId()); 1402 1406 1403 1407 if (!$client || $client->getEmail() !== $current_user->user_email) { 1404 1408 wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]); … … 1408 1412 } 1409 1413 } 1410 1414 1411 1415 // Update quote status to declined 1412 1416 $quote->setStatus('declined'); 1413 1417 $quote->setDeclinedDate(date('Y-m-d H:i:s')); 1414 1418 $quote->setDeclinedBy($current_user->ID); 1415 1419 1416 1420 // Save decline reason if provided 1417 1421 if (!empty($decline_reason)) { 1418 1422 $quote->setDeclineReason($decline_reason); 1419 1423 } 1420 1424 1421 1425 // Save the quote 1422 1426 $saved = $quote->save(); 1423 1427 1424 1428 if (!$saved) { 1425 1429 wp_send_json_error(['message' => __('Failed to decline quote.', 'easy-invoice')]); 1426 1430 } 1427 1431 1428 1432 // Log the quote decline 1429 1433 $this->quote_log_service->logDecline($quote_id, $decline_reason, [ 1430 1434 'user_type' => $is_admin ? 'admin' : 'client' 1431 1435 ]); 1432 1436 1433 1437 // Send notification email to admin 1434 1438 if (!$is_admin) { 1435 1439 $this->sendQuoteDeclineNotification($quote); 1436 1440 } 1437 1441 1438 1442 wp_send_json_success([ 1439 1443 'message' => __('Quote declined successfully.', 'easy-invoice'), … … 1444 1448 ]); 1445 1449 } 1446 1450 1447 1451 /** 1448 1452 * Send quote acceptance notification to admin … … 1455 1459 $email_manager->sendAdminQuoteNotification($quote, 'accepted'); 1456 1460 } 1457 1461 1458 1462 /** 1459 1463 * Send quote decline notification to admin … … 1466 1470 $email_manager->sendAdminQuoteNotification($quote, 'declined'); 1467 1471 } 1468 1472 1469 1473 /** 1470 1474 * Handle AJAX request to update existing quotes with missing data … … 1477 1481 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1478 1482 } 1479 1483 1480 1484 // Check permissions 1481 1485 if (!current_user_can('manage_options')) { 1482 1486 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1483 1487 } 1484 1488 1485 1489 $updated_count = 0; 1486 1490 $quotes = $this->quote_repository->findAll(); 1487 1491 1488 1492 foreach ($quotes as $quote) { 1489 1493 $post = get_post($quote->getId()); … … 1492 1496 $post_title = $quote->getTitle() ?: $quote->getNumber() ?: 'Untitled Quote'; 1493 1497 $post_name = sanitize_title($post_title); 1494 1498 1495 1499 // Ensure uniqueness 1496 1500 $original_slug = $post_name; … … 1500 1504 $counter++; 1501 1505 } 1502 1506 1503 1507 // Update the post with the new slug 1504 1508 wp_update_post([ … … 1506 1510 'post_name' => $post_name 1507 1511 ]); 1508 1512 1509 1513 $updated_count++; 1510 1514 } 1511 1515 } 1512 1516 1513 1517 wp_send_json_success([ 1514 1518 'message' => sprintf(__('Updated %d quotes with proper URLs.', 'easy-invoice'), $updated_count) 1515 1519 ]); 1516 1520 } 1517 1521 1518 1522 /** 1519 1523 * Handle AJAX request to duplicate a quote … … 1526 1530 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1527 1531 } 1528 1532 1529 1533 // Check permissions 1530 1534 if (!current_user_can('manage_options')) { 1531 1535 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1532 1536 } 1533 1537 1534 1538 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1535 1539 1536 1540 if ($quote_id <= 0) { 1537 1541 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1538 1542 } 1539 1543 1540 1544 $quote = $this->quote_repository->find($quote_id); 1541 1545 1542 1546 if (!$quote) { 1543 1547 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1544 1548 } 1545 1549 1546 1550 // Get global quote settings 1547 1551 $settings_controller = new \EasyInvoice\Controllers\SettingsController(); … … 1553 1557 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice')); 1554 1558 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice')); 1555 1559 1556 1560 // Create the duplicate quote 1557 1561 $duplicate_data = [ … … 1572 1576 'declined_message' => $quote_declined_message, 1573 1577 ]; 1574 1578 1575 1579 // Set client ID to 0 for a new quote 1576 1580 $duplicate_data['client_id'] = 0; 1577 1581 1578 1582 $duplicate_quote = $this->quote_repository->create($duplicate_data); 1579 1583 1580 1584 if ($duplicate_quote) { 1581 1585 $this->quote_log_service->logActivity($quote_id, 'duplicate', 'Quote duplicated', ['duplicate_id' => $duplicate_quote->getId()]); … … 1592 1596 } 1593 1597 } 1594 1598 1595 1599 /** 1596 1600 * Handle regular POST form actions for quote accept/decline … … 1603 1607 return; 1604 1608 } 1605 1609 1606 1610 // Handle accept quote 1607 1611 if (isset($_POST['accept_quote']) && isset($_POST['quote_id'])) { 1608 1612 $this->handleAcceptQuoteForm(); 1609 1613 } 1610 1614 1611 1615 // Handle decline quote 1612 1616 if (isset($_POST['decline_quote']) && isset($_POST['quote_id'])) { … … 1614 1618 } 1615 1619 } 1616 1620 1617 1621 /** 1618 1622 * Handle accept quote form submission … … 1625 1629 wp_die(__('Security check failed.', 'easy-invoice')); 1626 1630 } 1627 1631 1628 1632 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1629 1633 1630 1634 if ($quote_id <= 0) { 1631 1635 wp_die(__('Invalid quote ID.', 'easy-invoice')); 1632 1636 } 1633 1637 1634 1638 // Get the quote 1635 1639 $quote = $this->quote_repository->find($quote_id); 1636 1640 1637 1641 if (!$quote) { 1638 1642 wp_die(__('Quote not found.', 'easy-invoice')); 1639 1643 } 1640 1644 1641 1645 // Check if user has permission to accept this quote 1642 1646 $current_user = wp_get_current_user(); 1643 1647 $is_admin = current_user_can('manage_options'); 1644 1648 1649 $restrict = get_option('easy_invoice_pro_restrict_quote_to_client', 'no'); 1650 1651 if (!$is_admin && $restrict === 'yes') { // For non-admins, check if they are the client 1652 if ($quote->getClientId()) { 1653 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1654 $client = $client_repository->find($quote->getClientId()); 1655 1656 if (!$client || $client->getEmail() !== $current_user->user_email) { 1657 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1658 } 1659 } else { 1660 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1661 } 1662 } 1663 1664 // Update quote status to accepted 1665 $quote->setStatus('accepted'); 1666 $quote->setAcceptedDate(date('Y-m-d H:i:s')); 1667 $quote->setAcceptedBy($current_user->ID); 1668 1669 // Save the quote 1670 $saved = $quote->save(); 1671 1672 if (!$saved) { 1673 wp_die(__('Failed to accept quote.', 'easy-invoice')); 1674 } 1675 1676 // Send notification email to admin 1677 if (!$is_admin) { 1678 $this->sendQuoteAcceptanceNotification($quote); 1679 } 1680 1681 // Redirect back to the quote page with success message 1682 $redirect_url = add_query_arg('action', 'accepted', get_permalink($quote_id)); 1683 wp_redirect($redirect_url); 1684 exit; 1685 } 1686 1687 /** 1688 * Handle decline quote form submission 1689 * 1690 * @since 1.0.0 1691 */ 1692 private function handleDeclineQuoteForm(): void { 1693 // Verify nonce 1694 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) { 1695 wp_die(__('Security check failed.', 'easy-invoice')); 1696 } 1697 1698 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1699 1700 if ($quote_id <= 0) { 1701 wp_die(__('Invalid quote ID.', 'easy-invoice')); 1702 } 1703 1704 // Get the quote 1705 $quote = $this->quote_repository->find($quote_id); 1706 1707 if (!$quote) { 1708 wp_die(__('Quote not found.', 'easy-invoice')); 1709 } 1710 1711 // Check if user has permission to decline this quote 1712 $current_user = wp_get_current_user(); 1713 $is_admin = current_user_can('manage_options'); 1714 1645 1715 if (!$is_admin) { 1646 1716 // For non-admins, check if they are the client … … 1648 1718 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1649 1719 $client = $client_repository->find($quote->getClientId()); 1650 1651 if (!$client || $client->getEmail() !== $current_user->user_email) { 1652 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1653 } 1654 } else { 1655 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice')); 1656 } 1657 } 1658 1659 // Update quote status to accepted 1660 $quote->setStatus('accepted'); 1661 $quote->setAcceptedDate(date('Y-m-d H:i:s')); 1662 $quote->setAcceptedBy($current_user->ID); 1663 1664 // Save the quote 1665 $saved = $quote->save(); 1666 1667 if (!$saved) { 1668 wp_die(__('Failed to accept quote.', 'easy-invoice')); 1669 } 1670 1671 // Send notification email to admin 1672 if (!$is_admin) { 1673 $this->sendQuoteAcceptanceNotification($quote); 1674 } 1675 1676 // Redirect back to the quote page with success message 1677 $redirect_url = add_query_arg('action', 'accepted', get_permalink($quote_id)); 1678 wp_redirect($redirect_url); 1679 exit; 1680 } 1681 1682 /** 1683 * Handle decline quote form submission 1684 * 1685 * @since 1.0.0 1686 */ 1687 private function handleDeclineQuoteForm(): void { 1688 // Verify nonce 1689 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) { 1690 wp_die(__('Security check failed.', 'easy-invoice')); 1691 } 1692 1693 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1694 1695 if ($quote_id <= 0) { 1696 wp_die(__('Invalid quote ID.', 'easy-invoice')); 1697 } 1698 1699 // Get the quote 1700 $quote = $this->quote_repository->find($quote_id); 1701 1702 if (!$quote) { 1703 wp_die(__('Quote not found.', 'easy-invoice')); 1704 } 1705 1706 // Check if user has permission to decline this quote 1707 $current_user = wp_get_current_user(); 1708 $is_admin = current_user_can('manage_options'); 1709 1710 if (!$is_admin) { 1711 // For non-admins, check if they are the client 1712 if ($quote->getClientId()) { 1713 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository(); 1714 $client = $client_repository->find($quote->getClientId()); 1715 1720 1716 1721 if (!$client || $client->getEmail() !== $current_user->user_email) { 1717 1722 wp_die(__('You do not have permission to decline this quote.', 'easy-invoice')); … … 1721 1726 } 1722 1727 } 1723 1728 1724 1729 // Update quote status to declined 1725 1730 $quote->setStatus('declined'); 1726 1731 $quote->setDeclinedDate(date('Y-m-d H:i:s')); 1727 1732 $quote->setDeclinedBy($current_user->ID); 1728 1733 1729 1734 // Save the quote 1730 1735 $saved = $quote->save(); 1731 1736 1732 1737 if (!$saved) { 1733 1738 wp_die(__('Failed to decline quote.', 'easy-invoice')); 1734 1739 } 1735 1740 1736 1741 // Send notification email to admin 1737 1742 if (!$is_admin) { 1738 1743 $this->sendQuoteDeclineNotification($quote); 1739 1744 } 1740 1745 1741 1746 // Redirect back to the quote page with success message 1742 1747 $redirect_url = add_query_arg('action', 'declined', get_permalink($quote_id)); … … 1744 1749 exit; 1745 1750 } 1746 1751 1747 1752 /** 1748 1753 * Handle AJAX request for bulk quote actions … … 1755 1760 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1756 1761 } 1757 1762 1758 1763 // Check permissions 1759 1764 if (!current_user_can('manage_options')) { 1760 1765 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1761 1766 } 1762 1767 1763 1768 $quote_ids = isset($_POST['quote_ids']) ? array_map('intval', $_POST['quote_ids']) : []; 1764 1769 $bulk_action = sanitize_text_field($_POST['bulk_action'] ?? ''); 1765 1770 1766 1771 if (empty($quote_ids)) { 1767 1772 wp_send_json_error(['message' => __('No quotes selected.', 'easy-invoice')]); 1768 1773 } 1769 1774 1770 1775 if (empty($bulk_action)) { 1771 1776 wp_send_json_error(['message' => __('No action selected.', 'easy-invoice')]); 1772 1777 } 1773 1778 1774 1779 $success_count = 0; 1775 1780 $error_count = 0; 1776 1781 1777 1782 foreach ($quote_ids as $quote_id) { 1778 1783 $quote = $this->quote_repository->find($quote_id); 1779 1784 1780 1785 if (!$quote) { 1781 1786 $error_count++; 1782 1787 continue; 1783 1788 } 1784 1789 1785 1790 try { 1786 1791 switch ($bulk_action) { … … 1793 1798 } 1794 1799 break; 1795 1800 1796 1801 case 'trash': 1797 1802 $old_status = $quote->getStatus(); … … 1804 1809 } 1805 1810 break; 1806 1811 1807 1812 case 'draft': 1808 1813 $old_status = $quote->getStatus(); … … 1815 1820 } 1816 1821 break; 1817 1822 1818 1823 case 'restore': 1819 1824 $old_status = $quote->getStatus(); … … 1826 1831 } 1827 1832 break; 1828 1833 1829 1834 default: 1830 1835 $error_count++; … … 1836 1841 } 1837 1842 } 1838 1843 1839 1844 if ($error_count > 0) { 1840 1845 wp_send_json_success([ … … 1855 1860 } 1856 1861 } 1857 1862 1858 1863 /** 1859 1864 * Handle AJAX request to trash a quote … … 1866 1871 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1867 1872 } 1868 1873 1869 1874 // Check permissions 1870 1875 if (!current_user_can('manage_options')) { 1871 1876 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1872 1877 } 1873 1878 1874 1879 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1875 1880 1876 1881 if ($quote_id <= 0) { 1877 1882 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1878 1883 } 1879 1884 1880 1885 $quote = $this->quote_repository->find($quote_id); 1881 1886 1882 1887 if (!$quote) { 1883 1888 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1884 1889 } 1885 1890 1886 1891 // Set status to cancelled before moving to trash 1887 1892 $old_status = $quote->getStatus(); 1888 1893 $quote->setStatus('cancelled'); 1889 1894 $quote->save(); 1890 1895 1891 1896 // Move the post to trash status 1892 1897 $result = wp_trash_post($quote_id); 1893 1898 1894 1899 if ($result) { 1895 1900 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled'); … … 1905 1910 } 1906 1911 } 1907 1912 1908 1913 /** 1909 1914 * Handle AJAX request to move a quote to draft … … 1916 1921 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1917 1922 } 1918 1923 1919 1924 // Check permissions 1920 1925 if (!current_user_can('manage_options')) { 1921 1926 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1922 1927 } 1923 1928 1924 1929 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1925 1930 1926 1931 if ($quote_id <= 0) { 1927 1932 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1928 1933 } 1929 1934 1930 1935 $quote = $this->quote_repository->find($quote_id); 1931 1936 1932 1937 if (!$quote) { 1933 1938 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1934 1939 } 1935 1940 1936 1941 // Set status to draft 1937 1942 $old_status = $quote->getStatus(); 1938 1943 $quote->setStatus('draft'); 1939 1944 1940 1945 if ($quote->save()) { 1941 1946 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft'); … … 1951 1956 } 1952 1957 } 1953 1958 1954 1959 /** 1955 1960 * Handle AJAX request to restore a trashed quote … … 1962 1967 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 1963 1968 } 1964 1969 1965 1970 // Check permissions 1966 1971 if (!current_user_can('manage_options')) { 1967 1972 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 1968 1973 } 1969 1974 1970 1975 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 1971 1976 1972 1977 if ($quote_id <= 0) { 1973 1978 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 1974 1979 } 1975 1980 1976 1981 $quote = $this->quote_repository->find($quote_id); 1977 1982 1978 1983 if (!$quote) { 1979 1984 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]); 1980 1985 } 1981 1986 1982 1987 // Restore the post from trash 1983 1988 $result = wp_untrash_post($quote_id); 1984 1989 1985 1990 if ($result) { 1986 1991 // After restoring from trash, set the meta status to available 1987 1992 $quote->setStatus('available'); 1988 1993 $quote->save(); 1989 1994 1990 1995 $this->quote_log_service->logRestoration($quote_id); 1991 1996 wp_send_json_success([ … … 2000 2005 } 2001 2006 } 2002 2007 2003 2008 /** 2004 2009 * Handle AJAX request to empty trash … … 2012 2017 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 2013 2018 } 2014 2019 2015 2020 // Check permissions 2016 2021 if (!current_user_can('manage_options')) { 2017 2022 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 2018 2023 } 2019 2024 2020 2025 // Get all quotes in trash (post_status = 'trash') 2021 2026 global $wpdb; 2022 2027 $quote_ids = $wpdb->get_col($wpdb->prepare( 2023 "SELECT ID FROM {$wpdb->posts} 2024 WHERE post_type = %s 2028 "SELECT ID FROM {$wpdb->posts} 2029 WHERE post_type = %s 2025 2030 AND post_status = 'trash'", 2026 2031 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE 2027 2032 )); 2028 2033 2029 2034 if (empty($quote_ids)) { 2030 2035 wp_send_json_error(['message' => __('No quotes found in trash.', 'easy-invoice')]); 2031 2036 } 2032 2037 2033 2038 $success_count = 0; 2034 2039 $error_count = 0; 2035 2040 2036 2041 foreach ($quote_ids as $quote_id) { 2037 2042 if (wp_delete_post($quote_id, true)) { … … 2042 2047 } 2043 2048 } 2044 2049 2045 2050 if ($error_count > 0) { 2046 2051 wp_send_json_success([ … … 2064 2069 ]); 2065 2070 } 2066 2071 2067 2072 } catch (\Exception $e) { 2068 2073 error_log('Error emptying quote trash: ' . $e->getMessage()); … … 2073 2078 } 2074 2079 } 2075 2080 2076 2081 /** 2077 2082 * Handle AJAX request to get quote logs … … 2084 2089 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]); 2085 2090 } 2086 2091 2087 2092 // Check permissions 2088 2093 if (!current_user_can('manage_options')) { 2089 2094 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]); 2090 2095 } 2091 2096 2092 2097 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0; 2093 2098 2094 2099 if ($quote_id <= 0) { 2095 2100 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]); 2096 2101 } 2097 2102 2098 2103 try { 2099 2104 $logs = $this->quote_log_service->getLogs($quote_id); 2100 2105 2101 2106 // Convert QuoteLog objects to arrays for JSON response 2102 2107 $logs_data = []; … … 2113 2118 ]; 2114 2119 } 2115 2120 2116 2121 wp_send_json_success([ 2117 2122 'logs' => $logs_data, 2118 2123 'count' => count($logs_data) 2119 2124 ]); 2120 2125 2121 2126 } catch (\Exception $e) { 2122 2127 wp_send_json_error([ … … 2126 2131 } 2127 2132 } 2128 2133 2129 2134 /** 2130 2135 * Format currency amount using QuoteFormatter … … 2138 2143 return $formatter->format($amount); 2139 2144 } 2140 } 2145 } -
easy-invoice/trunk/readme.txt
r3417137 r3420035 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2.1. 67 Stable tag: 2.1.7 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 137 137 138 138 == Changelog == 139 139 = 2.1.7 - 2025-12-15 = 140 * Checked - fix quote accept and decline issue 140 141 = 2.1.6 - 2025-12-11 = 141 142 * Checked - WordPress 6.9 compatibility tested
Note: See TracChangeset
for help on using the changeset viewer.