PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.2
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Controllers / QuoteController.php

QuoteController.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.1.2, at includes/Controllers/QuoteController.php

2,094 lines 80.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quote Controller
4 *
5 * @package EasyInvoice
6 * @author Your Name
7 * @copyright Copyright (c) 2023, Your Company
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Controllers;
13
14 use EasyInvoice\Repositories\QuoteRepository;
15 use EasyInvoice\Repositories\ClientRepository;
16 use EasyInvoice\Forms\FormProcessor;
17 use EasyInvoice\Constants\PagesSlugs;
18 use EasyInvoice\Constants\PostTypes;
19 use EasyInvoice\Services\QuoteLogService;
20
21 /**
22 * Quote Controller
23 *
24 * Handles quote-related operations and displays.
25 *
26 * @since 1.0.0
27 */
28 class QuoteController {
29
30 /**
31 * Quote repository
32 *
33 * @var QuoteRepository
34 */
35 private $quote_repository;
36
37 /**
38 * Client repository
39 *
40 * @var ClientRepository
41 */
42 private $client_repository;
43
44 /**
45 * Form processor
46 *
47 * @var FormProcessor
48 */
49 private $form_processor;
50
51 /**
52 * Quote log service
53 *
54 * @var QuoteLogService
55 */
56 private $quote_log_service;
57
58 /**
59 * Constructor
60 *
61 * @since 1.0.0
62 */
63 public function __construct() {
64 $this->quote_repository = new QuoteRepository();
65 $this->client_repository = new ClientRepository();
66 $this->form_processor = new FormProcessor();
67 $this->quote_log_service = new QuoteLogService();
68 }
69
70 /**
71 * Initialize the controller
72 *
73 * @since 1.0.0
74 */
75 public function init(): void {
76 // Allow plugins to extend the controller initialization
77 do_action('easy_invoice_quote_controller_before_init', $this);
78
79 // Add AJAX handlers
80 add_action('wp_ajax_easy_invoice_delete_quote', [$this, 'handleDeleteQuote']);
81 add_action('wp_ajax_easy_invoice_get_quote', [$this, 'handleGetQuote']);
82 add_action('wp_ajax_easy_invoice_load_quote_template', [$this, 'handleLoadQuoteTemplate']);
83 add_action('wp_ajax_easy_invoice_create_new_quote', [$this, 'handleCreateNewQuote']);
84 add_action('wp_ajax_easy_invoice_search_clients', [$this, 'handleSearchClients']);
85 add_action('wp_ajax_easy_invoice_load_quote_form', [$this, 'handleLoadQuoteForm']);
86 add_action('wp_ajax_easy_invoice_accept_quote', [$this, 'handleAcceptQuote']);
87 add_action('wp_ajax_easy_invoice_decline_quote', [$this, 'handleDeclineQuote']);
88 add_action('wp_ajax_nopriv_easy_invoice_accept_quote', [$this, 'handleAcceptQuote']);
89 add_action('wp_ajax_nopriv_easy_invoice_decline_quote', [$this, 'handleDeclineQuote']);
90 add_action('wp_ajax_easy_invoice_update_existing_quotes', [$this, 'handleUpdateExistingQuotes']);
91
92 // Add missing AJAX handlers for quote listing actions
93 add_action('wp_ajax_easy_invoice_bulk_quote_action', [$this, 'handleBulkQuoteAction']);
94 add_action('wp_ajax_easy_invoice_trash_quote', [$this, 'handleTrashQuote']);
95 add_action('wp_ajax_easy_invoice_draft_quote', [$this, 'handleDraftQuote']);
96
97 // Add regular POST form handlers for quote actions
98 add_action('init', [$this, 'handleQuoteFormActions']);
99
100 // Add new AJAX handler for restoring a trashed quote
101 add_action('wp_ajax_easy_invoice_restore_quote', [ $this, 'handleRestoreQuote' ]);
102
103 // Add new AJAX handler for emptying trash
104 add_action('wp_ajax_easy_invoice_empty_trash', [ $this, 'handleEmptyTrash' ]);
105
106 // Add new AJAX handler for getting quote logs
107 add_action('wp_ajax_easy_invoice_get_quote_logs', [ $this, 'handleGetQuoteLogs' ]);
108
109 // Allow plugins to extend the controller initialization
110 do_action('easy_invoice_quote_controller_after_init', $this);
111 }
112
113 /**
114 * Display quote pages
115 *
116 * @since 1.0.0
117 * @param array $args Display arguments
118 */
119 public function display(array $args = []): void {
120 // Allow plugins to modify display arguments
121 $args = apply_filters('easy_invoice_quote_controller_display_args', $args);
122
123 $page = $args['page'] ?? '';
124
125 // Allow plugins to modify the page before processing
126 $page = apply_filters('easy_invoice_quote_controller_display_page', $page, $args);
127
128 switch ($page) {
129 case PagesSlugs::ALL_QUOTES:
130 $this->displayListing();
131 break;
132
133 case PagesSlugs::QUOTE_NEW:
134 $this->displayBuilder();
135 break;
136
137 case PagesSlugs::QUOTE_PREVIEW:
138 $this->displayPreview($args);
139 break;
140
141 default:
142 $this->displayListing();
143 break;
144 }
145
146 // Allow plugins to perform actions after display
147 do_action('easy_invoice_quote_controller_after_display', $page, $args);
148 }
149
150 /**
151 * Display quote listing page
152 *
153 * @since 1.0.0
154 */
155 private function displayListing(): void {
156 // First, get all counts independently of any filtering
157 global $wpdb;
158
159 // Get trash count first (based on post_status)
160 $trash_count = (int)$wpdb->get_var($wpdb->prepare(
161 "SELECT COUNT(*) FROM {$wpdb->posts}
162 WHERE post_type = %s AND post_status = 'trash'",
163 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
164 ));
165
166 // Get counts for each meta status (excluding trashed posts)
167 $status_counts = $wpdb->get_results($wpdb->prepare(
168 "SELECT COALESCE(pm.meta_value, 'draft') as status, COUNT(*) as count
169 FROM {$wpdb->posts} p
170 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_easy_invoice_quote_status'
171 WHERE p.post_type = %s
172 AND p.post_status != 'trash'
173 GROUP BY COALESCE(pm.meta_value, 'draft')",
174 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
175 ));
176
177 // Initialize counts
178 $draft_count = 0;
179 $available_count = 0;
180 $sent_count = 0;
181 $accepted_count = 0;
182 $declined_count = 0;
183 $expired_count = 0;
184 $cancelled_count = 0;
185 $all_count = 0;
186
187 // Process status counts
188 foreach ($status_counts as $status) {
189 $count = (int)$status->count;
190 $all_count += $count; // Add to total (excluding trash)
191
192 switch ($status->status) {
193 case 'draft':
194 $draft_count = $count;
195 break;
196 case 'available':
197 $available_count = $count;
198 break;
199 case 'sent':
200 $sent_count = $count;
201 break;
202 case 'accepted':
203 $accepted_count = $count;
204 break;
205 case 'declined':
206 $declined_count = $count;
207 break;
208 case 'expired':
209 $expired_count = $count;
210 break;
211 case 'cancelled':
212 $cancelled_count = $count;
213 break;
214 }
215 }
216
217 // Now handle the display filtering
218 // Allow plugins to perform actions before displaying listing
219 do_action('easy_invoice_quote_controller_before_display_listing');
220
221 // Get filter parameters
222 $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
223 $search_query = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : '';
224 $current_view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all';
225 $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
226 $per_page = 20;
227
228 // Build query args for display
229 $query_args = [
230 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
231 'posts_per_page' => $per_page,
232 'paged' => $current_page,
233 'orderby' => 'date',
234 'order' => 'DESC',
235 'no_found_rows' => false,
236 'update_post_term_cache' => false,
237 'update_post_meta_cache' => false
238 ];
239
240 // Handle view filtering
241 if ($current_view === 'trash' || $current_view === 'cancelled') {
242 // For trash and cancelled views, look at post_status = 'trash'
243 $query_args['post_status'] = 'trash';
244
245 // For cancelled view, also filter by meta status
246 if ($current_view === 'cancelled') {
247 $query_args['meta_query'] = [
248 [
249 'key' => '_easy_invoice_quote_status',
250 'value' => 'cancelled',
251 'compare' => '='
252 ]
253 ];
254 }
255 } else {
256 // For all other views, exclude trashed posts
257 $query_args['post_status'] = ['publish', 'draft', 'private', 'pending'];
258
259 if ($current_view !== 'all') {
260 // For specific status views, add meta query
261 $query_args['meta_query'] = [
262 [
263 'key' => '_easy_invoice_quote_status',
264 'value' => $current_view,
265 'compare' => '='
266 ]
267 ];
268 }
269 }
270
271 // Add search if provided
272 if (!empty($search_query)) {
273 $search_ids = [];
274
275 // Build base query args for search
276 $search_query_args = [
277 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
278 'post_status' => $query_args['post_status'],
279 'posts_per_page' => -1,
280 'fields' => 'ids' // Only get IDs for better performance
281 ];
282
283 // Search in title and content
284 $title_search_args = array_merge($search_query_args, [
285 's' => $search_query
286 ]);
287 $title_search = new \WP_Query($title_search_args);
288 $search_ids = $title_search->posts;
289
290 // Search in meta
291 $meta_search_args = array_merge($search_query_args, [
292 'meta_query' => [
293 'relation' => 'OR',
294 [
295 'key' => '_easy_invoice_quote_number',
296 'value' => $search_query,
297 'compare' => 'LIKE'
298 ],
299 [
300 'key' => '_easy_invoice_quote_client_name',
301 'value' => $search_query,
302 'compare' => 'LIKE'
303 ],
304 [
305 'key' => '_easy_invoice_quote_client_email',
306 'value' => $search_query,
307 'compare' => 'LIKE'
308 ]
309 ]
310 ]);
311 $meta_search = new \WP_Query($meta_search_args);
312
313 if ($meta_search->have_posts()) {
314 $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID'));
315 }
316
317 $search_ids = array_unique($search_ids);
318
319 if (!empty($search_ids)) {
320 $query_args['post__in'] = $search_ids;
321 } else {
322 $query_args['post__in'] = [0];
323 }
324 }
325
326 // Allow plugins to modify query args
327 $query_args = apply_filters('easy_invoice_quote_controller_final_query_args', $query_args);
328 // Get filtered quotes for display
329 $wp_query = new \WP_Query($query_args);
330 $quotes = [];
331
332 if ($wp_query->have_posts()) {
333 foreach ($wp_query->posts as $post) {
334 $quote = $this->quote_repository->find($post->ID);
335 if ($quote) {
336 $quotes[] = $quote;
337 }
338 }
339 }
340
341 // Allow plugins to modify the quotes array
342 $quotes = apply_filters('easy_invoice_quote_controller_quotes_list', $quotes, $wp_query);
343
344 // Get pagination info from WordPress query
345 $total_quotes = $wp_query->found_posts;
346 $total_pages = $wp_query->max_num_pages;
347
348 // Initialize counts
349 $draft_count = 0;
350 $available_count = 0;
351 $sent_count = 0;
352 $accepted_count = 0;
353 $declined_count = 0;
354 $expired_count = 0;
355 $cancelled_count = 0;
356
357 // Process status counts
358 foreach ($status_counts as $status) {
359 switch ($status->status) {
360 case 'draft':
361 $draft_count = $status->count;
362 break;
363 case 'available':
364 $available_count = $status->count;
365 break;
366 case 'sent':
367 $sent_count = $status->count;
368 break;
369 case 'accepted':
370 $accepted_count = $status->count;
371 break;
372 case 'declined':
373 $declined_count = $status->count;
374 break;
375 case 'expired':
376 $expired_count = $status->count;
377 break;
378 case 'cancelled':
379 $cancelled_count = $status->count;
380 break;
381 }
382 }
383
384 // Prepare template data
385 $template_data = [
386 'quotes' => $quotes,
387 'current_view' => $current_view,
388 'status_filter' => $status_filter,
389 'search_query' => $search_query,
390 'all_count' => (int)$all_count,
391 'trash_count' => (int)$trash_count,
392 'draft_count' => (int)$draft_count,
393 'available_count' => (int)$available_count,
394 'sent_count' => (int)$sent_count,
395 'accepted_count' => (int)$accepted_count,
396 'declined_count' => (int)$declined_count,
397 'expired_count' => (int)$expired_count,
398 'cancelled_count' => (int)$cancelled_count,
399 'repository' => $this->quote_repository,
400 'current_page' => $current_page,
401 'per_page' => $per_page,
402 'total_quotes' => $total_quotes,
403 'total_pages' => $total_pages,
404 'wp_query' => $wp_query
405 ];
406
407 // Allow plugins to modify template data
408 $template_data = apply_filters('easy_invoice_quote_controller_template_data', $template_data);
409
410 // Display the template
411 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/listing.php';
412
413 // Allow plugins to perform actions after displaying listing
414 do_action('easy_invoice_quote_controller_after_display_listing', $template_data);
415 }
416
417 /**
418 * Display quote builder page
419 *
420 * @since 1.0.0
421 */
422 private function displayBuilder(): void {
423 // Allow plugins to perform actions before displaying builder
424 do_action('easy_invoice_quote_controller_before_display_builder');
425
426 $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
427 $quote = null;
428
429 if ($quote_id > 0) {
430 $quote = $this->quote_repository->find($quote_id);
431 }
432
433 $clients = $this->client_repository->all();
434
435 // Allow plugins to modify the data
436 $quote = apply_filters('easy_invoice_quote_controller_builder_quote', $quote, $quote_id);
437 $clients = apply_filters('easy_invoice_quote_controller_builder_clients', $clients);
438
439 // Include the builder template
440 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/builder.php';
441
442 // Allow plugins to perform actions after displaying builder
443 do_action('easy_invoice_quote_controller_after_display_builder', $quote, $clients);
444 }
445
446 /**
447 * Display quote preview page
448 *
449 * @since 1.0.0
450 * @param array $args Display arguments
451 */
452 private function displayPreview(array $args): void {
453 // Allow plugins to perform actions before displaying preview
454 do_action('easy_invoice_quote_controller_before_display_preview', $args);
455
456 $quote_id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
457
458 if ($quote_id <= 0) {
459 wp_die(__('Quote not found.', 'easy-invoice'));
460 }
461
462 $quote = $this->quote_repository->find($quote_id);
463 if (!$quote) {
464 wp_die(__('Quote not found.', 'easy-invoice'));
465 }
466
467 // Allow plugins to modify the quote
468 $quote = apply_filters('easy_invoice_quote_controller_preview_quote', $quote, $quote_id);
469
470 // Include the preview template
471 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/preview.php';
472
473 // Allow plugins to perform actions after displaying preview
474 do_action('easy_invoice_quote_controller_after_display_preview', $quote, $args);
475 }
476
477 /**
478 * Handle delete quote AJAX request
479 *
480 * @since 1.0.0
481 */
482 public function handleDeleteQuote(): void {
483 // Verify nonce
484 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
485 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
486 }
487
488 // Check permissions
489 if (!current_user_can('manage_options')) {
490 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
491 }
492
493 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
494
495 if ($quote_id <= 0) {
496 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
497 }
498
499 if ($this->quote_repository->delete($quote_id)) {
500 // Log the quote deletion
501 $this->quote_log_service->logDeletion($quote_id);
502
503 wp_send_json_success([
504 'message' => __('Quote deleted successfully.', 'easy-invoice'),
505 'toast' => [
506 'type' => 'success',
507 'message' => __('Quote deleted successfully.', 'easy-invoice')
508 ]
509 ]);
510 } else {
511 wp_send_json_error(['message' => __('Failed to delete quote.', 'easy-invoice')]);
512 }
513 }
514
515 /**
516 * Handle get quote AJAX request
517 *
518 * @since 1.0.0
519 */
520 public function handleGetQuote(): void {
521 // Verify nonce
522 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_get_quote')) {
523 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
524 }
525
526 // Check permissions
527 if (!current_user_can('manage_options')) {
528 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
529 }
530
531 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
532
533 if ($quote_id <= 0) {
534 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
535 }
536
537 $quote = $this->quote_repository->find($quote_id);
538
539 if (!$quote) {
540 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
541 }
542
543 wp_send_json_success(['quote' => $quote->toArray()]);
544 }
545
546 /**
547 * Handle AJAX request to load quote template
548 *
549 * @since 1.0.0
550 */
551 public function handleLoadQuoteTemplate(): void {
552 // Verify nonce
553 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_nonce')) {
554 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
555 }
556
557 // Check permissions
558 if (!current_user_can('manage_options')) {
559 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
560 }
561
562 $template_id = sanitize_text_field($_POST['template'] ?? '');
563 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
564
565 if (empty($template_id)) {
566 wp_send_json_error(['message' => __('Template ID is required.', 'easy-invoice')]);
567 }
568
569 // Load quote if provided
570 $quote = null;
571 if ($quote_id > 0) {
572 $quote = $this->quote_repository->find($quote_id);
573 }
574
575 // Check if template file exists
576 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/quote-templates/' . $template_id . '.php';
577
578 if (!file_exists($template_file)) {
579 wp_send_json_error(['message' => __('Template not found.', 'easy-invoice')]);
580 }
581
582 // Start output buffering to capture template HTML
583 ob_start();
584
585 // Include the template file
586 include $template_file;
587
588 // Get the captured HTML
589 $html = ob_get_clean();
590
591 wp_send_json_success(['html' => $html]);
592 }
593
594 /**
595 * Handle AJAX request to create a new quote with just the title
596 *
597 * @since 1.0.0
598 */
599 public function handleCreateNewQuote(): void {
600 // Verify nonce
601 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
602 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
603 }
604 // Check permissions
605 if (!current_user_can('manage_options')) {
606 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
607 }
608 $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
609 if (empty($title)) {
610 wp_send_json_error(['message' => __('Quote title is required.', 'easy-invoice')]);
611 }
612
613 // Generate a unique quote number
614 $quote_number = '';
615 if (class_exists('\\EasyInvoice\\Services\\QuoteNumberService')) {
616 $quote_number_service = new \EasyInvoice\Services\QuoteNumberService();
617 $quote_number = $quote_number_service->generateUniqueNumber();
618 } else {
619 // Fallback if service doesn't exist
620 $quote_number = 'QT-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
621 }
622
623 // Get global quote settings
624 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
625 $quote_terms = $settings_controller::getQuoteTermsConditions();
626 $quote_footer = $settings_controller::getQuoteFooterText();
627 $quote_accept_button = get_option('easy_invoice_quote_accept_button', 'yes');
628 $quote_accept_action = get_option('easy_invoice_quote_accept_action', 'email');
629 $quote_accept_text = get_option('easy_invoice_quote_accept_text', __('Accept Quote', 'easy-invoice'));
630 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
631 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
632
633 // Create the quote with just the title and default values
634 $data = [
635 'title' => $title,
636 'status' => 'draft',
637 'number' => $quote_number, // Use the generated unique number
638 'issue_date' => date('Y-m-d'),
639 'expiry_date' => date('Y-m-d', strtotime('+30 days')),
640 'items' => [],
641 'notes' => '', // Ensure notes is never null
642 'terms' => $quote_terms, // Use global terms setting
643 'footer_text' => $quote_footer, // Use global footer setting
644 'accept_button' => $quote_accept_button, // Use global accept button setting
645 'accept_action' => $quote_accept_action, // Use global accept action setting
646 'accept_text' => $quote_accept_text, // Use global accept text setting
647 'accepted_message' => $quote_accepted_message, // Use global accepted message setting
648 'declined_message' => $quote_declined_message, // Use global declined message setting
649 ];
650 $quote = $this->quote_repository->create($data);
651 if (!$quote) {
652 wp_send_json_error(['message' => __('Failed to create quote.', 'easy-invoice')]);
653 }
654 wp_send_json_success(['quote_id' => $quote->getId()]);
655 }
656
657 /**
658 * Handle AJAX request to load quote form for modal
659 *
660 * @since 1.0.0
661 */
662 public function handleLoadQuoteForm(): void {
663 // Verify nonce
664 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
665 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
666 }
667
668 // Check permissions
669 if (!current_user_can('manage_options')) {
670 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
671 }
672
673 // Get global quote settings
674 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
675 $quote_terms = $settings_controller::getQuoteTermsConditions();
676 $quote_footer = $settings_controller::getQuoteFooterText();
677 $quote_accept_button = get_option('easy_invoice_quote_accept_button', 'yes');
678 $quote_accept_action = get_option('easy_invoice_quote_accept_action', 'email');
679 $quote_accept_text = get_option('easy_invoice_quote_accept_text', __('Accept Quote', 'easy-invoice'));
680 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
681 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
682
683 // Create a new quote object for the form
684 $quote_number_service = function_exists('easy_invoice_get_quote_number_service') ? easy_invoice_get_quote_number_service() : null;
685 $quote_data = array(
686 'number' => $quote_number_service ? $quote_number_service->getNextNumber() : 'QT-1',
687 'date' => date('Y-m-d'),
688 'expiry_date' => date('Y-m-d', strtotime('+30 days')),
689 'client_id' => 0,
690 'client_name' => '',
691 'client_email' => '',
692 'client_phone' => '',
693 'client_address' => '',
694 'items' => array(),
695 'notes' => '',
696 'internal_notes' => '',
697 'discount' => 0,
698 'discount_type' => 'percentage',
699 'calculation_method' => 'before_tax',
700 'tax_rate' => 10,
701 'prices_include_tax' => 'no',
702 'status' => 'draft',
703 'currency' => 'USD',
704 'currency_symbol' => '$',
705 'title' => '',
706 'description' => '',
707 'terms' => $quote_terms, // Use global terms setting
708 'footer_text' => $quote_footer, // Use global footer setting
709 'accept_button' => $quote_accept_button, // Use global accept button setting
710 'accept_action' => $quote_accept_action, // Use global accept action setting
711 'accept_text' => $quote_accept_text, // Use global accept text setting
712 'accepted_message' => $quote_accepted_message, // Use global accepted message setting
713 'declined_message' => $quote_declined_message, // Use global declined message setting
714 );
715
716 // Create a temporary WP_Post object for new quote
717 $empty_post = new \WP_Post((object) array(
718 'ID' => 0,
719 'post_author' => get_current_user_id(),
720 'post_date' => current_time('mysql'),
721 'post_date_gmt' => current_time('mysql', 1),
722 'post_title' => $quote_data['number'],
723 'post_status' => 'auto-draft',
724 'comment_status' => 'closed',
725 'ping_status' => 'closed',
726 'post_name' => '',
727 'post_modified' => current_time('mysql'),
728 'post_modified_gmt' => current_time('mysql', 1),
729 'post_parent' => 0,
730 'guid' => '',
731 'menu_order' => 0,
732 'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
733 'post_mime_type' => '',
734 'comment_count' => 0,
735 'filter' => 'raw',
736 ));
737
738 $quote = new \EasyInvoice\Models\Quote($empty_post);
739
740 // Set default values on the quote object
741 foreach ($quote_data as $key => $value) {
742 $setter = 'set' . easy_invoice_str_replace('_', '', ucwords($key, '_'));
743 if (method_exists($quote, $setter)) {
744 switch ($setter) {
745 case 'setClientId':
746 $quote->setClientId((int) $value);
747 break;
748 case 'setItems':
749 $quote->setItems((array) $value);
750 break;
751 case 'setSubtotal':
752 case 'setTaxAmount':
753 case 'setDiscountAmount':
754 case 'setTotal':
755 case 'setDiscountValue':
756 case 'setTaxRate':
757 $quote->$setter((float) $value);
758 break;
759 case 'setPricesIncludeTax':
760 $quote->$setter((bool) $value);
761 break;
762 default:
763 $quote->$setter((string) $value);
764 break;
765 }
766 }
767 }
768
769 // Initialize empty items array
770 $quote->setItems([]);
771
772 // Set variables needed by the form template
773 $quote_id = 0;
774 $clients = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->all();
775 $quote_form_manager = new \EasyInvoice\Forms\Quote\QuoteFormManager();
776 $quote_items_json = json_encode([]);
777 $admin_nonce = wp_create_nonce('easy_invoice_admin_nonce');
778 $quote_field_config = $quote_form_manager->getFieldConfigForJavaScript();
779
780 // Start output buffering to capture form HTML
781 ob_start();
782
783 // Include the quote form template
784 include EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/form.php';
785
786 // Get the captured HTML
787 $html = ob_get_clean();
788
789 wp_send_json_success(['html' => $html]);
790 }
791
792 /**
793 * Handle search clients AJAX request
794 *
795 * @since 1.0.0
796 */
797 public function handleSearchClients(): void {
798 // Verify nonce
799 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_nonce')) {
800 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
801 }
802
803 // Check permissions
804 if (!current_user_can('manage_options')) {
805 wp_send_json_error(['message' => __('Insufficient permissions.', 'easy-invoice')]);
806 }
807
808 $query = sanitize_text_field($_POST['query'] ?? '');
809
810 // If query is empty, get all clients
811 if (empty($query)) {
812 $clients = $this->client_repository->all();
813 } else {
814 // Search clients by name, email, or company
815 $clients = $this->client_repository->search($query);
816 }
817
818 $results = [];
819 foreach ($clients as $client) {
820 $results[] = [
821 'id' => $client->getId(),
822 'name' => $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()),
823 'email' => $client->getEmail(),
824 'company' => $client->getBusinessClientName(),
825 'phone' => $client->getExtraInfo(),
826 'website' => $client->getWebsite(),
827 'address' => $client->getAddress()
828 ];
829 }
830
831 wp_send_json_success($results);
832 }
833
834 /**
835 * Handle AJAX request to accept a quote
836 *
837 * @since 1.0.0
838 */
839 public function handleAcceptQuote(): void {
840 // Verify nonce
841 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_quote_action')) {
842 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
843 }
844
845 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
846
847 if ($quote_id <= 0) {
848 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
849 }
850
851 // Get the quote
852 $quote = $this->quote_repository->find($quote_id);
853
854 if (!$quote) {
855 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
856 }
857
858 // Check if user has permission to accept this quote
859 $current_user = wp_get_current_user();
860 $is_admin = current_user_can('manage_options');
861
862 if (!$is_admin) {
863 // For non-admins, check if they are the client
864 if ($quote->getClientId()) {
865 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
866 $client = $client_repository->find($quote->getClientId());
867
868 if (!$client || $client->getEmail() !== $current_user->user_email) {
869 wp_send_json_error(['message' => __('You do not have permission to accept this quote.', 'easy-invoice')]);
870 }
871 } else {
872 wp_send_json_error(['message' => __('You do not have permission to accept this quote.', 'easy-invoice')]);
873 }
874 }
875
876 // Get global accept action setting
877 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
878 $accept_action = $settings_controller::getQuoteAcceptAction();
879
880 // Update quote status to accepted
881 $quote->setStatus('accepted');
882 $quote->setAcceptedDate(date('Y-m-d H:i:s'));
883 $quote->setAcceptedBy($current_user->ID);
884
885 // Save the quote
886 $saved = $quote->save();
887
888 if (!$saved) {
889 wp_send_json_error(['message' => __('Failed to accept quote.', 'easy-invoice')]);
890 }
891
892 // Log the quote acceptance
893 $this->quote_log_service->logAcceptance($quote_id, [
894 'accept_action' => $accept_action,
895 'user_type' => $is_admin ? 'admin' : 'client'
896 ]);
897
898 // Perform the configured accept action
899 $invoice_id = null;
900 $action_message = '';
901
902 switch ($accept_action) {
903 case 'convert':
904 // Convert quote to invoice (Draft status)
905 $invoice_id = $this->convertQuoteToInvoice($quote, 'draft');
906 if ($invoice_id) {
907 $this->quote_log_service->logConversionToInvoice($quote_id, $invoice_id);
908 }
909 $action_message = __('Quote converted to invoice successfully.', 'easy-invoice');
910 break;
911
912 case 'convert_available':
913 // Convert quote to invoice (Available status)
914 $invoice_id = $this->convertQuoteToInvoice($quote, 'available');
915 if ($invoice_id) {
916 $this->quote_log_service->logConversionToInvoice($quote_id, $invoice_id);
917 }
918 $action_message = __('Quote converted to invoice successfully.', 'easy-invoice');
919 break;
920
921 case 'convert_send':
922 // Convert quote to invoice and send to client (Available status)
923 $invoice_id = $this->convertQuoteToInvoice($quote, 'available');
924 if ($invoice_id) {
925 $this->sendInvoiceToClient($invoice_id);
926 }
927 $action_message = __('Quote converted to invoice and sent to client successfully.', 'easy-invoice');
928 break;
929
930 case 'duplicate':
931 // Create new invoice, keep quote as-is (Draft status)
932 $invoice_id = $this->createInvoiceFromQuote($quote, 'draft');
933 if ($invoice_id) {
934 $this->quote_log_service->logDuplicationToInvoice($quote_id, $invoice_id);
935 }
936 $action_message = __('New invoice created from quote successfully.', 'easy-invoice');
937 break;
938
939 case 'duplicate_send':
940 // Create new invoice and send to client, keep quote as-is (Available status)
941 $invoice_id = $this->createInvoiceFromQuote($quote, 'available');
942 if ($invoice_id) {
943 $this->sendInvoiceToClient($invoice_id);
944 }
945 $action_message = __('New invoice created and sent to client successfully.', 'easy-invoice');
946 break;
947
948 case 'do_nothing':
949 default:
950 // Do nothing additional
951 $action_message = __('Quote accepted successfully.', 'easy-invoice');
952 break;
953 }
954
955 // Send notification email to admin
956 if (!$is_admin) {
957 $this->sendQuoteAcceptanceNotification($quote);
958 }
959
960 // Get URLs for the new invoice
961 $invoice_url = null;
962 $secure_url = null;
963
964 if ($invoice_id) {
965 // Always use WordPress permalink
966 $invoice_url = get_permalink($invoice_id);
967 // If Pro and secure link available, use secure link
968 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
969 $secure_url = \EasyInvoicePro\Controllers\PermalinkController::getInvoiceSecureLinkUrl($invoice_id);
970 if ($secure_url) {
971 $invoice_url = $secure_url;
972 }
973 }
974 }
975
976 wp_send_json_success([
977 'message' => $action_message,
978 'invoice_id' => $invoice_id,
979 'invoice_url' => $invoice_url,
980 'secure_url' => $secure_url,
981 'toast' => [
982 'type' => 'success',
983 'message' => $action_message
984 ]
985 ]);
986 }
987
988 /**
989 * Convert quote to invoice
990 *
991 * @param \EasyInvoice\Models\Quote $quote The quote to convert
992 * @param string $status The status for the new invoice ('draft' or 'available')
993 * @return int|null The invoice ID if successful, null otherwise
994 */
995 private function convertQuoteToInvoice($quote, $status = 'draft'): ?int {
996 try {
997 // Get invoice repository
998 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
999
1000 // Create invoice data from quote - convert ALL fields
1001 $invoice_data = [
1002 'title' => $quote->getTitle() ?: 'Invoice from Quote ' . $quote->getNumber(),
1003 'number' => $this->generateInvoiceNumber(),
1004 'status' => $status,
1005 'issue_date' => date('Y-m-d'),
1006 'due_date' => date('Y-m-d', strtotime('+30 days')),
1007 'client_id' => $quote->getClientId(),
1008 'customer_name' => $quote->getCustomerName(),
1009 'customer_email' => $quote->getCustomerEmail(),
1010 'customer_address' => $quote->getCustomerAddress(),
1011 'shipping_name' => $quote->getCustomerName(), // Use customer name as shipping name
1012 'shipping_address' => $quote->getCustomerAddress(), // Use customer address as shipping address
1013 'items' => $this->convertQuoteItemsToInvoiceItems($quote->getItems()),
1014 'notes' => $quote->getNotes(),
1015 'description' => $quote->getDescription(),
1016 'terms' => $quote->getTerms(),
1017 'internal_notes' => $quote->getInternalNotes(),
1018 'payment_instructions' => '', // Invoice-specific field, leave empty
1019 'payment_gateways' => [], // Invoice-specific field, leave empty
1020 'template' => $quote->getTemplate(),
1021 'subtotal' => $quote->getSubtotal(),
1022 'tax_rate' => $quote->getTaxRate(),
1023 'tax_amount' => $quote->getTaxAmount(),
1024 'discount_type' => $quote->getDiscountType(),
1025 'discount_value' => $quote->getDiscountValue(),
1026 'discount_amount' => $quote->getDiscountAmount(),
1027 'total' => $quote->getTotal(),
1028 'currency_code' => $quote->getCurrencyCode() ?: 'USD',
1029 'currency_position' => $quote->getCurrencyPosition() ?: 'left',
1030 'footer_text' => $quote->getFooterText(),
1031 'calculation_method' => 'standard', // Default calculation method for invoices
1032 'prices_include_tax' => $quote->getPricesIncludeTax(),
1033 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
1034 ];
1035
1036 // Create the invoice
1037 $invoice = $invoice_repository->create($invoice_data);
1038
1039 if ($invoice) {
1040 // Store the quote ID in the invoice's meta for tracking
1041 update_post_meta($invoice->getId(), '_converted_from_quote', $quote->getId());
1042
1043 // Update quote to reference the created invoice
1044 $quote->setCustomField('converted_invoice_id', $invoice->getId());
1045 $quote->save();
1046
1047 // Ensure secure link is generated for the new invoice (Pro version)
1048 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
1049 // Trigger the save_post hook to generate secure link
1050 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
1051 }
1052
1053 return $invoice->getId();
1054 }
1055
1056 return null;
1057 } catch (\Exception $e) {
1058 // Error converting quote to invoice
1059 return null;
1060 }
1061 }
1062
1063 /**
1064 * Create new invoice from quote (duplicate)
1065 *
1066 * @param \EasyInvoice\Models\Quote $quote The quote to duplicate
1067 * @param string $status The status for the new invoice ('draft' or 'available')
1068 * @return int|null The invoice ID if successful, null otherwise
1069 */
1070 private function createInvoiceFromQuote($quote, $status = 'draft'): ?int {
1071 try {
1072 // Get invoice repository
1073 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
1074
1075 // Create invoice data from quote - convert ALL fields
1076 $invoice_data = [
1077 'title' => 'Invoice from Quote ' . $quote->getNumber(),
1078 'number' => $this->generateInvoiceNumber(),
1079 'status' => $status,
1080 'issue_date' => date('Y-m-d'),
1081 'due_date' => date('Y-m-d', strtotime('+30 days')),
1082 'client_id' => $quote->getClientId(),
1083 'customer_name' => $quote->getCustomerName(),
1084 'customer_email' => $quote->getCustomerEmail(),
1085 'customer_address' => $quote->getCustomerAddress(),
1086 'shipping_name' => $quote->getCustomerName(), // Use customer name as shipping name
1087 'shipping_address' => $quote->getCustomerAddress(), // Use customer address as shipping address
1088 'items' => $this->convertQuoteItemsToInvoiceItems($quote->getItems()),
1089 'notes' => $quote->getNotes(),
1090 'description' => $quote->getDescription(),
1091 'terms' => $quote->getTerms(),
1092 'internal_notes' => $quote->getInternalNotes(),
1093 'payment_instructions' => '', // Invoice-specific field, leave empty
1094 'payment_gateways' => [], // Invoice-specific field, leave empty
1095 'template' => $quote->getTemplate(),
1096 'subtotal' => $quote->getSubtotal(),
1097 'tax_rate' => $quote->getTaxRate(),
1098 'tax_amount' => $quote->getTaxAmount(),
1099 'discount_type' => $quote->getDiscountType(),
1100 'discount_value' => $quote->getDiscountValue(),
1101 'discount_amount' => $quote->getDiscountAmount(),
1102 'total' => $quote->getTotal(),
1103 'currency_code' => $quote->getCurrencyCode() ?: 'USD',
1104 'currency_position' => $quote->getCurrencyPosition() ?: 'left',
1105 'footer_text' => $quote->getFooterText(),
1106 'calculation_method' => 'standard', // Default calculation method for invoices
1107 'prices_include_tax' => $quote->getPricesIncludeTax(),
1108 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
1109 ];
1110
1111 // Create the invoice
1112 $invoice = $invoice_repository->create($invoice_data);
1113
1114 if ($invoice) {
1115 // Link the invoice to the quote
1116 $quote->setCustomField('related_invoice_id', $invoice->getId());
1117 $quote->save();
1118
1119 // Ensure secure link is generated for the new invoice (Pro version)
1120 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
1121 // Trigger the save_post hook to generate secure link
1122 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
1123 }
1124
1125 return $invoice->getId();
1126 }
1127
1128 return null;
1129 } catch (\Exception $e) {
1130 // Error creating invoice from quote
1131 return null;
1132 }
1133 }
1134
1135 /**
1136 * Send invoice to client
1137 *
1138 * @param int $invoice_id The invoice ID
1139 * @return bool True if sent successfully
1140 */
1141 private function sendInvoiceToClient(int $invoice_id): bool {
1142 try {
1143 // Get invoice
1144 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
1145 $invoice = $invoice_repository->find($invoice_id);
1146
1147 if (!$invoice) {
1148 return false;
1149 }
1150
1151 // Get email manager
1152 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1153
1154 // Send invoice email
1155 $result = $email_manager->sendInvoiceEmail($invoice, 'new');
1156
1157 return $result['success'];
1158 } catch (\Exception $e) {
1159 // Error sending invoice to client
1160 return false;
1161 }
1162 }
1163
1164 /**
1165 * Convert quote items to invoice items
1166 *
1167 * @param array $quote_items Array of quote items
1168 * @return array Array of invoice items
1169 */
1170 private function convertQuoteItemsToInvoiceItems(array $quote_items): array {
1171 $invoice_items = [];
1172
1173 foreach ($quote_items as $quote_item) {
1174 if (is_object($quote_item) && method_exists($quote_item, 'toArray')) {
1175 // Convert QuoteItem object to InvoiceItem array
1176 $item_data = $quote_item->toArray();
1177 $invoice_items[] = [
1178 'name' => $item_data['name'] ?? '',
1179 'description' => $item_data['description'] ?? '',
1180 'quantity' => $item_data['quantity'] ?? 1,
1181 'price' => $item_data['price'] ?? 0,
1182 'amount' => $item_data['amount'] ?? 0,
1183 'taxable' => $item_data['taxable'] ?? true,
1184 // Map adjust_percentage to a similar field if needed
1185 'adjust_percentage' => $item_data['adjust_percentage'] ?? 0,
1186 ];
1187 } elseif (is_array($quote_item)) {
1188 // Convert array item directly
1189 $invoice_items[] = [
1190 'name' => $quote_item['name'] ?? $quote_item['title'] ?? '',
1191 'description' => $quote_item['description'] ?? '',
1192 'quantity' => $quote_item['quantity'] ?? 1,
1193 'price' => $quote_item['price'] ?? 0,
1194 'amount' => $quote_item['amount'] ?? $quote_item['total'] ?? 0,
1195 'taxable' => $quote_item['taxable'] ?? true,
1196 'adjust_percentage' => $quote_item['adjust_percentage'] ?? 0,
1197 ];
1198 }
1199 }
1200
1201 return $invoice_items;
1202 }
1203
1204 /**
1205 * Generate unique invoice number
1206 *
1207 * @return string The invoice number
1208 */
1209 private function generateInvoiceNumber(): string {
1210 // Try to use invoice number service if available
1211 if (class_exists('\\EasyInvoice\\Services\\InvoiceNumberService')) {
1212 $invoice_number_service = new \EasyInvoice\Services\InvoiceNumberService();
1213 return $invoice_number_service->generateUniqueNumber();
1214 }
1215
1216 // Fallback to timestamp-based number
1217 return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
1218 }
1219
1220 /**
1221 * Get changes between two quote versions
1222 *
1223 * @param \EasyInvoice\Models\Quote $old_quote Old quote
1224 * @param \EasyInvoice\Models\Quote $new_quote New quote
1225 * @return array Array of changes
1226 */
1227 private function getQuoteChanges($old_quote, $new_quote): array {
1228 $changes = [];
1229
1230 // Compare key fields
1231 $fields_to_compare = [
1232 'title' => 'Title',
1233 'status' => 'Status',
1234 'customer_name' => 'Customer Name',
1235 'customer_email' => 'Customer Email',
1236 'customer_address' => 'Customer Address',
1237 'issue_date' => 'Issue Date',
1238 'expiry_date' => 'Expiry Date',
1239 'total' => 'Total Amount',
1240 'notes' => 'Notes',
1241 'terms' => 'Terms',
1242 ];
1243
1244 foreach ($fields_to_compare as $field => $label) {
1245 $method_name = 'get' . easy_invoice_str_replace('_', '', ucwords($field, '_'));
1246
1247 if (method_exists($old_quote, $method_name) && method_exists($new_quote, $method_name)) {
1248 $old_value = $old_quote->$method_name();
1249 $new_value = $new_quote->$method_name();
1250
1251 if ($old_value !== $new_value) {
1252 $changes[$field] = $new_value;
1253 }
1254 }
1255 }
1256
1257 return $changes;
1258 }
1259
1260 /**
1261 * Handle AJAX request to decline a quote
1262 *
1263 * @since 1.0.0
1264 */
1265 public function handleDeclineQuote(): void {
1266 // Verify nonce
1267 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_quote_action')) {
1268 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1269 }
1270
1271 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1272 $decline_reason = isset($_POST['decline_reason']) ? sanitize_textarea_field($_POST['decline_reason']) : '';
1273
1274 if ($quote_id <= 0) {
1275 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1276 }
1277
1278 // Get the quote
1279 $quote = $this->quote_repository->find($quote_id);
1280
1281 if (!$quote) {
1282 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1283 }
1284
1285 // Check if decline reason is required by global settings
1286 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
1287 if ($settings_controller::isDeclineReasonRequired() && empty(trim($decline_reason))) {
1288 wp_send_json_error(['message' => __('Reason for declining is required.', 'easy-invoice')]);
1289 }
1290
1291 // Check if user has permission to decline this quote
1292 $current_user = wp_get_current_user();
1293 $is_admin = current_user_can('manage_options');
1294
1295 if (!$is_admin) {
1296 // For non-admins, check if they are the client
1297 if ($quote->getClientId()) {
1298 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1299 $client = $client_repository->find($quote->getClientId());
1300
1301 if (!$client || $client->getEmail() !== $current_user->user_email) {
1302 wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]);
1303 }
1304 } else {
1305 wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]);
1306 }
1307 }
1308
1309 // Update quote status to declined
1310 $quote->setStatus('declined');
1311 $quote->setDeclinedDate(date('Y-m-d H:i:s'));
1312 $quote->setDeclinedBy($current_user->ID);
1313
1314 // Save decline reason if provided
1315 if (!empty($decline_reason)) {
1316 $quote->setDeclineReason($decline_reason);
1317 }
1318
1319 // Save the quote
1320 $saved = $quote->save();
1321
1322 if (!$saved) {
1323 wp_send_json_error(['message' => __('Failed to decline quote.', 'easy-invoice')]);
1324 }
1325
1326 // Log the quote decline
1327 $this->quote_log_service->logDecline($quote_id, $decline_reason, [
1328 'user_type' => $is_admin ? 'admin' : 'client'
1329 ]);
1330
1331 // Send notification email to admin
1332 if (!$is_admin) {
1333 $this->sendQuoteDeclineNotification($quote);
1334 }
1335
1336 wp_send_json_success([
1337 'message' => __('Quote declined successfully.', 'easy-invoice'),
1338 'toast' => [
1339 'type' => 'success',
1340 'message' => __('Quote declined successfully.', 'easy-invoice')
1341 ]
1342 ]);
1343 }
1344
1345 /**
1346 * Send quote acceptance notification to admin
1347 *
1348 * @param \EasyInvoice\Models\Quote $quote The quote that was accepted
1349 */
1350 private function sendQuoteAcceptanceNotification($quote): void {
1351 $admin_email = get_option('admin_email');
1352 $site_name = get_bloginfo('name');
1353
1354 $subject = sprintf(__('Quote %s has been accepted', 'easy-invoice'), $quote->getNumber());
1355
1356 $message = sprintf(
1357 __('Hello,
1358
1359 The quote %s for %s has been accepted by the client.
1360
1361 Quote Details:
1362 - Quote Number: %s
1363 - Client: %s
1364 - Total Amount: %s
1365 - Accepted Date: %s
1366
1367 You can view the quote at: %s
1368
1369 Best regards,
1370 %s', 'easy-invoice'),
1371 $quote->getNumber(),
1372 $quote->getCustomerName(),
1373 $quote->getNumber(),
1374 $quote->getCustomerName(),
1375 $this->formatCurrency($quote->getTotal(), $quote),
1376 date_i18n(get_option('date_format') . ' ' . get_option('time_format')),
1377 get_permalink($quote->getId()),
1378 $site_name
1379 );
1380
1381 wp_mail($admin_email, $subject, $message);
1382 }
1383
1384 /**
1385 * Send quote decline notification to admin
1386 *
1387 * @param \EasyInvoice\Models\Quote $quote The quote that was declined
1388 */
1389 private function sendQuoteDeclineNotification($quote): void {
1390 $admin_email = get_option('admin_email');
1391 $site_name = get_bloginfo('name');
1392
1393 $subject = sprintf(__('Quote %s has been declined', 'easy-invoice'), $quote->getNumber());
1394
1395 $message = sprintf(
1396 __('Hello,
1397
1398 The quote %s for %s has been declined by the client.
1399
1400 Quote Details:
1401 - Quote Number: %s
1402 - Client: %s
1403 - Total Amount: %s
1404 - Declined Date: %s
1405
1406 You can view the quote at: %s
1407
1408 Best regards,
1409 %s', 'easy-invoice'),
1410 $quote->getNumber(),
1411 $quote->getCustomerName(),
1412 $quote->getNumber(),
1413 $quote->getCustomerName(),
1414 $this->formatCurrency($quote->getTotal(), $quote),
1415 date_i18n(get_option('date_format') . ' ' . get_option('time_format')),
1416 get_permalink($quote->getId()),
1417 $site_name
1418 );
1419
1420 wp_mail($admin_email, $subject, $message);
1421 }
1422
1423 /**
1424 * Handle AJAX request to update existing quotes with missing data
1425 *
1426 * @since 1.0.0
1427 */
1428 public function handleUpdateExistingQuotes(): void {
1429 // Verify nonce - match the nonce being sent from JavaScript
1430 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1431 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1432 }
1433
1434 // Check permissions
1435 if (!current_user_can('manage_options')) {
1436 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1437 }
1438
1439 $updated_count = 0;
1440 $quotes = $this->quote_repository->findAll();
1441
1442 foreach ($quotes as $quote) {
1443 $post = get_post($quote->getId());
1444 if ($post && empty($post->post_name)) {
1445 // Generate a proper slug for this quote
1446 $post_title = $quote->getTitle() ?: $quote->getNumber() ?: 'Untitled Quote';
1447 $post_name = sanitize_title($post_title);
1448
1449 // Ensure uniqueness
1450 $original_slug = $post_name;
1451 $counter = 1;
1452 while (get_page_by_path($post_name, OBJECT, \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE)) {
1453 $post_name = $original_slug . '-' . $counter;
1454 $counter++;
1455 }
1456
1457 // Update the post with the new slug
1458 wp_update_post([
1459 'ID' => $quote->getId(),
1460 'post_name' => $post_name
1461 ]);
1462
1463 $updated_count++;
1464 }
1465 }
1466
1467 wp_send_json_success([
1468 'message' => sprintf(__('Updated %d quotes with proper URLs.', 'easy-invoice'), $updated_count)
1469 ]);
1470 }
1471
1472 /**
1473 * Handle AJAX request to duplicate a quote
1474 *
1475 * @since 1.0.0
1476 */
1477 public function handleDuplicateQuote(): void {
1478 // Verify nonce
1479 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1480 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1481 }
1482
1483 // Check permissions
1484 if (!current_user_can('manage_options')) {
1485 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1486 }
1487
1488 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1489
1490 if ($quote_id <= 0) {
1491 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1492 }
1493
1494 $quote = $this->quote_repository->find($quote_id);
1495
1496 if (!$quote) {
1497 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1498 }
1499
1500 // Get global quote settings
1501 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
1502 $quote_terms = $settings_controller::getQuoteTermsConditions();
1503 $quote_footer = $settings_controller::getQuoteFooterText();
1504 $quote_accept_button = get_option('easy_invoice_quote_accept_button', 'yes');
1505 $quote_accept_action = get_option('easy_invoice_quote_accept_action', 'email');
1506 $quote_accept_text = get_option('easy_invoice_quote_accept_text', __('Accept Quote', 'easy-invoice'));
1507 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
1508 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
1509
1510 // Create the duplicate quote
1511 $duplicate_data = [
1512 'title' => $quote->getTitle() . ' (Copy)',
1513 'status' => 'draft',
1514 'number' => $this->generateInvoiceNumber(), // Use invoice number service for consistency
1515 'issue_date' => date('Y-m-d'),
1516 'expiry_date' => date('Y-m-d', strtotime('+30 days')),
1517 'items' => $this->convertQuoteItemsToInvoiceItems($quote->getItems()), // Use invoice item conversion
1518 'notes' => $quote->getNotes(),
1519 'description' => $quote->getDescription(),
1520 'terms' => $quote_terms,
1521 'internal_notes' => $quote->getInternalNotes(),
1522 'accept_button' => $quote_accept_button,
1523 'accept_action' => $quote_accept_action,
1524 'accept_text' => $quote_accept_text,
1525 'accepted_message' => $quote_accepted_message,
1526 'declined_message' => $quote_declined_message,
1527 ];
1528
1529 // Set client ID to 0 for a new quote
1530 $duplicate_data['client_id'] = 0;
1531
1532 $duplicate_quote = $this->quote_repository->create($duplicate_data);
1533
1534 if ($duplicate_quote) {
1535 $this->quote_log_service->logActivity($quote_id, 'duplicate', 'Quote duplicated', ['duplicate_id' => $duplicate_quote->getId()]);
1536 wp_send_json_success([
1537 'message' => __('Quote duplicated successfully.', 'easy-invoice'),
1538 'quote_id' => $duplicate_quote->getId(),
1539 'toast' => [
1540 'type' => 'success',
1541 'message' => __('Quote duplicated successfully.', 'easy-invoice')
1542 ]
1543 ]);
1544 } else {
1545 wp_send_json_error(['message' => __('Failed to duplicate quote.', 'easy-invoice')]);
1546 }
1547 }
1548
1549 /**
1550 * Handle regular POST form actions for quote accept/decline
1551 *
1552 * @since 1.0.0
1553 */
1554 public function handleQuoteFormActions(): void {
1555 // Only process on POST requests
1556 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
1557 return;
1558 }
1559
1560 // Handle accept quote
1561 if (isset($_POST['accept_quote']) && isset($_POST['quote_id'])) {
1562 $this->handleAcceptQuoteForm();
1563 }
1564
1565 // Handle decline quote
1566 if (isset($_POST['decline_quote']) && isset($_POST['quote_id'])) {
1567 $this->handleDeclineQuoteForm();
1568 }
1569 }
1570
1571 /**
1572 * Handle accept quote form submission
1573 *
1574 * @since 1.0.0
1575 */
1576 private function handleAcceptQuoteForm(): void {
1577 // Verify nonce
1578 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) {
1579 wp_die(__('Security check failed.', 'easy-invoice'));
1580 }
1581
1582 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1583
1584 if ($quote_id <= 0) {
1585 wp_die(__('Invalid quote ID.', 'easy-invoice'));
1586 }
1587
1588 // Get the quote
1589 $quote = $this->quote_repository->find($quote_id);
1590
1591 if (!$quote) {
1592 wp_die(__('Quote not found.', 'easy-invoice'));
1593 }
1594
1595 // Check if user has permission to accept this quote
1596 $current_user = wp_get_current_user();
1597 $is_admin = current_user_can('manage_options');
1598
1599 if (!$is_admin) {
1600 // For non-admins, check if they are the client
1601 if ($quote->getClientId()) {
1602 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1603 $client = $client_repository->find($quote->getClientId());
1604
1605 if (!$client || $client->getEmail() !== $current_user->user_email) {
1606 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice'));
1607 }
1608 } else {
1609 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice'));
1610 }
1611 }
1612
1613 // Update quote status to accepted
1614 $quote->setStatus('accepted');
1615 $quote->setAcceptedDate(date('Y-m-d H:i:s'));
1616 $quote->setAcceptedBy($current_user->ID);
1617
1618 // Save the quote
1619 $saved = $quote->save();
1620
1621 if (!$saved) {
1622 wp_die(__('Failed to accept quote.', 'easy-invoice'));
1623 }
1624
1625 // Send notification email to admin
1626 if (!$is_admin) {
1627 $this->sendQuoteAcceptanceNotification($quote);
1628 }
1629
1630 // Redirect back to the quote page with success message
1631 $redirect_url = add_query_arg('action', 'accepted', get_permalink($quote_id));
1632 wp_redirect($redirect_url);
1633 exit;
1634 }
1635
1636 /**
1637 * Handle decline quote form submission
1638 *
1639 * @since 1.0.0
1640 */
1641 private function handleDeclineQuoteForm(): void {
1642 // Verify nonce
1643 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) {
1644 wp_die(__('Security check failed.', 'easy-invoice'));
1645 }
1646
1647 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1648
1649 if ($quote_id <= 0) {
1650 wp_die(__('Invalid quote ID.', 'easy-invoice'));
1651 }
1652
1653 // Get the quote
1654 $quote = $this->quote_repository->find($quote_id);
1655
1656 if (!$quote) {
1657 wp_die(__('Quote not found.', 'easy-invoice'));
1658 }
1659
1660 // Check if user has permission to decline this quote
1661 $current_user = wp_get_current_user();
1662 $is_admin = current_user_can('manage_options');
1663
1664 if (!$is_admin) {
1665 // For non-admins, check if they are the client
1666 if ($quote->getClientId()) {
1667 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1668 $client = $client_repository->find($quote->getClientId());
1669
1670 if (!$client || $client->getEmail() !== $current_user->user_email) {
1671 wp_die(__('You do not have permission to decline this quote.', 'easy-invoice'));
1672 }
1673 } else {
1674 wp_die(__('You do not have permission to decline this quote.', 'easy-invoice'));
1675 }
1676 }
1677
1678 // Update quote status to declined
1679 $quote->setStatus('declined');
1680 $quote->setDeclinedDate(date('Y-m-d H:i:s'));
1681 $quote->setDeclinedBy($current_user->ID);
1682
1683 // Save the quote
1684 $saved = $quote->save();
1685
1686 if (!$saved) {
1687 wp_die(__('Failed to decline quote.', 'easy-invoice'));
1688 }
1689
1690 // Send notification email to admin
1691 if (!$is_admin) {
1692 $this->sendQuoteDeclineNotification($quote);
1693 }
1694
1695 // Redirect back to the quote page with success message
1696 $redirect_url = add_query_arg('action', 'declined', get_permalink($quote_id));
1697 wp_redirect($redirect_url);
1698 exit;
1699 }
1700
1701 /**
1702 * Handle AJAX request for bulk quote actions
1703 *
1704 * @since 1.0.0
1705 */
1706 public function handleBulkQuoteAction(): void {
1707 // Verify nonce
1708 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1709 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1710 }
1711
1712 // Check permissions
1713 if (!current_user_can('manage_options')) {
1714 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1715 }
1716
1717 $quote_ids = isset($_POST['quote_ids']) ? array_map('intval', $_POST['quote_ids']) : [];
1718 $bulk_action = sanitize_text_field($_POST['bulk_action'] ?? '');
1719
1720 if (empty($quote_ids)) {
1721 wp_send_json_error(['message' => __('No quotes selected.', 'easy-invoice')]);
1722 }
1723
1724 if (empty($bulk_action)) {
1725 wp_send_json_error(['message' => __('No action selected.', 'easy-invoice')]);
1726 }
1727
1728 $success_count = 0;
1729 $error_count = 0;
1730
1731 foreach ($quote_ids as $quote_id) {
1732 $quote = $this->quote_repository->find($quote_id);
1733
1734 if (!$quote) {
1735 $error_count++;
1736 continue;
1737 }
1738
1739 try {
1740 switch ($bulk_action) {
1741 case 'delete':
1742 if ($this->quote_repository->delete($quote_id)) {
1743 $this->quote_log_service->logDeletion($quote_id);
1744 $success_count++;
1745 } else {
1746 $error_count++;
1747 }
1748 break;
1749
1750 case 'trash':
1751 $old_status = $quote->getStatus();
1752 $quote->setStatus('cancelled'); // Using cancelled as trash status
1753 if ($quote->save()) {
1754 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled');
1755 $success_count++;
1756 } else {
1757 $error_count++;
1758 }
1759 break;
1760
1761 case 'draft':
1762 $old_status = $quote->getStatus();
1763 $quote->setStatus('draft');
1764 if ($quote->save()) {
1765 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft');
1766 $success_count++;
1767 } else {
1768 $error_count++;
1769 }
1770 break;
1771
1772 case 'restore':
1773 $old_status = $quote->getStatus();
1774 $quote->setStatus('draft');
1775 if ($quote->save()) {
1776 $this->quote_log_service->logRestoration($quote_id);
1777 $success_count++;
1778 } else {
1779 $error_count++;
1780 }
1781 break;
1782
1783 default:
1784 $error_count++;
1785 break;
1786 }
1787 } catch (\Exception $e) {
1788 $error_count++;
1789 // Error in bulk action
1790 }
1791 }
1792
1793 if ($error_count > 0) {
1794 wp_send_json_success([
1795 'message' => sprintf(__('Processed %d quotes successfully. %d failed.', 'easy-invoice'), $success_count, $error_count),
1796 'toast' => [
1797 'type' => 'warning',
1798 'message' => sprintf(__('Processed %d quotes successfully. %d failed.', 'easy-invoice'), $success_count, $error_count)
1799 ]
1800 ]);
1801 } else {
1802 wp_send_json_success([
1803 'message' => sprintf(__('Successfully processed %d quotes.', 'easy-invoice'), $success_count),
1804 'toast' => [
1805 'type' => 'success',
1806 'message' => sprintf(__('Successfully processed %d quotes.', 'easy-invoice'), $success_count)
1807 ]
1808 ]);
1809 }
1810 }
1811
1812 /**
1813 * Handle AJAX request to trash a quote
1814 *
1815 * @since 1.0.0
1816 */
1817 public function handleTrashQuote(): void {
1818 // Verify nonce
1819 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1820 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1821 }
1822
1823 // Check permissions
1824 if (!current_user_can('manage_options')) {
1825 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1826 }
1827
1828 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1829
1830 if ($quote_id <= 0) {
1831 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1832 }
1833
1834 $quote = $this->quote_repository->find($quote_id);
1835
1836 if (!$quote) {
1837 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1838 }
1839
1840 // Set status to cancelled before moving to trash
1841 $old_status = $quote->getStatus();
1842 $quote->setStatus('cancelled');
1843 $quote->save();
1844
1845 // Move the post to trash status
1846 $result = wp_trash_post($quote_id);
1847
1848 if ($result) {
1849 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled');
1850 wp_send_json_success([
1851 'message' => __('Quote moved to trash successfully.', 'easy-invoice'),
1852 'toast' => [
1853 'type' => 'success',
1854 'message' => __('Quote moved to trash successfully.', 'easy-invoice')
1855 ]
1856 ]);
1857 } else {
1858 wp_send_json_error(['message' => __('Failed to move quote to trash.', 'easy-invoice')]);
1859 }
1860 }
1861
1862 /**
1863 * Handle AJAX request to move a quote to draft
1864 *
1865 * @since 1.0.0
1866 */
1867 public function handleDraftQuote(): void {
1868 // Verify nonce
1869 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1870 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1871 }
1872
1873 // Check permissions
1874 if (!current_user_can('manage_options')) {
1875 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1876 }
1877
1878 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1879
1880 if ($quote_id <= 0) {
1881 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1882 }
1883
1884 $quote = $this->quote_repository->find($quote_id);
1885
1886 if (!$quote) {
1887 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1888 }
1889
1890 // Set status to draft
1891 $old_status = $quote->getStatus();
1892 $quote->setStatus('draft');
1893
1894 if ($quote->save()) {
1895 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft');
1896 wp_send_json_success([
1897 'message' => __('Quote moved to draft successfully.', 'easy-invoice'),
1898 'toast' => [
1899 'type' => 'success',
1900 'message' => __('Quote moved to draft successfully.', 'easy-invoice')
1901 ]
1902 ]);
1903 } else {
1904 wp_send_json_error(['message' => __('Failed to move quote to draft.', 'easy-invoice')]);
1905 }
1906 }
1907
1908 /**
1909 * Handle AJAX request to restore a trashed quote
1910 *
1911 * @since 1.0.0
1912 */
1913 public function handleRestoreQuote(): void {
1914 // Verify nonce
1915 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1916 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1917 }
1918
1919 // Check permissions
1920 if (!current_user_can('manage_options')) {
1921 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1922 }
1923
1924 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1925
1926 if ($quote_id <= 0) {
1927 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1928 }
1929
1930 $quote = $this->quote_repository->find($quote_id);
1931
1932 if (!$quote) {
1933 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1934 }
1935
1936 // Restore the post from trash
1937 $result = wp_untrash_post($quote_id);
1938
1939 if ($result) {
1940 // After restoring from trash, set the meta status to available
1941 $quote->setStatus('available');
1942 $quote->save();
1943
1944 $this->quote_log_service->logRestoration($quote_id);
1945 wp_send_json_success([
1946 'message' => __('Quote restored successfully.', 'easy-invoice'),
1947 'toast' => [
1948 'type' => 'success',
1949 'message' => __('Quote restored successfully.', 'easy-invoice')
1950 ]
1951 ]);
1952 } else {
1953 wp_send_json_error(['message' => __('Failed to restore quote.', 'easy-invoice')]);
1954 }
1955 }
1956
1957 /**
1958 * Handle AJAX request to empty trash
1959 *
1960 * @since 1.0.0
1961 */
1962 public function handleEmptyTrash(): void {
1963 try {
1964 // Verify nonce
1965 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_nonce')) {
1966 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1967 }
1968
1969 // Check permissions
1970 if (!current_user_can('manage_options')) {
1971 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1972 }
1973
1974 // Get all quotes in trash (post_status = 'trash')
1975 global $wpdb;
1976 $quote_ids = $wpdb->get_col($wpdb->prepare(
1977 "SELECT ID FROM {$wpdb->posts}
1978 WHERE post_type = %s
1979 AND post_status = 'trash'",
1980 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
1981 ));
1982
1983 if (empty($quote_ids)) {
1984 wp_send_json_error(['message' => __('No quotes found in trash.', 'easy-invoice')]);
1985 }
1986
1987 $success_count = 0;
1988 $error_count = 0;
1989
1990 foreach ($quote_ids as $quote_id) {
1991 if (wp_delete_post($quote_id, true)) {
1992 $this->quote_log_service->logDeletion($quote_id);
1993 $success_count++;
1994 } else {
1995 $error_count++;
1996 }
1997 }
1998
1999 if ($error_count > 0) {
2000 wp_send_json_success([
2001 'message' => sprintf(__('Emptied trash: %d quotes deleted successfully, %d failed.', 'easy-invoice'), $success_count, $error_count),
2002 'success_count' => $success_count,
2003 'error_count' => $error_count,
2004 'toast' => [
2005 'type' => 'warning',
2006 'message' => sprintf(__('Emptied trash: %d quotes deleted successfully, %d failed.', 'easy-invoice'), $success_count, $error_count)
2007 ]
2008 ]);
2009 } else {
2010 wp_send_json_success([
2011 'message' => sprintf(__('Successfully emptied trash: %d quotes deleted.', 'easy-invoice'), $success_count),
2012 'success_count' => $success_count,
2013 'error_count' => 0,
2014 'toast' => [
2015 'type' => 'success',
2016 'message' => sprintf(__('Successfully emptied trash: %d quotes deleted.', 'easy-invoice'), $success_count)
2017 ]
2018 ]);
2019 }
2020
2021 } catch (\Exception $e) {
2022 error_log('Error emptying quote trash: ' . $e->getMessage());
2023 wp_send_json_error([
2024 'message' => __('Failed to empty trash.', 'easy-invoice'),
2025 'debug' => $e->getMessage()
2026 ]);
2027 }
2028 }
2029
2030 /**
2031 * Handle AJAX request to get quote logs
2032 *
2033 * @since 1.0.0
2034 */
2035 public function handleGetQuoteLogs(): void {
2036 // Verify nonce
2037 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
2038 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
2039 }
2040
2041 // Check permissions
2042 if (!current_user_can('manage_options')) {
2043 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
2044 }
2045
2046 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
2047
2048 if ($quote_id <= 0) {
2049 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
2050 }
2051
2052 try {
2053 $logs = $this->quote_log_service->getLogs($quote_id);
2054
2055 // Convert QuoteLog objects to arrays for JSON response
2056 $logs_data = [];
2057 foreach ($logs as $log) {
2058 $logs_data[] = [
2059 'action' => $log->getAction(),
2060 'description' => $log->getDescription(),
2061 'user_id' => $log->getUserId(),
2062 'user_name' => $log->getUserName(),
2063 'ip_address' => $log->getIpAddress(),
2064 'user_agent' => $log->getUserAgent(),
2065 'additional_data' => $log->getAdditionalData(),
2066 'created_date' => $log->getCreatedDate(),
2067 ];
2068 }
2069
2070 wp_send_json_success([
2071 'logs' => $logs_data,
2072 'count' => count($logs_data)
2073 ]);
2074
2075 } catch (\Exception $e) {
2076 wp_send_json_error([
2077 'message' => __('Error retrieving quote logs.', 'easy-invoice'),
2078 'debug' => $e->getMessage()
2079 ]);
2080 }
2081 }
2082
2083 /**
2084 * Format currency amount using QuoteFormatter
2085 *
2086 * @param float $amount The amount to format
2087 * @param \EasyInvoice\Models\Quote|null $quote The quote object for currency settings
2088 * @return string Formatted currency string
2089 */
2090 private function formatCurrency(float $amount, $quote = null): string {
2091 $formatter = new \EasyInvoice\Helpers\QuoteFormatter($quote);
2092 return $formatter->format($amount);
2093 }
2094 }