Plugin Directory

Changeset 3420035


Ignore:
Timestamp:
12/15/2025 11:27:02 AM (3 months ago)
Author:
matrixaddons
Message:

Update to version 2.1.7 from GitHub

Location:
easy-invoice
Files:
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • easy-invoice/tags/2.1.7/easy-invoice.php

    r3417137 r3420035  
    44 * Plugin URI: https://matrixaddons.com/plugins/easy-invoice
    55 * Description: A beautiful, full-featured invoicing solution for WordPress. Create professional invoices, quotes, and manage payments with ease.
    6  * Version: 2.1.6
     6 * Version: 2.1.7
    77 * Author: MatrixAddons
    88 * Author URI: https://matrixaddons.com
     
    2525
    2626// Define plugin constants.
    27 define( 'EASY_INVOICE_VERSION', '2.1.6' );
     27define( 'EASY_INVOICE_VERSION', '2.1.7' );
    2828define( 'EASY_INVOICE_PLUGIN_FILE', __FILE__ );
    2929define( 'EASY_INVOICE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • easy-invoice/tags/2.1.7/includes/Controllers/QuoteController.php

    r3399346 r3420035  
    2727 */
    2828class QuoteController {
    29    
     29
    3030    /**
    3131     * Quote repository
     
    3434     */
    3535    private $quote_repository;
    36    
     36
    3737    /**
    3838     * Client repository
     
    4141     */
    4242    private $client_repository;
    43    
     43
    4444    /**
    4545     * Form processor
     
    4848     */
    4949    private $form_processor;
    50    
     50
    5151    /**
    5252     * Quote log service
     
    5555     */
    5656    private $quote_log_service;
    57    
     57
    5858    /**
    5959     * Constructor
     
    6767        $this->quote_log_service = new QuoteLogService();
    6868    }
    69    
     69
    7070    /**
    7171     * Initialize the controller
     
    7676        // Allow plugins to extend the controller initialization
    7777        do_action('easy_invoice_quote_controller_before_init', $this);
    78        
     78
    7979        // Add AJAX handlers
    8080        add_action('wp_ajax_easy_invoice_delete_quote', [$this, 'handleDeleteQuote']);
     
    8989        add_action('wp_ajax_nopriv_easy_invoice_decline_quote', [$this, 'handleDeclineQuote']);
    9090        add_action('wp_ajax_easy_invoice_update_existing_quotes', [$this, 'handleUpdateExistingQuotes']);
    91        
     91
    9292        // Add missing AJAX handlers for quote listing actions
    9393        add_action('wp_ajax_easy_invoice_bulk_quote_action', [$this, 'handleBulkQuoteAction']);
    9494        add_action('wp_ajax_easy_invoice_trash_quote', [$this, 'handleTrashQuote']);
    9595        add_action('wp_ajax_easy_invoice_draft_quote', [$this, 'handleDraftQuote']);
    96        
     96
    9797        // Add regular POST form handlers for quote actions
    9898        add_action('init', [$this, 'handleQuoteFormActions']);
    99        
     99
    100100        // Add new AJAX handler for restoring a trashed quote
    101101        add_action('wp_ajax_easy_invoice_restore_quote', [ $this, 'handleRestoreQuote' ]);
    102        
     102
    103103        // Add new AJAX handler for emptying trash
    104104        add_action('wp_ajax_easy_invoice_empty_trash', [ $this, 'handleEmptyTrash' ]);
    105        
     105
    106106        // Add new AJAX handler for getting quote logs
    107107        add_action('wp_ajax_easy_invoice_get_quote_logs', [ $this, 'handleGetQuoteLogs' ]);
    108        
     108
    109109        // Allow plugins to extend the controller initialization
    110110        do_action('easy_invoice_quote_controller_after_init', $this);
    111111    }
    112    
     112
    113113    /**
    114114     * Display quote pages
     
    120120        // Allow plugins to modify display arguments
    121121        $args = apply_filters('easy_invoice_quote_controller_display_args', $args);
    122        
     122
    123123        $page = $args['page'] ?? '';
    124        
     124
    125125        // Allow plugins to modify the page before processing
    126126        $page = apply_filters('easy_invoice_quote_controller_display_page', $page, $args);
    127        
     127
    128128        switch ($page) {
    129129            case PagesSlugs::ALL_QUOTES:
    130130                $this->displayListing();
    131131                break;
    132                
     132
    133133            case PagesSlugs::QUOTE_NEW:
    134134                $this->displayBuilder();
    135135                break;
    136                
     136
    137137            case PagesSlugs::QUOTE_PREVIEW:
    138138                $this->displayPreview($args);
    139139                break;
    140                
     140
    141141            default:
    142142                $this->displayListing();
    143143                break;
    144144        }
    145        
     145
    146146        // Allow plugins to perform actions after display
    147147        do_action('easy_invoice_quote_controller_after_display', $page, $args);
    148148    }
    149    
     149
    150150    /**
    151151     * Display quote listing page
     
    159159        // Get trash count first (based on post_status)
    160160        $trash_count = (int)$wpdb->get_var($wpdb->prepare(
    161             "SELECT COUNT(*) FROM {$wpdb->posts} 
     161            "SELECT COUNT(*) FROM {$wpdb->posts}
    162162            WHERE post_type = %s AND post_status = 'trash'",
    163163            PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
    164164        ));
    165        
     165
    166166        // Get counts for each meta status (excluding trashed posts)
    167167        $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
    170170            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
    172172            AND p.post_status != 'trash'
    173173            GROUP BY COALESCE(pm.meta_value, 'draft')",
     
    189189            $count = (int)$status->count;
    190190            $all_count += $count;  // Add to total (excluding trash)
    191            
     191
    192192            switch ($status->status) {
    193193                case 'draft':
     
    218218        // Allow plugins to perform actions before displaying listing
    219219        do_action('easy_invoice_quote_controller_before_display_listing');
    220        
     220
    221221        // Get filter parameters
    222222        $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
     
    242242            // For trash and cancelled views, look at post_status = 'trash'
    243243            $query_args['post_status'] = 'trash';
    244            
     244
    245245            // For cancelled view, also filter by meta status
    246246            if ($current_view === 'cancelled') {
     
    256256            // For all other views, exclude trashed posts
    257257            $query_args['post_status'] = ['publish', 'draft', 'private', 'pending'];
    258            
     258
    259259            if ($current_view !== 'all') {
    260260                // For specific status views, add meta query
     
    272272        if (!empty($search_query)) {
    273273            $search_ids = [];
    274            
     274
    275275            // Build base query args for search
    276276            $search_query_args = [
     
    310310            ]);
    311311            $meta_search = new \WP_Query($meta_search_args);
    312            
     312
    313313            if ($meta_search->have_posts()) {
    314314                $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID'));
    315315            }
    316            
     316
    317317            $search_ids = array_unique($search_ids);
    318            
     318
    319319            if (!empty($search_ids)) {
    320320                $query_args['post__in'] = $search_ids;
     
    329329        $wp_query = new \WP_Query($query_args);
    330330        $quotes = [];
    331        
     331
    332332        if ($wp_query->have_posts()) {
    333333            foreach ($wp_query->posts as $post) {
     
    341341        // Allow plugins to modify the quotes array
    342342        $quotes = apply_filters('easy_invoice_quote_controller_quotes_list', $quotes, $wp_query);
    343        
     343
    344344        // Get pagination info from WordPress query
    345345        $total_quotes = $wp_query->found_posts;
     
    381381            }
    382382        }
    383        
     383
    384384        // Prepare template data
    385385        $template_data = [
     
    404404            'wp_query' => $wp_query
    405405        ];
    406        
     406
    407407        // Allow plugins to modify template data
    408408        $template_data = apply_filters('easy_invoice_quote_controller_template_data', $template_data);
    409        
     409
    410410        // Display the template
    411411        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/listing.php';
    412        
     412
    413413        // Allow plugins to perform actions after displaying listing
    414414        do_action('easy_invoice_quote_controller_after_display_listing', $template_data);
    415415    }
    416    
     416
    417417    /**
    418418     * Display quote builder page
     
    423423        // Allow plugins to perform actions before displaying builder
    424424        do_action('easy_invoice_quote_controller_before_display_builder');
    425        
     425
    426426        $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
    427427        $quote = null;
    428        
     428
    429429        if ($quote_id > 0) {
    430430            $quote = $this->quote_repository->find($quote_id);
    431431        }
    432        
     432
    433433        $clients = $this->client_repository->all();
    434        
     434
    435435        // Allow plugins to modify the data
    436436        $quote = apply_filters('easy_invoice_quote_controller_builder_quote', $quote, $quote_id);
     
    439439        // Include the builder template
    440440        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/builder.php';
    441        
     441
    442442        // Allow plugins to perform actions after displaying builder
    443443        do_action('easy_invoice_quote_controller_after_display_builder', $quote, $clients);
    444444    }
    445    
     445
    446446    /**
    447447     * Display quote preview page
     
    453453        // Allow plugins to perform actions before displaying preview
    454454        do_action('easy_invoice_quote_controller_before_display_preview', $args);
    455        
     455
    456456        $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
    457        
     457
    458458        if ($quote_id <= 0) {
    459459            wp_die(__('Quote not found.', 'easy-invoice'));
    460460        }
    461        
     461
    462462        $quote = $this->quote_repository->find($quote_id);
    463463        if (!$quote) {
    464464            wp_die(__('Quote not found.', 'easy-invoice'));
    465465        }
    466        
     466
    467467        // Allow plugins to modify the quote
    468468        $quote = apply_filters('easy_invoice_quote_controller_preview_quote', $quote, $quote_id);
    469        
     469
    470470        // Include the preview template
    471471        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/preview.php';
    472        
     472
    473473        // Allow plugins to perform actions after displaying preview
    474474        do_action('easy_invoice_quote_controller_after_display_preview', $quote, $args);
    475475    }
    476    
     476
    477477    /**
    478478     * Handle delete quote AJAX request
     
    485485            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    486486        }
    487        
     487
    488488        // Check permissions
    489489        if (!current_user_can('manage_options')) {
    490490            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    491491        }
    492        
     492
    493493        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    494        
     494
    495495        if ($quote_id <= 0) {
    496496            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    497497        }
    498        
     498
    499499        if ($this->quote_repository->delete($quote_id)) {
    500500            // Log the quote deletion
    501501            $this->quote_log_service->logDeletion($quote_id);
    502            
     502
    503503            wp_send_json_success([
    504504                'message' => __('Quote deleted successfully.', 'easy-invoice'),
     
    512512        }
    513513    }
    514    
     514
    515515    /**
    516516     * Handle get quote AJAX request
     
    523523            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    524524        }
    525        
     525
    526526        // Check permissions
    527527        if (!current_user_can('manage_options')) {
    528528            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    529529        }
    530        
     530
    531531        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    532        
     532
    533533        if ($quote_id <= 0) {
    534534            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    535535        }
    536        
     536
    537537        $quote = $this->quote_repository->find($quote_id);
    538        
     538
    539539        if (!$quote) {
    540540            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    541541        }
    542        
     542
    543543        wp_send_json_success(['quote' => $quote->toArray()]);
    544544    }
    545    
     545
    546546    /**
    547547     * Handle AJAX request to load quote template
     
    554554            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    555555        }
    556        
     556
    557557        // Check permissions
    558558        if (!current_user_can('manage_options')) {
    559559            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    560560        }
    561        
     561
    562562        $template_id = sanitize_text_field($_POST['template'] ?? '');
    563563        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    564        
     564
    565565        if (empty($template_id)) {
    566566            wp_send_json_error(['message' => __('Template ID is required.', 'easy-invoice')]);
    567567        }
    568        
     568
    569569        // Validate template name securely
    570570        $template_id = $this->validateTemplateName($template_id, 'quote');
    571        
     571
    572572        // Get secure template file path
    573573        $template_file = $this->getSecureTemplatePath($template_id, 'quote');
    574        
     574
    575575        if (!$template_file) {
    576576            wp_send_json_error(['message' => __('Template not found.', 'easy-invoice')]);
    577577        }
    578        
     578
    579579        // Load quote if provided
    580580        $quote = null;
     
    582582            $quote = $this->quote_repository->find($quote_id);
    583583        }
    584        
     584
    585585        // Start output buffering to capture template HTML
    586586        ob_start();
    587        
     587
    588588        // Include the template file
    589589        include $template_file;
    590        
     590
    591591        // Get the captured HTML
    592592        $html = ob_get_clean();
    593        
     593
    594594        wp_send_json_success(['html' => $html]);
    595595    }
     
    597597    /**
    598598     * Validate and sanitize template name to prevent directory traversal attacks
    599      * 
     599     *
    600600     * @param string $template The template name to validate
    601601     * @param string $type Either 'invoice' or 'quote'
     
    611611        // Strip any directory components using basename
    612612        $template = basename($template);
    613        
     613
    614614        // Remove any file extension
    615615        $template = preg_replace('/\.(php|html|htm)$/i', '', $template);
    616        
     616
    617617        // Remove any non-alphanumeric characters except hyphens and underscores
    618618        $template = preg_replace('/[^a-z0-9_-]/i', '', $template);
    619        
     619
    620620        // Check if template is in whitelist
    621621        if (isset($allowed_templates[$type]) && in_array($template, $allowed_templates[$type], true)) {
    622622            return $template;
    623623        }
    624        
     624
    625625        // Return default template if not in whitelist
    626626        return 'standard';
     
    629629    /**
    630630     * Get secure template file path with directory traversal protection
    631      * 
     631     *
    632632     * @param string $template The validated template name
    633633     * @param string $type Either 'invoice' or 'quote'
     
    646646
    647647        $template_dir = $template_dirs[$type];
    648        
     648
    649649        // Ensure template directory exists and is a directory
    650650        if (!is_dir($template_dir)) {
     
    660660        // Construct the template file path
    661661        $template_file = $real_template_dir . DIRECTORY_SEPARATOR . $template . '.php';
    662        
     662
    663663        // Get the real path of the template file (resolves any .. or . components)
    664664        $real_template_file = realpath($template_file);
    665        
     665
    666666        // Verify that the resolved path is within the template directory
    667667        // This prevents directory traversal attacks
     
    670670            $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php';
    671671            $real_default_file = realpath($default_file);
    672            
     672
    673673            if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0) {
    674674                return $real_default_file;
    675675            }
    676            
     676
    677677            return false;
    678678        }
     
    683683            $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php';
    684684            $real_default_file = realpath($default_file);
    685            
     685
    686686            if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0 && is_file($real_default_file) && is_readable($real_default_file)) {
    687687                return $real_default_file;
    688688            }
    689            
     689
    690690            return false;
    691691        }
     
    693693        return $real_template_file;
    694694    }
    695    
     695
    696696    /**
    697697     * Handle AJAX request to create a new quote with just the title
     
    712712            wp_send_json_error(['message' => __('Quote title is required.', 'easy-invoice')]);
    713713        }
    714        
     714
    715715        // Generate a unique quote number
    716716        $quote_number = '';
     
    722722            $quote_number = 'QT-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
    723723        }
    724        
     724
    725725        // Get global quote settings
    726726        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    732732        $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
    733733        $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
    734        
     734
    735735        // Create the quote with just the title and default values
    736736        $data = [
     
    756756        wp_send_json_success(['quote_id' => $quote->getId()]);
    757757    }
    758    
     758
    759759    /**
    760760     * Handle AJAX request to load quote form for modal
     
    767767            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    768768        }
    769        
     769
    770770        // Check permissions
    771771        if (!current_user_can('manage_options')) {
    772772            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    773773        }
    774        
     774
    775775        // Get global quote settings
    776776        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    782782        $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
    783783        $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
    784        
     784
    785785        // Create a new quote object for the form
    786786        $quote_number_service = function_exists('easy_invoice_get_quote_number_service') ? easy_invoice_get_quote_number_service() : null;
     
    815815            'declined_message' => $quote_declined_message, // Use global declined message setting
    816816        );
    817        
     817
    818818        // Create a temporary WP_Post object for new quote
    819819        $empty_post = new \WP_Post((object) array(
     
    837837            'filter' => 'raw',
    838838        ));
    839        
     839
    840840        $quote = new \EasyInvoice\Models\Quote($empty_post);
    841        
     841
    842842        // Set default values on the quote object
    843843        foreach ($quote_data as $key => $value) {
     
    868868            }
    869869        }
    870        
     870
    871871        // Initialize empty items array
    872872        $quote->setItems([]);
    873        
     873
    874874        // Set variables needed by the form template
    875875        $quote_id = 0;
     
    879879        $admin_nonce = wp_create_nonce('easy_invoice_admin_nonce');
    880880        $quote_field_config = $quote_form_manager->getFieldConfigForJavaScript();
    881        
     881
    882882        // Start output buffering to capture form HTML
    883883        ob_start();
    884        
     884
    885885        // Include the quote form template
    886886        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/form.php';
    887        
     887
    888888        // Get the captured HTML
    889889        $html = ob_get_clean();
    890        
     890
    891891        wp_send_json_success(['html' => $html]);
    892892    }
    893    
     893
    894894    /**
    895895     * Handle search clients AJAX request
     
    902902            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    903903        }
    904        
     904
    905905        // Check permissions
    906906        if (!current_user_can('manage_options')) {
    907907            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    908908        }
    909        
     909
    910910        $query = sanitize_text_field($_POST['query'] ?? '');
    911        
     911
    912912        // If query is empty, get all clients
    913913        if (empty($query)) {
     
    917917        $clients = $this->client_repository->search($query);
    918918        }
    919        
     919
    920920        $results = [];
    921921        foreach ($clients as $client) {
     
    930930            ];
    931931        }
    932        
     932
    933933        wp_send_json_success($results);
    934934    }
    935    
     935
    936936    /**
    937937     * Handle AJAX request to accept a quote
     
    944944            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    945945        }
    946        
     946
    947947        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    948        
     948
    949949        if ($quote_id <= 0) {
    950950            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    951951        }
    952        
     952
    953953        // Get the quote
    954954        $quote = $this->quote_repository->find($quote_id);
    955        
     955
    956956        if (!$quote) {
    957957            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    958958        }
    959        
     959
    960960        // Check if user has permission to accept this quote
    961961        $current_user = wp_get_current_user();
    962962        $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
    966968            if ($quote->getClientId()) {
    967969                $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
    968970                $client = $client_repository->find($quote->getClientId());
    969                
     971
    970972                if (!$client || $client->getEmail() !== $current_user->user_email) {
    971973                    wp_send_json_error(['message' => __('You do not have permission to accept this quote.', 'easy-invoice')]);
     
    975977            }
    976978        }
    977        
     979
    978980        // Get global accept action setting
    979981        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
    980982        $accept_action = $settings_controller::getQuoteAcceptAction();
    981        
     983
    982984        // Update quote status to accepted
    983985        $quote->setStatus('accepted');
    984986        $quote->setAcceptedDate(date('Y-m-d H:i:s'));
    985987        $quote->setAcceptedBy($current_user->ID);
    986        
     988
    987989        // Save the quote
    988990        $saved = $quote->save();
    989        
     991
    990992        if (!$saved) {
    991993            wp_send_json_error(['message' => __('Failed to accept quote.', 'easy-invoice')]);
    992994        }
    993        
     995
    994996        // Log the quote acceptance
    995997        $this->quote_log_service->logAcceptance($quote_id, [
     
    997999            'user_type' => $is_admin ? 'admin' : 'client'
    9981000        ]);
    999        
     1001
    10001002        // Perform the configured accept action
    10011003        $invoice_id = null;
    10021004        $action_message = '';
    1003        
     1005
    10041006        switch ($accept_action) {
    10051007            case 'convert':
     
    10111013                $action_message = __('Quote converted to invoice successfully.', 'easy-invoice');
    10121014                break;
    1013                
     1015
    10141016            case 'convert_available':
    10151017                // Convert quote to invoice (Available status)
     
    10201022                $action_message = __('Quote converted to invoice successfully.', 'easy-invoice');
    10211023                break;
    1022                
     1024
    10231025            case 'convert_send':
    10241026                // Convert quote to invoice and send to client (Available status)
     
    10291031                $action_message = __('Quote converted to invoice and sent to client successfully.', 'easy-invoice');
    10301032                break;
    1031                
     1033
    10321034            case 'duplicate':
    10331035                // Create new invoice, keep quote as-is (Draft status)
     
    10381040                $action_message = __('New invoice created from quote successfully.', 'easy-invoice');
    10391041                break;
    1040                
     1042
    10411043            case 'duplicate_send':
    10421044                // Create new invoice and send to client, keep quote as-is (Available status)
     
    10471049                $action_message = __('New invoice created and sent to client successfully.', 'easy-invoice');
    10481050                break;
    1049                
     1051
    10501052            case 'do_nothing':
    10511053            default:
     
    10541056                break;
    10551057        }
    1056        
     1058
    10571059        // Send notification email to admin
    10581060        if (!$is_admin) {
    10591061            $this->sendQuoteAcceptanceNotification($quote);
    10601062        }
    1061        
     1063
    10621064        // Get URLs for the new invoice
    10631065        $invoice_url = null;
    10641066        $secure_url = null;
    1065        
     1067
    10661068        if ($invoice_id) {
    10671069            // Always use WordPress permalink
     
    10751077            }
    10761078        }
    1077        
     1079
    10781080        wp_send_json_success([
    10791081            'message' => $action_message,
     
    10871089        ]);
    10881090    }
    1089    
     1091
    10901092    /**
    10911093     * Convert quote to invoice
     
    10991101            // Get invoice repository
    11001102            $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
    1101            
     1103
    11021104            // Create invoice data from quote - convert ALL fields
    11031105            $invoice_data = [
     
    11351137                'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
    11361138            ];
    1137            
     1139
    11381140            // Create the invoice
    11391141            $invoice = $invoice_repository->create($invoice_data);
    1140            
     1142
    11411143            if ($invoice) {
    11421144                // Store the quote ID in the invoice's meta for tracking
    11431145                update_post_meta($invoice->getId(), '_converted_from_quote', $quote->getId());
    1144                
     1146
    11451147                // Update quote to reference the created invoice
    11461148                $quote->setCustomField('converted_invoice_id', $invoice->getId());
    11471149                $quote->save();
    1148                
     1150
    11491151                // Ensure secure link is generated for the new invoice (Pro version)
    11501152                if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
     
    11521154                    do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
    11531155                }
    1154                
     1156
    11551157                return $invoice->getId();
    11561158            }
    1157            
     1159
    11581160            return null;
    11591161        } catch (\Exception $e) {
     
    11621164        }
    11631165    }
    1164    
     1166
    11651167    /**
    11661168     * Create new invoice from quote (duplicate)
     
    11741176            // Get invoice repository
    11751177            $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
    1176            
     1178
    11771179            // Create invoice data from quote - convert ALL fields
    11781180            $invoice_data = [
     
    12101212                'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
    12111213            ];
    1212            
     1214
    12131215            // Create the invoice
    12141216            $invoice = $invoice_repository->create($invoice_data);
    1215            
     1217
    12161218            if ($invoice) {
    12171219                // Link the invoice to the quote
    12181220                $quote->setCustomField('related_invoice_id', $invoice->getId());
    12191221                $quote->save();
    1220                
     1222
    12211223                // Ensure secure link is generated for the new invoice (Pro version)
    12221224                if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
     
    12241226                    do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
    12251227                }
    1226                
     1228
    12271229                return $invoice->getId();
    12281230            }
    1229            
     1231
    12301232            return null;
    12311233        } catch (\Exception $e) {
     
    12341236        }
    12351237    }
    1236    
     1238
    12371239    /**
    12381240     * Send invoice to client
     
    12461248            $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
    12471249            $invoice = $invoice_repository->find($invoice_id);
    1248            
     1250
    12491251            if (!$invoice) {
    12501252                return false;
    12511253            }
    1252            
     1254
    12531255            // Get email manager
    12541256            $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
    1255            
     1257
    12561258            // Send invoice email
    12571259            $result = $email_manager->sendInvoiceEmail($invoice, 'new');
    1258            
     1260
    12591261            return $result['success'];
    12601262        } catch (\Exception $e) {
     
    12631265        }
    12641266    }
    1265    
     1267
    12661268    /**
    12671269     * Convert quote items to invoice items
     
    12721274    private function convertQuoteItemsToInvoiceItems(array $quote_items): array {
    12731275        $invoice_items = [];
    1274        
     1276
    12751277        foreach ($quote_items as $quote_item) {
    12761278            if (is_object($quote_item) && method_exists($quote_item, 'toArray')) {
     
    13001302            }
    13011303        }
    1302        
     1304
    13031305        return $invoice_items;
    13041306    }
    1305    
     1307
    13061308    /**
    13071309     * Generate unique invoice number
     
    13151317            return $invoice_number_service->generateUniqueNumber();
    13161318        }
    1317        
     1319
    13181320        // Fallback to timestamp-based number
    13191321        return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
    13201322    }
    1321    
     1323
    13221324    /**
    13231325     * Get changes between two quote versions
     
    13291331    private function getQuoteChanges($old_quote, $new_quote): array {
    13301332        $changes = [];
    1331        
     1333
    13321334        // Compare key fields
    13331335        $fields_to_compare = [
     
    13431345            'terms' => 'Terms',
    13441346        ];
    1345        
     1347
    13461348        foreach ($fields_to_compare as $field => $label) {
    13471349            $method_name = 'get' . easy_invoice_str_replace('_', '', ucwords($field, '_'));
    1348            
     1350
    13491351            if (method_exists($old_quote, $method_name) && method_exists($new_quote, $method_name)) {
    13501352                $old_value = $old_quote->$method_name();
    13511353                $new_value = $new_quote->$method_name();
    1352                
     1354
    13531355                if ($old_value !== $new_value) {
    13541356                    $changes[$field] = $new_value;
     
    13561358            }
    13571359        }
    1358        
     1360
    13591361        return $changes;
    13601362    }
    1361    
     1363
    13621364    /**
    13631365     * Handle AJAX request to decline a quote
     
    13701372            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    13711373        }
    1372        
     1374
    13731375        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    13741376        $decline_reason = isset($_POST['decline_reason']) ? sanitize_textarea_field($_POST['decline_reason']) : '';
    1375        
     1377
    13761378        if ($quote_id <= 0) {
    13771379            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    13781380        }
    1379        
     1381
    13801382        // Get the quote
    13811383        $quote = $this->quote_repository->find($quote_id);
    1382        
     1384
    13831385        if (!$quote) {
    13841386            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    13851387        }
    1386        
     1388
    13871389        // Check if decline reason is required by global settings
    13881390        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    13901392            wp_send_json_error(['message' => __('Reason for declining is required.', 'easy-invoice')]);
    13911393        }
    1392        
     1394
    13931395        // Check if user has permission to decline this quote
    13941396        $current_user = wp_get_current_user();
    13951397        $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') {
    13981402            // For non-admins, check if they are the client
    13991403            if ($quote->getClientId()) {
    14001404                $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
    14011405                $client = $client_repository->find($quote->getClientId());
    1402                
     1406
    14031407                if (!$client || $client->getEmail() !== $current_user->user_email) {
    14041408                    wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]);
     
    14081412            }
    14091413        }
    1410        
     1414
    14111415        // Update quote status to declined
    14121416        $quote->setStatus('declined');
    14131417        $quote->setDeclinedDate(date('Y-m-d H:i:s'));
    14141418        $quote->setDeclinedBy($current_user->ID);
    1415        
     1419
    14161420        // Save decline reason if provided
    14171421        if (!empty($decline_reason)) {
    14181422            $quote->setDeclineReason($decline_reason);
    14191423        }
    1420        
     1424
    14211425        // Save the quote
    14221426        $saved = $quote->save();
    1423        
     1427
    14241428        if (!$saved) {
    14251429            wp_send_json_error(['message' => __('Failed to decline quote.', 'easy-invoice')]);
    14261430        }
    1427        
     1431
    14281432        // Log the quote decline
    14291433        $this->quote_log_service->logDecline($quote_id, $decline_reason, [
    14301434            'user_type' => $is_admin ? 'admin' : 'client'
    14311435        ]);
    1432        
     1436
    14331437        // Send notification email to admin
    14341438        if (!$is_admin) {
    14351439            $this->sendQuoteDeclineNotification($quote);
    14361440        }
    1437        
     1441
    14381442        wp_send_json_success([
    14391443            'message' => __('Quote declined successfully.', 'easy-invoice'),
     
    14441448        ]);
    14451449    }
    1446    
     1450
    14471451    /**
    14481452     * Send quote acceptance notification to admin
     
    14551459        $email_manager->sendAdminQuoteNotification($quote, 'accepted');
    14561460    }
    1457    
     1461
    14581462    /**
    14591463     * Send quote decline notification to admin
     
    14661470        $email_manager->sendAdminQuoteNotification($quote, 'declined');
    14671471    }
    1468    
     1472
    14691473    /**
    14701474     * Handle AJAX request to update existing quotes with missing data
     
    14771481            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    14781482        }
    1479        
     1483
    14801484        // Check permissions
    14811485        if (!current_user_can('manage_options')) {
    14821486            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    14831487        }
    1484        
     1488
    14851489        $updated_count = 0;
    14861490        $quotes = $this->quote_repository->findAll();
    1487        
     1491
    14881492        foreach ($quotes as $quote) {
    14891493            $post = get_post($quote->getId());
     
    14921496                $post_title = $quote->getTitle() ?: $quote->getNumber() ?: 'Untitled Quote';
    14931497                $post_name = sanitize_title($post_title);
    1494                
     1498
    14951499                // Ensure uniqueness
    14961500                $original_slug = $post_name;
     
    15001504                    $counter++;
    15011505                }
    1502                
     1506
    15031507                // Update the post with the new slug
    15041508                wp_update_post([
     
    15061510                    'post_name' => $post_name
    15071511                ]);
    1508                
     1512
    15091513                $updated_count++;
    15101514            }
    15111515        }
    1512        
     1516
    15131517        wp_send_json_success([
    15141518            'message' => sprintf(__('Updated %d quotes with proper URLs.', 'easy-invoice'), $updated_count)
    15151519        ]);
    15161520    }
    1517    
     1521
    15181522    /**
    15191523     * Handle AJAX request to duplicate a quote
     
    15261530            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    15271531        }
    1528        
     1532
    15291533        // Check permissions
    15301534        if (!current_user_can('manage_options')) {
    15311535            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    15321536        }
    1533        
     1537
    15341538        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1535        
     1539
    15361540        if ($quote_id <= 0) {
    15371541            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    15381542        }
    1539        
     1543
    15401544        $quote = $this->quote_repository->find($quote_id);
    1541        
     1545
    15421546        if (!$quote) {
    15431547            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    15441548        }
    1545        
     1549
    15461550        // Get global quote settings
    15471551        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    15531557        $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
    15541558        $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
    1555        
     1559
    15561560        // Create the duplicate quote
    15571561        $duplicate_data = [
     
    15721576            'declined_message' => $quote_declined_message,
    15731577        ];
    1574        
     1578
    15751579        // Set client ID to 0 for a new quote
    15761580        $duplicate_data['client_id'] = 0;
    1577        
     1581
    15781582        $duplicate_quote = $this->quote_repository->create($duplicate_data);
    1579        
     1583
    15801584        if ($duplicate_quote) {
    15811585            $this->quote_log_service->logActivity($quote_id, 'duplicate', 'Quote duplicated', ['duplicate_id' => $duplicate_quote->getId()]);
     
    15921596        }
    15931597    }
    1594    
     1598
    15951599    /**
    15961600     * Handle regular POST form actions for quote accept/decline
     
    16031607            return;
    16041608        }
    1605        
     1609
    16061610        // Handle accept quote
    16071611        if (isset($_POST['accept_quote']) && isset($_POST['quote_id'])) {
    16081612            $this->handleAcceptQuoteForm();
    16091613        }
    1610        
     1614
    16111615        // Handle decline quote
    16121616        if (isset($_POST['decline_quote']) && isset($_POST['quote_id'])) {
     
    16141618        }
    16151619    }
    1616    
     1620
    16171621    /**
    16181622     * Handle accept quote form submission
     
    16251629            wp_die(__('Security check failed.', 'easy-invoice'));
    16261630        }
    1627        
     1631
    16281632        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1629        
     1633
    16301634        if ($quote_id <= 0) {
    16311635            wp_die(__('Invalid quote ID.', 'easy-invoice'));
    16321636        }
    1633        
     1637
    16341638        // Get the quote
    16351639        $quote = $this->quote_repository->find($quote_id);
    1636        
     1640
    16371641        if (!$quote) {
    16381642            wp_die(__('Quote not found.', 'easy-invoice'));
    16391643        }
    1640        
     1644
    16411645        // Check if user has permission to accept this quote
    16421646        $current_user = wp_get_current_user();
    16431647        $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
    16451715        if (!$is_admin) {
    16461716            // For non-admins, check if they are the client
     
    16481718                $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
    16491719                $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
    17161721                if (!$client || $client->getEmail() !== $current_user->user_email) {
    17171722                    wp_die(__('You do not have permission to decline this quote.', 'easy-invoice'));
     
    17211726            }
    17221727        }
    1723        
     1728
    17241729        // Update quote status to declined
    17251730        $quote->setStatus('declined');
    17261731        $quote->setDeclinedDate(date('Y-m-d H:i:s'));
    17271732        $quote->setDeclinedBy($current_user->ID);
    1728        
     1733
    17291734        // Save the quote
    17301735        $saved = $quote->save();
    1731        
     1736
    17321737        if (!$saved) {
    17331738            wp_die(__('Failed to decline quote.', 'easy-invoice'));
    17341739        }
    1735        
     1740
    17361741        // Send notification email to admin
    17371742        if (!$is_admin) {
    17381743            $this->sendQuoteDeclineNotification($quote);
    17391744        }
    1740        
     1745
    17411746        // Redirect back to the quote page with success message
    17421747        $redirect_url = add_query_arg('action', 'declined', get_permalink($quote_id));
     
    17441749        exit;
    17451750    }
    1746    
     1751
    17471752    /**
    17481753     * Handle AJAX request for bulk quote actions
     
    17551760            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    17561761        }
    1757        
     1762
    17581763        // Check permissions
    17591764        if (!current_user_can('manage_options')) {
    17601765            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    17611766        }
    1762        
     1767
    17631768        $quote_ids = isset($_POST['quote_ids']) ? array_map('intval', $_POST['quote_ids']) : [];
    17641769        $bulk_action = sanitize_text_field($_POST['bulk_action'] ?? '');
    1765        
     1770
    17661771        if (empty($quote_ids)) {
    17671772            wp_send_json_error(['message' => __('No quotes selected.', 'easy-invoice')]);
    17681773        }
    1769        
     1774
    17701775        if (empty($bulk_action)) {
    17711776            wp_send_json_error(['message' => __('No action selected.', 'easy-invoice')]);
    17721777        }
    1773        
     1778
    17741779        $success_count = 0;
    17751780        $error_count = 0;
    1776        
     1781
    17771782        foreach ($quote_ids as $quote_id) {
    17781783            $quote = $this->quote_repository->find($quote_id);
    1779            
     1784
    17801785            if (!$quote) {
    17811786                $error_count++;
    17821787                continue;
    17831788            }
    1784            
     1789
    17851790            try {
    17861791                switch ($bulk_action) {
     
    17931798                        }
    17941799                        break;
    1795                        
     1800
    17961801                    case 'trash':
    17971802                        $old_status = $quote->getStatus();
     
    18041809                        }
    18051810                        break;
    1806                        
     1811
    18071812                    case 'draft':
    18081813                        $old_status = $quote->getStatus();
     
    18151820                        }
    18161821                        break;
    1817                        
     1822
    18181823                    case 'restore':
    18191824                        $old_status = $quote->getStatus();
     
    18261831                        }
    18271832                        break;
    1828                        
     1833
    18291834                    default:
    18301835                        $error_count++;
     
    18361841            }
    18371842        }
    1838        
     1843
    18391844        if ($error_count > 0) {
    18401845            wp_send_json_success([
     
    18551860        }
    18561861    }
    1857    
     1862
    18581863    /**
    18591864     * Handle AJAX request to trash a quote
     
    18661871            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    18671872        }
    1868        
     1873
    18691874        // Check permissions
    18701875        if (!current_user_can('manage_options')) {
    18711876            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    18721877        }
    1873        
     1878
    18741879        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1875        
     1880
    18761881        if ($quote_id <= 0) {
    18771882            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    18781883        }
    1879        
     1884
    18801885        $quote = $this->quote_repository->find($quote_id);
    1881        
     1886
    18821887        if (!$quote) {
    18831888            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    18841889        }
    1885        
     1890
    18861891        // Set status to cancelled before moving to trash
    18871892        $old_status = $quote->getStatus();
    18881893        $quote->setStatus('cancelled');
    18891894        $quote->save();
    1890        
     1895
    18911896        // Move the post to trash status
    18921897        $result = wp_trash_post($quote_id);
    1893        
     1898
    18941899        if ($result) {
    18951900            $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled');
     
    19051910        }
    19061911    }
    1907    
     1912
    19081913    /**
    19091914     * Handle AJAX request to move a quote to draft
     
    19161921            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    19171922        }
    1918        
     1923
    19191924        // Check permissions
    19201925        if (!current_user_can('manage_options')) {
    19211926            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    19221927        }
    1923        
     1928
    19241929        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1925        
     1930
    19261931        if ($quote_id <= 0) {
    19271932            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    19281933        }
    1929        
     1934
    19301935        $quote = $this->quote_repository->find($quote_id);
    1931        
     1936
    19321937        if (!$quote) {
    19331938            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    19341939        }
    1935        
     1940
    19361941        // Set status to draft
    19371942        $old_status = $quote->getStatus();
    19381943        $quote->setStatus('draft');
    1939        
     1944
    19401945        if ($quote->save()) {
    19411946            $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft');
     
    19511956        }
    19521957    }
    1953    
     1958
    19541959    /**
    19551960     * Handle AJAX request to restore a trashed quote
     
    19621967            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    19631968        }
    1964        
     1969
    19651970        // Check permissions
    19661971        if (!current_user_can('manage_options')) {
    19671972            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    19681973        }
    1969        
     1974
    19701975        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1971        
     1976
    19721977        if ($quote_id <= 0) {
    19731978            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    19741979        }
    1975        
     1980
    19761981        $quote = $this->quote_repository->find($quote_id);
    1977        
     1982
    19781983        if (!$quote) {
    19791984            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    19801985        }
    1981        
     1986
    19821987        // Restore the post from trash
    19831988        $result = wp_untrash_post($quote_id);
    1984        
     1989
    19851990        if ($result) {
    19861991            // After restoring from trash, set the meta status to available
    19871992            $quote->setStatus('available');
    19881993            $quote->save();
    1989            
     1994
    19901995            $this->quote_log_service->logRestoration($quote_id);
    19911996            wp_send_json_success([
     
    20002005        }
    20012006    }
    2002    
     2007
    20032008    /**
    20042009     * Handle AJAX request to empty trash
     
    20122017                wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    20132018            }
    2014            
     2019
    20152020            // Check permissions
    20162021            if (!current_user_can('manage_options')) {
    20172022                wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    20182023            }
    2019            
     2024
    20202025            // Get all quotes in trash (post_status = 'trash')
    20212026            global $wpdb;
    20222027            $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
    20252030                AND post_status = 'trash'",
    20262031                PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
    20272032            ));
    2028            
     2033
    20292034            if (empty($quote_ids)) {
    20302035                wp_send_json_error(['message' => __('No quotes found in trash.', 'easy-invoice')]);
    20312036            }
    2032            
     2037
    20332038            $success_count = 0;
    20342039            $error_count = 0;
    2035            
     2040
    20362041            foreach ($quote_ids as $quote_id) {
    20372042                if (wp_delete_post($quote_id, true)) {
     
    20422047                }
    20432048            }
    2044            
     2049
    20452050            if ($error_count > 0) {
    20462051                wp_send_json_success([
     
    20642069                ]);
    20652070            }
    2066            
     2071
    20672072        } catch (\Exception $e) {
    20682073            error_log('Error emptying quote trash: ' . $e->getMessage());
     
    20732078        }
    20742079    }
    2075    
     2080
    20762081    /**
    20772082     * Handle AJAX request to get quote logs
     
    20842089            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    20852090        }
    2086        
     2091
    20872092        // Check permissions
    20882093        if (!current_user_can('manage_options')) {
    20892094            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    20902095        }
    2091        
     2096
    20922097        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    2093        
     2098
    20942099        if ($quote_id <= 0) {
    20952100            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    20962101        }
    2097        
     2102
    20982103        try {
    20992104            $logs = $this->quote_log_service->getLogs($quote_id);
    2100            
     2105
    21012106            // Convert QuoteLog objects to arrays for JSON response
    21022107            $logs_data = [];
     
    21132118                ];
    21142119            }
    2115            
     2120
    21162121            wp_send_json_success([
    21172122                'logs' => $logs_data,
    21182123                'count' => count($logs_data)
    21192124            ]);
    2120            
     2125
    21212126        } catch (\Exception $e) {
    21222127            wp_send_json_error([
     
    21262131        }
    21272132    }
    2128    
     2133
    21292134    /**
    21302135     * Format currency amount using QuoteFormatter
     
    21382143        return $formatter->format($amount);
    21392144    }
    2140 } 
     2145}
  • easy-invoice/tags/2.1.7/readme.txt

    r3417137 r3420035  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.1.6
     7Stable tag: 2.1.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    137137
    138138== Changelog ==
    139 
     139= 2.1.7 - 2025-12-15 =
     140* Checked - fix quote accept and decline issue
    140141= 2.1.6 - 2025-12-11 =
    141142* Checked - WordPress 6.9 compatibility tested
  • easy-invoice/trunk/easy-invoice.php

    r3417137 r3420035  
    44 * Plugin URI: https://matrixaddons.com/plugins/easy-invoice
    55 * Description: A beautiful, full-featured invoicing solution for WordPress. Create professional invoices, quotes, and manage payments with ease.
    6  * Version: 2.1.6
     6 * Version: 2.1.7
    77 * Author: MatrixAddons
    88 * Author URI: https://matrixaddons.com
     
    2525
    2626// Define plugin constants.
    27 define( 'EASY_INVOICE_VERSION', '2.1.6' );
     27define( 'EASY_INVOICE_VERSION', '2.1.7' );
    2828define( 'EASY_INVOICE_PLUGIN_FILE', __FILE__ );
    2929define( 'EASY_INVOICE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • easy-invoice/trunk/includes/Controllers/QuoteController.php

    r3399346 r3420035  
    2727 */
    2828class QuoteController {
    29    
     29
    3030    /**
    3131     * Quote repository
     
    3434     */
    3535    private $quote_repository;
    36    
     36
    3737    /**
    3838     * Client repository
     
    4141     */
    4242    private $client_repository;
    43    
     43
    4444    /**
    4545     * Form processor
     
    4848     */
    4949    private $form_processor;
    50    
     50
    5151    /**
    5252     * Quote log service
     
    5555     */
    5656    private $quote_log_service;
    57    
     57
    5858    /**
    5959     * Constructor
     
    6767        $this->quote_log_service = new QuoteLogService();
    6868    }
    69    
     69
    7070    /**
    7171     * Initialize the controller
     
    7676        // Allow plugins to extend the controller initialization
    7777        do_action('easy_invoice_quote_controller_before_init', $this);
    78        
     78
    7979        // Add AJAX handlers
    8080        add_action('wp_ajax_easy_invoice_delete_quote', [$this, 'handleDeleteQuote']);
     
    8989        add_action('wp_ajax_nopriv_easy_invoice_decline_quote', [$this, 'handleDeclineQuote']);
    9090        add_action('wp_ajax_easy_invoice_update_existing_quotes', [$this, 'handleUpdateExistingQuotes']);
    91        
     91
    9292        // Add missing AJAX handlers for quote listing actions
    9393        add_action('wp_ajax_easy_invoice_bulk_quote_action', [$this, 'handleBulkQuoteAction']);
    9494        add_action('wp_ajax_easy_invoice_trash_quote', [$this, 'handleTrashQuote']);
    9595        add_action('wp_ajax_easy_invoice_draft_quote', [$this, 'handleDraftQuote']);
    96        
     96
    9797        // Add regular POST form handlers for quote actions
    9898        add_action('init', [$this, 'handleQuoteFormActions']);
    99        
     99
    100100        // Add new AJAX handler for restoring a trashed quote
    101101        add_action('wp_ajax_easy_invoice_restore_quote', [ $this, 'handleRestoreQuote' ]);
    102        
     102
    103103        // Add new AJAX handler for emptying trash
    104104        add_action('wp_ajax_easy_invoice_empty_trash', [ $this, 'handleEmptyTrash' ]);
    105        
     105
    106106        // Add new AJAX handler for getting quote logs
    107107        add_action('wp_ajax_easy_invoice_get_quote_logs', [ $this, 'handleGetQuoteLogs' ]);
    108        
     108
    109109        // Allow plugins to extend the controller initialization
    110110        do_action('easy_invoice_quote_controller_after_init', $this);
    111111    }
    112    
     112
    113113    /**
    114114     * Display quote pages
     
    120120        // Allow plugins to modify display arguments
    121121        $args = apply_filters('easy_invoice_quote_controller_display_args', $args);
    122        
     122
    123123        $page = $args['page'] ?? '';
    124        
     124
    125125        // Allow plugins to modify the page before processing
    126126        $page = apply_filters('easy_invoice_quote_controller_display_page', $page, $args);
    127        
     127
    128128        switch ($page) {
    129129            case PagesSlugs::ALL_QUOTES:
    130130                $this->displayListing();
    131131                break;
    132                
     132
    133133            case PagesSlugs::QUOTE_NEW:
    134134                $this->displayBuilder();
    135135                break;
    136                
     136
    137137            case PagesSlugs::QUOTE_PREVIEW:
    138138                $this->displayPreview($args);
    139139                break;
    140                
     140
    141141            default:
    142142                $this->displayListing();
    143143                break;
    144144        }
    145        
     145
    146146        // Allow plugins to perform actions after display
    147147        do_action('easy_invoice_quote_controller_after_display', $page, $args);
    148148    }
    149    
     149
    150150    /**
    151151     * Display quote listing page
     
    159159        // Get trash count first (based on post_status)
    160160        $trash_count = (int)$wpdb->get_var($wpdb->prepare(
    161             "SELECT COUNT(*) FROM {$wpdb->posts} 
     161            "SELECT COUNT(*) FROM {$wpdb->posts}
    162162            WHERE post_type = %s AND post_status = 'trash'",
    163163            PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
    164164        ));
    165        
     165
    166166        // Get counts for each meta status (excluding trashed posts)
    167167        $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
    170170            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
    172172            AND p.post_status != 'trash'
    173173            GROUP BY COALESCE(pm.meta_value, 'draft')",
     
    189189            $count = (int)$status->count;
    190190            $all_count += $count;  // Add to total (excluding trash)
    191            
     191
    192192            switch ($status->status) {
    193193                case 'draft':
     
    218218        // Allow plugins to perform actions before displaying listing
    219219        do_action('easy_invoice_quote_controller_before_display_listing');
    220        
     220
    221221        // Get filter parameters
    222222        $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
     
    242242            // For trash and cancelled views, look at post_status = 'trash'
    243243            $query_args['post_status'] = 'trash';
    244            
     244
    245245            // For cancelled view, also filter by meta status
    246246            if ($current_view === 'cancelled') {
     
    256256            // For all other views, exclude trashed posts
    257257            $query_args['post_status'] = ['publish', 'draft', 'private', 'pending'];
    258            
     258
    259259            if ($current_view !== 'all') {
    260260                // For specific status views, add meta query
     
    272272        if (!empty($search_query)) {
    273273            $search_ids = [];
    274            
     274
    275275            // Build base query args for search
    276276            $search_query_args = [
     
    310310            ]);
    311311            $meta_search = new \WP_Query($meta_search_args);
    312            
     312
    313313            if ($meta_search->have_posts()) {
    314314                $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID'));
    315315            }
    316            
     316
    317317            $search_ids = array_unique($search_ids);
    318            
     318
    319319            if (!empty($search_ids)) {
    320320                $query_args['post__in'] = $search_ids;
     
    329329        $wp_query = new \WP_Query($query_args);
    330330        $quotes = [];
    331        
     331
    332332        if ($wp_query->have_posts()) {
    333333            foreach ($wp_query->posts as $post) {
     
    341341        // Allow plugins to modify the quotes array
    342342        $quotes = apply_filters('easy_invoice_quote_controller_quotes_list', $quotes, $wp_query);
    343        
     343
    344344        // Get pagination info from WordPress query
    345345        $total_quotes = $wp_query->found_posts;
     
    381381            }
    382382        }
    383        
     383
    384384        // Prepare template data
    385385        $template_data = [
     
    404404            'wp_query' => $wp_query
    405405        ];
    406        
     406
    407407        // Allow plugins to modify template data
    408408        $template_data = apply_filters('easy_invoice_quote_controller_template_data', $template_data);
    409        
     409
    410410        // Display the template
    411411        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/listing.php';
    412        
     412
    413413        // Allow plugins to perform actions after displaying listing
    414414        do_action('easy_invoice_quote_controller_after_display_listing', $template_data);
    415415    }
    416    
     416
    417417    /**
    418418     * Display quote builder page
     
    423423        // Allow plugins to perform actions before displaying builder
    424424        do_action('easy_invoice_quote_controller_before_display_builder');
    425        
     425
    426426        $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
    427427        $quote = null;
    428        
     428
    429429        if ($quote_id > 0) {
    430430            $quote = $this->quote_repository->find($quote_id);
    431431        }
    432        
     432
    433433        $clients = $this->client_repository->all();
    434        
     434
    435435        // Allow plugins to modify the data
    436436        $quote = apply_filters('easy_invoice_quote_controller_builder_quote', $quote, $quote_id);
     
    439439        // Include the builder template
    440440        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/builder.php';
    441        
     441
    442442        // Allow plugins to perform actions after displaying builder
    443443        do_action('easy_invoice_quote_controller_after_display_builder', $quote, $clients);
    444444    }
    445    
     445
    446446    /**
    447447     * Display quote preview page
     
    453453        // Allow plugins to perform actions before displaying preview
    454454        do_action('easy_invoice_quote_controller_before_display_preview', $args);
    455        
     455
    456456        $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
    457        
     457
    458458        if ($quote_id <= 0) {
    459459            wp_die(__('Quote not found.', 'easy-invoice'));
    460460        }
    461        
     461
    462462        $quote = $this->quote_repository->find($quote_id);
    463463        if (!$quote) {
    464464            wp_die(__('Quote not found.', 'easy-invoice'));
    465465        }
    466        
     466
    467467        // Allow plugins to modify the quote
    468468        $quote = apply_filters('easy_invoice_quote_controller_preview_quote', $quote, $quote_id);
    469        
     469
    470470        // Include the preview template
    471471        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/preview.php';
    472        
     472
    473473        // Allow plugins to perform actions after displaying preview
    474474        do_action('easy_invoice_quote_controller_after_display_preview', $quote, $args);
    475475    }
    476    
     476
    477477    /**
    478478     * Handle delete quote AJAX request
     
    485485            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    486486        }
    487        
     487
    488488        // Check permissions
    489489        if (!current_user_can('manage_options')) {
    490490            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    491491        }
    492        
     492
    493493        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    494        
     494
    495495        if ($quote_id <= 0) {
    496496            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    497497        }
    498        
     498
    499499        if ($this->quote_repository->delete($quote_id)) {
    500500            // Log the quote deletion
    501501            $this->quote_log_service->logDeletion($quote_id);
    502            
     502
    503503            wp_send_json_success([
    504504                'message' => __('Quote deleted successfully.', 'easy-invoice'),
     
    512512        }
    513513    }
    514    
     514
    515515    /**
    516516     * Handle get quote AJAX request
     
    523523            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    524524        }
    525        
     525
    526526        // Check permissions
    527527        if (!current_user_can('manage_options')) {
    528528            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    529529        }
    530        
     530
    531531        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    532        
     532
    533533        if ($quote_id <= 0) {
    534534            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    535535        }
    536        
     536
    537537        $quote = $this->quote_repository->find($quote_id);
    538        
     538
    539539        if (!$quote) {
    540540            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    541541        }
    542        
     542
    543543        wp_send_json_success(['quote' => $quote->toArray()]);
    544544    }
    545    
     545
    546546    /**
    547547     * Handle AJAX request to load quote template
     
    554554            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    555555        }
    556        
     556
    557557        // Check permissions
    558558        if (!current_user_can('manage_options')) {
    559559            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    560560        }
    561        
     561
    562562        $template_id = sanitize_text_field($_POST['template'] ?? '');
    563563        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    564        
     564
    565565        if (empty($template_id)) {
    566566            wp_send_json_error(['message' => __('Template ID is required.', 'easy-invoice')]);
    567567        }
    568        
     568
    569569        // Validate template name securely
    570570        $template_id = $this->validateTemplateName($template_id, 'quote');
    571        
     571
    572572        // Get secure template file path
    573573        $template_file = $this->getSecureTemplatePath($template_id, 'quote');
    574        
     574
    575575        if (!$template_file) {
    576576            wp_send_json_error(['message' => __('Template not found.', 'easy-invoice')]);
    577577        }
    578        
     578
    579579        // Load quote if provided
    580580        $quote = null;
     
    582582            $quote = $this->quote_repository->find($quote_id);
    583583        }
    584        
     584
    585585        // Start output buffering to capture template HTML
    586586        ob_start();
    587        
     587
    588588        // Include the template file
    589589        include $template_file;
    590        
     590
    591591        // Get the captured HTML
    592592        $html = ob_get_clean();
    593        
     593
    594594        wp_send_json_success(['html' => $html]);
    595595    }
     
    597597    /**
    598598     * Validate and sanitize template name to prevent directory traversal attacks
    599      * 
     599     *
    600600     * @param string $template The template name to validate
    601601     * @param string $type Either 'invoice' or 'quote'
     
    611611        // Strip any directory components using basename
    612612        $template = basename($template);
    613        
     613
    614614        // Remove any file extension
    615615        $template = preg_replace('/\.(php|html|htm)$/i', '', $template);
    616        
     616
    617617        // Remove any non-alphanumeric characters except hyphens and underscores
    618618        $template = preg_replace('/[^a-z0-9_-]/i', '', $template);
    619        
     619
    620620        // Check if template is in whitelist
    621621        if (isset($allowed_templates[$type]) && in_array($template, $allowed_templates[$type], true)) {
    622622            return $template;
    623623        }
    624        
     624
    625625        // Return default template if not in whitelist
    626626        return 'standard';
     
    629629    /**
    630630     * Get secure template file path with directory traversal protection
    631      * 
     631     *
    632632     * @param string $template The validated template name
    633633     * @param string $type Either 'invoice' or 'quote'
     
    646646
    647647        $template_dir = $template_dirs[$type];
    648        
     648
    649649        // Ensure template directory exists and is a directory
    650650        if (!is_dir($template_dir)) {
     
    660660        // Construct the template file path
    661661        $template_file = $real_template_dir . DIRECTORY_SEPARATOR . $template . '.php';
    662        
     662
    663663        // Get the real path of the template file (resolves any .. or . components)
    664664        $real_template_file = realpath($template_file);
    665        
     665
    666666        // Verify that the resolved path is within the template directory
    667667        // This prevents directory traversal attacks
     
    670670            $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php';
    671671            $real_default_file = realpath($default_file);
    672            
     672
    673673            if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0) {
    674674                return $real_default_file;
    675675            }
    676            
     676
    677677            return false;
    678678        }
     
    683683            $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php';
    684684            $real_default_file = realpath($default_file);
    685            
     685
    686686            if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0 && is_file($real_default_file) && is_readable($real_default_file)) {
    687687                return $real_default_file;
    688688            }
    689            
     689
    690690            return false;
    691691        }
     
    693693        return $real_template_file;
    694694    }
    695    
     695
    696696    /**
    697697     * Handle AJAX request to create a new quote with just the title
     
    712712            wp_send_json_error(['message' => __('Quote title is required.', 'easy-invoice')]);
    713713        }
    714        
     714
    715715        // Generate a unique quote number
    716716        $quote_number = '';
     
    722722            $quote_number = 'QT-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
    723723        }
    724        
     724
    725725        // Get global quote settings
    726726        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    732732        $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
    733733        $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
    734        
     734
    735735        // Create the quote with just the title and default values
    736736        $data = [
     
    756756        wp_send_json_success(['quote_id' => $quote->getId()]);
    757757    }
    758    
     758
    759759    /**
    760760     * Handle AJAX request to load quote form for modal
     
    767767            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    768768        }
    769        
     769
    770770        // Check permissions
    771771        if (!current_user_can('manage_options')) {
    772772            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    773773        }
    774        
     774
    775775        // Get global quote settings
    776776        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    782782        $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
    783783        $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
    784        
     784
    785785        // Create a new quote object for the form
    786786        $quote_number_service = function_exists('easy_invoice_get_quote_number_service') ? easy_invoice_get_quote_number_service() : null;
     
    815815            'declined_message' => $quote_declined_message, // Use global declined message setting
    816816        );
    817        
     817
    818818        // Create a temporary WP_Post object for new quote
    819819        $empty_post = new \WP_Post((object) array(
     
    837837            'filter' => 'raw',
    838838        ));
    839        
     839
    840840        $quote = new \EasyInvoice\Models\Quote($empty_post);
    841        
     841
    842842        // Set default values on the quote object
    843843        foreach ($quote_data as $key => $value) {
     
    868868            }
    869869        }
    870        
     870
    871871        // Initialize empty items array
    872872        $quote->setItems([]);
    873        
     873
    874874        // Set variables needed by the form template
    875875        $quote_id = 0;
     
    879879        $admin_nonce = wp_create_nonce('easy_invoice_admin_nonce');
    880880        $quote_field_config = $quote_form_manager->getFieldConfigForJavaScript();
    881        
     881
    882882        // Start output buffering to capture form HTML
    883883        ob_start();
    884        
     884
    885885        // Include the quote form template
    886886        include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/form.php';
    887        
     887
    888888        // Get the captured HTML
    889889        $html = ob_get_clean();
    890        
     890
    891891        wp_send_json_success(['html' => $html]);
    892892    }
    893    
     893
    894894    /**
    895895     * Handle search clients AJAX request
     
    902902            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    903903        }
    904        
     904
    905905        // Check permissions
    906906        if (!current_user_can('manage_options')) {
    907907            wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
    908908        }
    909        
     909
    910910        $query = sanitize_text_field($_POST['query'] ?? '');
    911        
     911
    912912        // If query is empty, get all clients
    913913        if (empty($query)) {
     
    917917        $clients = $this->client_repository->search($query);
    918918        }
    919        
     919
    920920        $results = [];
    921921        foreach ($clients as $client) {
     
    930930            ];
    931931        }
    932        
     932
    933933        wp_send_json_success($results);
    934934    }
    935    
     935
    936936    /**
    937937     * Handle AJAX request to accept a quote
     
    944944            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    945945        }
    946        
     946
    947947        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    948        
     948
    949949        if ($quote_id <= 0) {
    950950            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    951951        }
    952        
     952
    953953        // Get the quote
    954954        $quote = $this->quote_repository->find($quote_id);
    955        
     955
    956956        if (!$quote) {
    957957            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    958958        }
    959        
     959
    960960        // Check if user has permission to accept this quote
    961961        $current_user = wp_get_current_user();
    962962        $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
    966968            if ($quote->getClientId()) {
    967969                $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
    968970                $client = $client_repository->find($quote->getClientId());
    969                
     971
    970972                if (!$client || $client->getEmail() !== $current_user->user_email) {
    971973                    wp_send_json_error(['message' => __('You do not have permission to accept this quote.', 'easy-invoice')]);
     
    975977            }
    976978        }
    977        
     979
    978980        // Get global accept action setting
    979981        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
    980982        $accept_action = $settings_controller::getQuoteAcceptAction();
    981        
     983
    982984        // Update quote status to accepted
    983985        $quote->setStatus('accepted');
    984986        $quote->setAcceptedDate(date('Y-m-d H:i:s'));
    985987        $quote->setAcceptedBy($current_user->ID);
    986        
     988
    987989        // Save the quote
    988990        $saved = $quote->save();
    989        
     991
    990992        if (!$saved) {
    991993            wp_send_json_error(['message' => __('Failed to accept quote.', 'easy-invoice')]);
    992994        }
    993        
     995
    994996        // Log the quote acceptance
    995997        $this->quote_log_service->logAcceptance($quote_id, [
     
    997999            'user_type' => $is_admin ? 'admin' : 'client'
    9981000        ]);
    999        
     1001
    10001002        // Perform the configured accept action
    10011003        $invoice_id = null;
    10021004        $action_message = '';
    1003        
     1005
    10041006        switch ($accept_action) {
    10051007            case 'convert':
     
    10111013                $action_message = __('Quote converted to invoice successfully.', 'easy-invoice');
    10121014                break;
    1013                
     1015
    10141016            case 'convert_available':
    10151017                // Convert quote to invoice (Available status)
     
    10201022                $action_message = __('Quote converted to invoice successfully.', 'easy-invoice');
    10211023                break;
    1022                
     1024
    10231025            case 'convert_send':
    10241026                // Convert quote to invoice and send to client (Available status)
     
    10291031                $action_message = __('Quote converted to invoice and sent to client successfully.', 'easy-invoice');
    10301032                break;
    1031                
     1033
    10321034            case 'duplicate':
    10331035                // Create new invoice, keep quote as-is (Draft status)
     
    10381040                $action_message = __('New invoice created from quote successfully.', 'easy-invoice');
    10391041                break;
    1040                
     1042
    10411043            case 'duplicate_send':
    10421044                // Create new invoice and send to client, keep quote as-is (Available status)
     
    10471049                $action_message = __('New invoice created and sent to client successfully.', 'easy-invoice');
    10481050                break;
    1049                
     1051
    10501052            case 'do_nothing':
    10511053            default:
     
    10541056                break;
    10551057        }
    1056        
     1058
    10571059        // Send notification email to admin
    10581060        if (!$is_admin) {
    10591061            $this->sendQuoteAcceptanceNotification($quote);
    10601062        }
    1061        
     1063
    10621064        // Get URLs for the new invoice
    10631065        $invoice_url = null;
    10641066        $secure_url = null;
    1065        
     1067
    10661068        if ($invoice_id) {
    10671069            // Always use WordPress permalink
     
    10751077            }
    10761078        }
    1077        
     1079
    10781080        wp_send_json_success([
    10791081            'message' => $action_message,
     
    10871089        ]);
    10881090    }
    1089    
     1091
    10901092    /**
    10911093     * Convert quote to invoice
     
    10991101            // Get invoice repository
    11001102            $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
    1101            
     1103
    11021104            // Create invoice data from quote - convert ALL fields
    11031105            $invoice_data = [
     
    11351137                'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
    11361138            ];
    1137            
     1139
    11381140            // Create the invoice
    11391141            $invoice = $invoice_repository->create($invoice_data);
    1140            
     1142
    11411143            if ($invoice) {
    11421144                // Store the quote ID in the invoice's meta for tracking
    11431145                update_post_meta($invoice->getId(), '_converted_from_quote', $quote->getId());
    1144                
     1146
    11451147                // Update quote to reference the created invoice
    11461148                $quote->setCustomField('converted_invoice_id', $invoice->getId());
    11471149                $quote->save();
    1148                
     1150
    11491151                // Ensure secure link is generated for the new invoice (Pro version)
    11501152                if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
     
    11521154                    do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
    11531155                }
    1154                
     1156
    11551157                return $invoice->getId();
    11561158            }
    1157            
     1159
    11581160            return null;
    11591161        } catch (\Exception $e) {
     
    11621164        }
    11631165    }
    1164    
     1166
    11651167    /**
    11661168     * Create new invoice from quote (duplicate)
     
    11741176            // Get invoice repository
    11751177            $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
    1176            
     1178
    11771179            // Create invoice data from quote - convert ALL fields
    11781180            $invoice_data = [
     
    12101212                'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
    12111213            ];
    1212            
     1214
    12131215            // Create the invoice
    12141216            $invoice = $invoice_repository->create($invoice_data);
    1215            
     1217
    12161218            if ($invoice) {
    12171219                // Link the invoice to the quote
    12181220                $quote->setCustomField('related_invoice_id', $invoice->getId());
    12191221                $quote->save();
    1220                
     1222
    12211223                // Ensure secure link is generated for the new invoice (Pro version)
    12221224                if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
     
    12241226                    do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
    12251227                }
    1226                
     1228
    12271229                return $invoice->getId();
    12281230            }
    1229            
     1231
    12301232            return null;
    12311233        } catch (\Exception $e) {
     
    12341236        }
    12351237    }
    1236    
     1238
    12371239    /**
    12381240     * Send invoice to client
     
    12461248            $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
    12471249            $invoice = $invoice_repository->find($invoice_id);
    1248            
     1250
    12491251            if (!$invoice) {
    12501252                return false;
    12511253            }
    1252            
     1254
    12531255            // Get email manager
    12541256            $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
    1255            
     1257
    12561258            // Send invoice email
    12571259            $result = $email_manager->sendInvoiceEmail($invoice, 'new');
    1258            
     1260
    12591261            return $result['success'];
    12601262        } catch (\Exception $e) {
     
    12631265        }
    12641266    }
    1265    
     1267
    12661268    /**
    12671269     * Convert quote items to invoice items
     
    12721274    private function convertQuoteItemsToInvoiceItems(array $quote_items): array {
    12731275        $invoice_items = [];
    1274        
     1276
    12751277        foreach ($quote_items as $quote_item) {
    12761278            if (is_object($quote_item) && method_exists($quote_item, 'toArray')) {
     
    13001302            }
    13011303        }
    1302        
     1304
    13031305        return $invoice_items;
    13041306    }
    1305    
     1307
    13061308    /**
    13071309     * Generate unique invoice number
     
    13151317            return $invoice_number_service->generateUniqueNumber();
    13161318        }
    1317        
     1319
    13181320        // Fallback to timestamp-based number
    13191321        return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
    13201322    }
    1321    
     1323
    13221324    /**
    13231325     * Get changes between two quote versions
     
    13291331    private function getQuoteChanges($old_quote, $new_quote): array {
    13301332        $changes = [];
    1331        
     1333
    13321334        // Compare key fields
    13331335        $fields_to_compare = [
     
    13431345            'terms' => 'Terms',
    13441346        ];
    1345        
     1347
    13461348        foreach ($fields_to_compare as $field => $label) {
    13471349            $method_name = 'get' . easy_invoice_str_replace('_', '', ucwords($field, '_'));
    1348            
     1350
    13491351            if (method_exists($old_quote, $method_name) && method_exists($new_quote, $method_name)) {
    13501352                $old_value = $old_quote->$method_name();
    13511353                $new_value = $new_quote->$method_name();
    1352                
     1354
    13531355                if ($old_value !== $new_value) {
    13541356                    $changes[$field] = $new_value;
     
    13561358            }
    13571359        }
    1358        
     1360
    13591361        return $changes;
    13601362    }
    1361    
     1363
    13621364    /**
    13631365     * Handle AJAX request to decline a quote
     
    13701372            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    13711373        }
    1372        
     1374
    13731375        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    13741376        $decline_reason = isset($_POST['decline_reason']) ? sanitize_textarea_field($_POST['decline_reason']) : '';
    1375        
     1377
    13761378        if ($quote_id <= 0) {
    13771379            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    13781380        }
    1379        
     1381
    13801382        // Get the quote
    13811383        $quote = $this->quote_repository->find($quote_id);
    1382        
     1384
    13831385        if (!$quote) {
    13841386            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    13851387        }
    1386        
     1388
    13871389        // Check if decline reason is required by global settings
    13881390        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    13901392            wp_send_json_error(['message' => __('Reason for declining is required.', 'easy-invoice')]);
    13911393        }
    1392        
     1394
    13931395        // Check if user has permission to decline this quote
    13941396        $current_user = wp_get_current_user();
    13951397        $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') {
    13981402            // For non-admins, check if they are the client
    13991403            if ($quote->getClientId()) {
    14001404                $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
    14011405                $client = $client_repository->find($quote->getClientId());
    1402                
     1406
    14031407                if (!$client || $client->getEmail() !== $current_user->user_email) {
    14041408                    wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]);
     
    14081412            }
    14091413        }
    1410        
     1414
    14111415        // Update quote status to declined
    14121416        $quote->setStatus('declined');
    14131417        $quote->setDeclinedDate(date('Y-m-d H:i:s'));
    14141418        $quote->setDeclinedBy($current_user->ID);
    1415        
     1419
    14161420        // Save decline reason if provided
    14171421        if (!empty($decline_reason)) {
    14181422            $quote->setDeclineReason($decline_reason);
    14191423        }
    1420        
     1424
    14211425        // Save the quote
    14221426        $saved = $quote->save();
    1423        
     1427
    14241428        if (!$saved) {
    14251429            wp_send_json_error(['message' => __('Failed to decline quote.', 'easy-invoice')]);
    14261430        }
    1427        
     1431
    14281432        // Log the quote decline
    14291433        $this->quote_log_service->logDecline($quote_id, $decline_reason, [
    14301434            'user_type' => $is_admin ? 'admin' : 'client'
    14311435        ]);
    1432        
     1436
    14331437        // Send notification email to admin
    14341438        if (!$is_admin) {
    14351439            $this->sendQuoteDeclineNotification($quote);
    14361440        }
    1437        
     1441
    14381442        wp_send_json_success([
    14391443            'message' => __('Quote declined successfully.', 'easy-invoice'),
     
    14441448        ]);
    14451449    }
    1446    
     1450
    14471451    /**
    14481452     * Send quote acceptance notification to admin
     
    14551459        $email_manager->sendAdminQuoteNotification($quote, 'accepted');
    14561460    }
    1457    
     1461
    14581462    /**
    14591463     * Send quote decline notification to admin
     
    14661470        $email_manager->sendAdminQuoteNotification($quote, 'declined');
    14671471    }
    1468    
     1472
    14691473    /**
    14701474     * Handle AJAX request to update existing quotes with missing data
     
    14771481            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    14781482        }
    1479        
     1483
    14801484        // Check permissions
    14811485        if (!current_user_can('manage_options')) {
    14821486            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    14831487        }
    1484        
     1488
    14851489        $updated_count = 0;
    14861490        $quotes = $this->quote_repository->findAll();
    1487        
     1491
    14881492        foreach ($quotes as $quote) {
    14891493            $post = get_post($quote->getId());
     
    14921496                $post_title = $quote->getTitle() ?: $quote->getNumber() ?: 'Untitled Quote';
    14931497                $post_name = sanitize_title($post_title);
    1494                
     1498
    14951499                // Ensure uniqueness
    14961500                $original_slug = $post_name;
     
    15001504                    $counter++;
    15011505                }
    1502                
     1506
    15031507                // Update the post with the new slug
    15041508                wp_update_post([
     
    15061510                    'post_name' => $post_name
    15071511                ]);
    1508                
     1512
    15091513                $updated_count++;
    15101514            }
    15111515        }
    1512        
     1516
    15131517        wp_send_json_success([
    15141518            'message' => sprintf(__('Updated %d quotes with proper URLs.', 'easy-invoice'), $updated_count)
    15151519        ]);
    15161520    }
    1517    
     1521
    15181522    /**
    15191523     * Handle AJAX request to duplicate a quote
     
    15261530            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    15271531        }
    1528        
     1532
    15291533        // Check permissions
    15301534        if (!current_user_can('manage_options')) {
    15311535            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    15321536        }
    1533        
     1537
    15341538        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1535        
     1539
    15361540        if ($quote_id <= 0) {
    15371541            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    15381542        }
    1539        
     1543
    15401544        $quote = $this->quote_repository->find($quote_id);
    1541        
     1545
    15421546        if (!$quote) {
    15431547            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    15441548        }
    1545        
     1549
    15461550        // Get global quote settings
    15471551        $settings_controller = new \EasyInvoice\Controllers\SettingsController();
     
    15531557        $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
    15541558        $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
    1555        
     1559
    15561560        // Create the duplicate quote
    15571561        $duplicate_data = [
     
    15721576            'declined_message' => $quote_declined_message,
    15731577        ];
    1574        
     1578
    15751579        // Set client ID to 0 for a new quote
    15761580        $duplicate_data['client_id'] = 0;
    1577        
     1581
    15781582        $duplicate_quote = $this->quote_repository->create($duplicate_data);
    1579        
     1583
    15801584        if ($duplicate_quote) {
    15811585            $this->quote_log_service->logActivity($quote_id, 'duplicate', 'Quote duplicated', ['duplicate_id' => $duplicate_quote->getId()]);
     
    15921596        }
    15931597    }
    1594    
     1598
    15951599    /**
    15961600     * Handle regular POST form actions for quote accept/decline
     
    16031607            return;
    16041608        }
    1605        
     1609
    16061610        // Handle accept quote
    16071611        if (isset($_POST['accept_quote']) && isset($_POST['quote_id'])) {
    16081612            $this->handleAcceptQuoteForm();
    16091613        }
    1610        
     1614
    16111615        // Handle decline quote
    16121616        if (isset($_POST['decline_quote']) && isset($_POST['quote_id'])) {
     
    16141618        }
    16151619    }
    1616    
     1620
    16171621    /**
    16181622     * Handle accept quote form submission
     
    16251629            wp_die(__('Security check failed.', 'easy-invoice'));
    16261630        }
    1627        
     1631
    16281632        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1629        
     1633
    16301634        if ($quote_id <= 0) {
    16311635            wp_die(__('Invalid quote ID.', 'easy-invoice'));
    16321636        }
    1633        
     1637
    16341638        // Get the quote
    16351639        $quote = $this->quote_repository->find($quote_id);
    1636        
     1640
    16371641        if (!$quote) {
    16381642            wp_die(__('Quote not found.', 'easy-invoice'));
    16391643        }
    1640        
     1644
    16411645        // Check if user has permission to accept this quote
    16421646        $current_user = wp_get_current_user();
    16431647        $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
    16451715        if (!$is_admin) {
    16461716            // For non-admins, check if they are the client
     
    16481718                $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
    16491719                $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
    17161721                if (!$client || $client->getEmail() !== $current_user->user_email) {
    17171722                    wp_die(__('You do not have permission to decline this quote.', 'easy-invoice'));
     
    17211726            }
    17221727        }
    1723        
     1728
    17241729        // Update quote status to declined
    17251730        $quote->setStatus('declined');
    17261731        $quote->setDeclinedDate(date('Y-m-d H:i:s'));
    17271732        $quote->setDeclinedBy($current_user->ID);
    1728        
     1733
    17291734        // Save the quote
    17301735        $saved = $quote->save();
    1731        
     1736
    17321737        if (!$saved) {
    17331738            wp_die(__('Failed to decline quote.', 'easy-invoice'));
    17341739        }
    1735        
     1740
    17361741        // Send notification email to admin
    17371742        if (!$is_admin) {
    17381743            $this->sendQuoteDeclineNotification($quote);
    17391744        }
    1740        
     1745
    17411746        // Redirect back to the quote page with success message
    17421747        $redirect_url = add_query_arg('action', 'declined', get_permalink($quote_id));
     
    17441749        exit;
    17451750    }
    1746    
     1751
    17471752    /**
    17481753     * Handle AJAX request for bulk quote actions
     
    17551760            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    17561761        }
    1757        
     1762
    17581763        // Check permissions
    17591764        if (!current_user_can('manage_options')) {
    17601765            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    17611766        }
    1762        
     1767
    17631768        $quote_ids = isset($_POST['quote_ids']) ? array_map('intval', $_POST['quote_ids']) : [];
    17641769        $bulk_action = sanitize_text_field($_POST['bulk_action'] ?? '');
    1765        
     1770
    17661771        if (empty($quote_ids)) {
    17671772            wp_send_json_error(['message' => __('No quotes selected.', 'easy-invoice')]);
    17681773        }
    1769        
     1774
    17701775        if (empty($bulk_action)) {
    17711776            wp_send_json_error(['message' => __('No action selected.', 'easy-invoice')]);
    17721777        }
    1773        
     1778
    17741779        $success_count = 0;
    17751780        $error_count = 0;
    1776        
     1781
    17771782        foreach ($quote_ids as $quote_id) {
    17781783            $quote = $this->quote_repository->find($quote_id);
    1779            
     1784
    17801785            if (!$quote) {
    17811786                $error_count++;
    17821787                continue;
    17831788            }
    1784            
     1789
    17851790            try {
    17861791                switch ($bulk_action) {
     
    17931798                        }
    17941799                        break;
    1795                        
     1800
    17961801                    case 'trash':
    17971802                        $old_status = $quote->getStatus();
     
    18041809                        }
    18051810                        break;
    1806                        
     1811
    18071812                    case 'draft':
    18081813                        $old_status = $quote->getStatus();
     
    18151820                        }
    18161821                        break;
    1817                        
     1822
    18181823                    case 'restore':
    18191824                        $old_status = $quote->getStatus();
     
    18261831                        }
    18271832                        break;
    1828                        
     1833
    18291834                    default:
    18301835                        $error_count++;
     
    18361841            }
    18371842        }
    1838        
     1843
    18391844        if ($error_count > 0) {
    18401845            wp_send_json_success([
     
    18551860        }
    18561861    }
    1857    
     1862
    18581863    /**
    18591864     * Handle AJAX request to trash a quote
     
    18661871            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    18671872        }
    1868        
     1873
    18691874        // Check permissions
    18701875        if (!current_user_can('manage_options')) {
    18711876            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    18721877        }
    1873        
     1878
    18741879        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1875        
     1880
    18761881        if ($quote_id <= 0) {
    18771882            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    18781883        }
    1879        
     1884
    18801885        $quote = $this->quote_repository->find($quote_id);
    1881        
     1886
    18821887        if (!$quote) {
    18831888            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    18841889        }
    1885        
     1890
    18861891        // Set status to cancelled before moving to trash
    18871892        $old_status = $quote->getStatus();
    18881893        $quote->setStatus('cancelled');
    18891894        $quote->save();
    1890        
     1895
    18911896        // Move the post to trash status
    18921897        $result = wp_trash_post($quote_id);
    1893        
     1898
    18941899        if ($result) {
    18951900            $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled');
     
    19051910        }
    19061911    }
    1907    
     1912
    19081913    /**
    19091914     * Handle AJAX request to move a quote to draft
     
    19161921            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    19171922        }
    1918        
     1923
    19191924        // Check permissions
    19201925        if (!current_user_can('manage_options')) {
    19211926            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    19221927        }
    1923        
     1928
    19241929        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1925        
     1930
    19261931        if ($quote_id <= 0) {
    19271932            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    19281933        }
    1929        
     1934
    19301935        $quote = $this->quote_repository->find($quote_id);
    1931        
     1936
    19321937        if (!$quote) {
    19331938            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    19341939        }
    1935        
     1940
    19361941        // Set status to draft
    19371942        $old_status = $quote->getStatus();
    19381943        $quote->setStatus('draft');
    1939        
     1944
    19401945        if ($quote->save()) {
    19411946            $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft');
     
    19511956        }
    19521957    }
    1953    
     1958
    19541959    /**
    19551960     * Handle AJAX request to restore a trashed quote
     
    19621967            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    19631968        }
    1964        
     1969
    19651970        // Check permissions
    19661971        if (!current_user_can('manage_options')) {
    19671972            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    19681973        }
    1969        
     1974
    19701975        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    1971        
     1976
    19721977        if ($quote_id <= 0) {
    19731978            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    19741979        }
    1975        
     1980
    19761981        $quote = $this->quote_repository->find($quote_id);
    1977        
     1982
    19781983        if (!$quote) {
    19791984            wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
    19801985        }
    1981        
     1986
    19821987        // Restore the post from trash
    19831988        $result = wp_untrash_post($quote_id);
    1984        
     1989
    19851990        if ($result) {
    19861991            // After restoring from trash, set the meta status to available
    19871992            $quote->setStatus('available');
    19881993            $quote->save();
    1989            
     1994
    19901995            $this->quote_log_service->logRestoration($quote_id);
    19911996            wp_send_json_success([
     
    20002005        }
    20012006    }
    2002    
     2007
    20032008    /**
    20042009     * Handle AJAX request to empty trash
     
    20122017                wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    20132018            }
    2014            
     2019
    20152020            // Check permissions
    20162021            if (!current_user_can('manage_options')) {
    20172022                wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    20182023            }
    2019            
     2024
    20202025            // Get all quotes in trash (post_status = 'trash')
    20212026            global $wpdb;
    20222027            $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
    20252030                AND post_status = 'trash'",
    20262031                PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
    20272032            ));
    2028            
     2033
    20292034            if (empty($quote_ids)) {
    20302035                wp_send_json_error(['message' => __('No quotes found in trash.', 'easy-invoice')]);
    20312036            }
    2032            
     2037
    20332038            $success_count = 0;
    20342039            $error_count = 0;
    2035            
     2040
    20362041            foreach ($quote_ids as $quote_id) {
    20372042                if (wp_delete_post($quote_id, true)) {
     
    20422047                }
    20432048            }
    2044            
     2049
    20452050            if ($error_count > 0) {
    20462051                wp_send_json_success([
     
    20642069                ]);
    20652070            }
    2066            
     2071
    20672072        } catch (\Exception $e) {
    20682073            error_log('Error emptying quote trash: ' . $e->getMessage());
     
    20732078        }
    20742079    }
    2075    
     2080
    20762081    /**
    20772082     * Handle AJAX request to get quote logs
     
    20842089            wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
    20852090        }
    2086        
     2091
    20872092        // Check permissions
    20882093        if (!current_user_can('manage_options')) {
    20892094            wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
    20902095        }
    2091        
     2096
    20922097        $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
    2093        
     2098
    20942099        if ($quote_id <= 0) {
    20952100            wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
    20962101        }
    2097        
     2102
    20982103        try {
    20992104            $logs = $this->quote_log_service->getLogs($quote_id);
    2100            
     2105
    21012106            // Convert QuoteLog objects to arrays for JSON response
    21022107            $logs_data = [];
     
    21132118                ];
    21142119            }
    2115            
     2120
    21162121            wp_send_json_success([
    21172122                'logs' => $logs_data,
    21182123                'count' => count($logs_data)
    21192124            ]);
    2120            
     2125
    21212126        } catch (\Exception $e) {
    21222127            wp_send_json_error([
     
    21262131        }
    21272132    }
    2128    
     2133
    21292134    /**
    21302135     * Format currency amount using QuoteFormatter
     
    21382143        return $formatter->format($amount);
    21392144    }
    2140 } 
     2145}
  • easy-invoice/trunk/readme.txt

    r3417137 r3420035  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.1.6
     7Stable tag: 2.1.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    137137
    138138== Changelog ==
    139 
     139= 2.1.7 - 2025-12-15 =
     140* Checked - fix quote accept and decline issue
    140141= 2.1.6 - 2025-12-11 =
    141142* Checked - WordPress 6.9 compatibility tested
Note: See TracChangeset for help on using the changeset viewer.