PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.0
2.4.3 2.4.4 2.4.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 All 60 releases
easy-invoice / includes / Controllers / QuoteController.php

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

2,097 lines 80.9 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 // Get regular invoice URL
966 $invoice_url = get_permalink($invoice_id);
967
968 // If permalink is not available, construct a fallback URL
969 if (!$invoice_url || $invoice_url === get_permalink(0)) {
970 $invoice_url = home_url('/invoice/' . $invoice_id . '/');
971 }
972
973 // Get secure URL for the new invoice if it exists (Pro version)
974 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
975 $secure_url = \EasyInvoicePro\Controllers\PermalinkController::getInvoiceSecureLinkUrl($invoice_id);
976 }
977 }
978
979 wp_send_json_success([
980 'message' => $action_message,
981 'invoice_id' => $invoice_id,
982 'invoice_url' => $invoice_url,
983 'secure_url' => $secure_url,
984 'toast' => [
985 'type' => 'success',
986 'message' => $action_message
987 ]
988 ]);
989 }
990
991 /**
992 * Convert quote to invoice
993 *
994 * @param \EasyInvoice\Models\Quote $quote The quote to convert
995 * @param string $status The status for the new invoice ('draft' or 'available')
996 * @return int|null The invoice ID if successful, null otherwise
997 */
998 private function convertQuoteToInvoice($quote, $status = 'draft'): ?int {
999 try {
1000 // Get invoice repository
1001 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
1002
1003 // Create invoice data from quote - convert ALL fields
1004 $invoice_data = [
1005 'title' => $quote->getTitle() ?: 'Invoice from Quote ' . $quote->getNumber(),
1006 'number' => $this->generateInvoiceNumber(),
1007 'status' => $status,
1008 'issue_date' => date('Y-m-d'),
1009 'due_date' => date('Y-m-d', strtotime('+30 days')),
1010 'client_id' => $quote->getClientId(),
1011 'customer_name' => $quote->getCustomerName(),
1012 'customer_email' => $quote->getCustomerEmail(),
1013 'customer_address' => $quote->getCustomerAddress(),
1014 'shipping_name' => $quote->getCustomerName(), // Use customer name as shipping name
1015 'shipping_address' => $quote->getCustomerAddress(), // Use customer address as shipping address
1016 'items' => $this->convertQuoteItemsToInvoiceItems($quote->getItems()),
1017 'notes' => $quote->getNotes(),
1018 'description' => $quote->getDescription(),
1019 'terms' => $quote->getTerms(),
1020 'internal_notes' => $quote->getInternalNotes(),
1021 'payment_instructions' => '', // Invoice-specific field, leave empty
1022 'payment_gateways' => [], // Invoice-specific field, leave empty
1023 'template' => $quote->getTemplate(),
1024 'subtotal' => $quote->getSubtotal(),
1025 'tax_rate' => $quote->getTaxRate(),
1026 'tax_amount' => $quote->getTaxAmount(),
1027 'discount_type' => $quote->getDiscountType(),
1028 'discount_value' => $quote->getDiscountValue(),
1029 'discount_amount' => $quote->getDiscountAmount(),
1030 'total' => $quote->getTotal(),
1031 'currency_code' => $quote->getCurrencyCode() ?: 'USD',
1032 'currency_position' => $quote->getCurrencyPosition() ?: 'left',
1033 'footer_text' => $quote->getFooterText(),
1034 'calculation_method' => 'standard', // Default calculation method for invoices
1035 'prices_include_tax' => $quote->getPricesIncludeTax(),
1036 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
1037 ];
1038
1039 // Create the invoice
1040 $invoice = $invoice_repository->create($invoice_data);
1041
1042 if ($invoice) {
1043 // Store the quote ID in the invoice's meta for tracking
1044 update_post_meta($invoice->getId(), '_converted_from_quote', $quote->getId());
1045
1046 // Update quote to reference the created invoice
1047 $quote->setCustomField('converted_invoice_id', $invoice->getId());
1048 $quote->save();
1049
1050 // Ensure secure link is generated for the new invoice (Pro version)
1051 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
1052 // Trigger the save_post hook to generate secure link
1053 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
1054 }
1055
1056 return $invoice->getId();
1057 }
1058
1059 return null;
1060 } catch (\Exception $e) {
1061 // Error converting quote to invoice
1062 return null;
1063 }
1064 }
1065
1066 /**
1067 * Create new invoice from quote (duplicate)
1068 *
1069 * @param \EasyInvoice\Models\Quote $quote The quote to duplicate
1070 * @param string $status The status for the new invoice ('draft' or 'available')
1071 * @return int|null The invoice ID if successful, null otherwise
1072 */
1073 private function createInvoiceFromQuote($quote, $status = 'draft'): ?int {
1074 try {
1075 // Get invoice repository
1076 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
1077
1078 // Create invoice data from quote - convert ALL fields
1079 $invoice_data = [
1080 'title' => 'Invoice from Quote ' . $quote->getNumber(),
1081 'number' => $this->generateInvoiceNumber(),
1082 'status' => $status,
1083 'issue_date' => date('Y-m-d'),
1084 'due_date' => date('Y-m-d', strtotime('+30 days')),
1085 'client_id' => $quote->getClientId(),
1086 'customer_name' => $quote->getCustomerName(),
1087 'customer_email' => $quote->getCustomerEmail(),
1088 'customer_address' => $quote->getCustomerAddress(),
1089 'shipping_name' => $quote->getCustomerName(), // Use customer name as shipping name
1090 'shipping_address' => $quote->getCustomerAddress(), // Use customer address as shipping address
1091 'items' => $this->convertQuoteItemsToInvoiceItems($quote->getItems()),
1092 'notes' => $quote->getNotes(),
1093 'description' => $quote->getDescription(),
1094 'terms' => $quote->getTerms(),
1095 'internal_notes' => $quote->getInternalNotes(),
1096 'payment_instructions' => '', // Invoice-specific field, leave empty
1097 'payment_gateways' => [], // Invoice-specific field, leave empty
1098 'template' => $quote->getTemplate(),
1099 'subtotal' => $quote->getSubtotal(),
1100 'tax_rate' => $quote->getTaxRate(),
1101 'tax_amount' => $quote->getTaxAmount(),
1102 'discount_type' => $quote->getDiscountType(),
1103 'discount_value' => $quote->getDiscountValue(),
1104 'discount_amount' => $quote->getDiscountAmount(),
1105 'total' => $quote->getTotal(),
1106 'currency_code' => $quote->getCurrencyCode() ?: 'USD',
1107 'currency_position' => $quote->getCurrencyPosition() ?: 'left',
1108 'footer_text' => $quote->getFooterText(),
1109 'calculation_method' => 'standard', // Default calculation method for invoices
1110 'prices_include_tax' => $quote->getPricesIncludeTax(),
1111 'custom_fields' => $quote->getCustomFields(), // Transfer custom fields
1112 ];
1113
1114 // Create the invoice
1115 $invoice = $invoice_repository->create($invoice_data);
1116
1117 if ($invoice) {
1118 // Link the invoice to the quote
1119 $quote->setCustomField('related_invoice_id', $invoice->getId());
1120 $quote->save();
1121
1122 // Ensure secure link is generated for the new invoice (Pro version)
1123 if (class_exists('\EasyInvoicePro\Controllers\PermalinkController')) {
1124 // Trigger the save_post hook to generate secure link
1125 do_action('save_post_easy_invoice', $invoice->getId(), get_post($invoice->getId()));
1126 }
1127
1128 return $invoice->getId();
1129 }
1130
1131 return null;
1132 } catch (\Exception $e) {
1133 // Error creating invoice from quote
1134 return null;
1135 }
1136 }
1137
1138 /**
1139 * Send invoice to client
1140 *
1141 * @param int $invoice_id The invoice ID
1142 * @return bool True if sent successfully
1143 */
1144 private function sendInvoiceToClient(int $invoice_id): bool {
1145 try {
1146 // Get invoice
1147 $invoice_repository = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository();
1148 $invoice = $invoice_repository->find($invoice_id);
1149
1150 if (!$invoice) {
1151 return false;
1152 }
1153
1154 // Get email manager
1155 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1156
1157 // Send invoice email
1158 $result = $email_manager->sendInvoiceEmail($invoice, 'new');
1159
1160 return $result['success'];
1161 } catch (\Exception $e) {
1162 // Error sending invoice to client
1163 return false;
1164 }
1165 }
1166
1167 /**
1168 * Convert quote items to invoice items
1169 *
1170 * @param array $quote_items Array of quote items
1171 * @return array Array of invoice items
1172 */
1173 private function convertQuoteItemsToInvoiceItems(array $quote_items): array {
1174 $invoice_items = [];
1175
1176 foreach ($quote_items as $quote_item) {
1177 if (is_object($quote_item) && method_exists($quote_item, 'toArray')) {
1178 // Convert QuoteItem object to InvoiceItem array
1179 $item_data = $quote_item->toArray();
1180 $invoice_items[] = [
1181 'name' => $item_data['name'] ?? '',
1182 'description' => $item_data['description'] ?? '',
1183 'quantity' => $item_data['quantity'] ?? 1,
1184 'price' => $item_data['price'] ?? 0,
1185 'amount' => $item_data['amount'] ?? 0,
1186 'taxable' => $item_data['taxable'] ?? true,
1187 // Map adjust_percentage to a similar field if needed
1188 'adjust_percentage' => $item_data['adjust_percentage'] ?? 0,
1189 ];
1190 } elseif (is_array($quote_item)) {
1191 // Convert array item directly
1192 $invoice_items[] = [
1193 'name' => $quote_item['name'] ?? $quote_item['title'] ?? '',
1194 'description' => $quote_item['description'] ?? '',
1195 'quantity' => $quote_item['quantity'] ?? 1,
1196 'price' => $quote_item['price'] ?? 0,
1197 'amount' => $quote_item['amount'] ?? $quote_item['total'] ?? 0,
1198 'taxable' => $quote_item['taxable'] ?? true,
1199 'adjust_percentage' => $quote_item['adjust_percentage'] ?? 0,
1200 ];
1201 }
1202 }
1203
1204 return $invoice_items;
1205 }
1206
1207 /**
1208 * Generate unique invoice number
1209 *
1210 * @return string The invoice number
1211 */
1212 private function generateInvoiceNumber(): string {
1213 // Try to use invoice number service if available
1214 if (class_exists('\\EasyInvoice\\Services\\InvoiceNumberService')) {
1215 $invoice_number_service = new \EasyInvoice\Services\InvoiceNumberService();
1216 return $invoice_number_service->generateUniqueNumber();
1217 }
1218
1219 // Fallback to timestamp-based number
1220 return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT);
1221 }
1222
1223 /**
1224 * Get changes between two quote versions
1225 *
1226 * @param \EasyInvoice\Models\Quote $old_quote Old quote
1227 * @param \EasyInvoice\Models\Quote $new_quote New quote
1228 * @return array Array of changes
1229 */
1230 private function getQuoteChanges($old_quote, $new_quote): array {
1231 $changes = [];
1232
1233 // Compare key fields
1234 $fields_to_compare = [
1235 'title' => 'Title',
1236 'status' => 'Status',
1237 'customer_name' => 'Customer Name',
1238 'customer_email' => 'Customer Email',
1239 'customer_address' => 'Customer Address',
1240 'issue_date' => 'Issue Date',
1241 'expiry_date' => 'Expiry Date',
1242 'total' => 'Total Amount',
1243 'notes' => 'Notes',
1244 'terms' => 'Terms',
1245 ];
1246
1247 foreach ($fields_to_compare as $field => $label) {
1248 $method_name = 'get' . easy_invoice_str_replace('_', '', ucwords($field, '_'));
1249
1250 if (method_exists($old_quote, $method_name) && method_exists($new_quote, $method_name)) {
1251 $old_value = $old_quote->$method_name();
1252 $new_value = $new_quote->$method_name();
1253
1254 if ($old_value !== $new_value) {
1255 $changes[$field] = $new_value;
1256 }
1257 }
1258 }
1259
1260 return $changes;
1261 }
1262
1263 /**
1264 * Handle AJAX request to decline a quote
1265 *
1266 * @since 1.0.0
1267 */
1268 public function handleDeclineQuote(): void {
1269 // Verify nonce
1270 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_quote_action')) {
1271 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1272 }
1273
1274 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1275 $decline_reason = isset($_POST['decline_reason']) ? sanitize_textarea_field($_POST['decline_reason']) : '';
1276
1277 if ($quote_id <= 0) {
1278 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1279 }
1280
1281 // Get the quote
1282 $quote = $this->quote_repository->find($quote_id);
1283
1284 if (!$quote) {
1285 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1286 }
1287
1288 // Check if decline reason is required by global settings
1289 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
1290 if ($settings_controller::isDeclineReasonRequired() && empty(trim($decline_reason))) {
1291 wp_send_json_error(['message' => __('Reason for declining is required.', 'easy-invoice')]);
1292 }
1293
1294 // Check if user has permission to decline this quote
1295 $current_user = wp_get_current_user();
1296 $is_admin = current_user_can('manage_options');
1297
1298 if (!$is_admin) {
1299 // For non-admins, check if they are the client
1300 if ($quote->getClientId()) {
1301 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1302 $client = $client_repository->find($quote->getClientId());
1303
1304 if (!$client || $client->getEmail() !== $current_user->user_email) {
1305 wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]);
1306 }
1307 } else {
1308 wp_send_json_error(['message' => __('You do not have permission to decline this quote.', 'easy-invoice')]);
1309 }
1310 }
1311
1312 // Update quote status to declined
1313 $quote->setStatus('declined');
1314 $quote->setDeclinedDate(date('Y-m-d H:i:s'));
1315 $quote->setDeclinedBy($current_user->ID);
1316
1317 // Save decline reason if provided
1318 if (!empty($decline_reason)) {
1319 $quote->setDeclineReason($decline_reason);
1320 }
1321
1322 // Save the quote
1323 $saved = $quote->save();
1324
1325 if (!$saved) {
1326 wp_send_json_error(['message' => __('Failed to decline quote.', 'easy-invoice')]);
1327 }
1328
1329 // Log the quote decline
1330 $this->quote_log_service->logDecline($quote_id, $decline_reason, [
1331 'user_type' => $is_admin ? 'admin' : 'client'
1332 ]);
1333
1334 // Send notification email to admin
1335 if (!$is_admin) {
1336 $this->sendQuoteDeclineNotification($quote);
1337 }
1338
1339 wp_send_json_success([
1340 'message' => __('Quote declined successfully.', 'easy-invoice'),
1341 'toast' => [
1342 'type' => 'success',
1343 'message' => __('Quote declined successfully.', 'easy-invoice')
1344 ]
1345 ]);
1346 }
1347
1348 /**
1349 * Send quote acceptance notification to admin
1350 *
1351 * @param \EasyInvoice\Models\Quote $quote The quote that was accepted
1352 */
1353 private function sendQuoteAcceptanceNotification($quote): void {
1354 $admin_email = get_option('admin_email');
1355 $site_name = get_bloginfo('name');
1356
1357 $subject = sprintf(__('Quote %s has been accepted', 'easy-invoice'), $quote->getNumber());
1358
1359 $message = sprintf(
1360 __('Hello,
1361
1362 The quote %s for %s has been accepted by the client.
1363
1364 Quote Details:
1365 - Quote Number: %s
1366 - Client: %s
1367 - Total Amount: %s
1368 - Accepted Date: %s
1369
1370 You can view the quote at: %s
1371
1372 Best regards,
1373 %s', 'easy-invoice'),
1374 $quote->getNumber(),
1375 $quote->getCustomerName(),
1376 $quote->getNumber(),
1377 $quote->getCustomerName(),
1378 $this->formatCurrency($quote->getTotal(), $quote),
1379 date_i18n(get_option('date_format') . ' ' . get_option('time_format')),
1380 get_permalink($quote->getId()),
1381 $site_name
1382 );
1383
1384 wp_mail($admin_email, $subject, $message);
1385 }
1386
1387 /**
1388 * Send quote decline notification to admin
1389 *
1390 * @param \EasyInvoice\Models\Quote $quote The quote that was declined
1391 */
1392 private function sendQuoteDeclineNotification($quote): void {
1393 $admin_email = get_option('admin_email');
1394 $site_name = get_bloginfo('name');
1395
1396 $subject = sprintf(__('Quote %s has been declined', 'easy-invoice'), $quote->getNumber());
1397
1398 $message = sprintf(
1399 __('Hello,
1400
1401 The quote %s for %s has been declined by the client.
1402
1403 Quote Details:
1404 - Quote Number: %s
1405 - Client: %s
1406 - Total Amount: %s
1407 - Declined Date: %s
1408
1409 You can view the quote at: %s
1410
1411 Best regards,
1412 %s', 'easy-invoice'),
1413 $quote->getNumber(),
1414 $quote->getCustomerName(),
1415 $quote->getNumber(),
1416 $quote->getCustomerName(),
1417 $this->formatCurrency($quote->getTotal(), $quote),
1418 date_i18n(get_option('date_format') . ' ' . get_option('time_format')),
1419 get_permalink($quote->getId()),
1420 $site_name
1421 );
1422
1423 wp_mail($admin_email, $subject, $message);
1424 }
1425
1426 /**
1427 * Handle AJAX request to update existing quotes with missing data
1428 *
1429 * @since 1.0.0
1430 */
1431 public function handleUpdateExistingQuotes(): void {
1432 // Verify nonce - match the nonce being sent from JavaScript
1433 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1434 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1435 }
1436
1437 // Check permissions
1438 if (!current_user_can('manage_options')) {
1439 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1440 }
1441
1442 $updated_count = 0;
1443 $quotes = $this->quote_repository->findAll();
1444
1445 foreach ($quotes as $quote) {
1446 $post = get_post($quote->getId());
1447 if ($post && empty($post->post_name)) {
1448 // Generate a proper slug for this quote
1449 $post_title = $quote->getTitle() ?: $quote->getNumber() ?: 'Untitled Quote';
1450 $post_name = sanitize_title($post_title);
1451
1452 // Ensure uniqueness
1453 $original_slug = $post_name;
1454 $counter = 1;
1455 while (get_page_by_path($post_name, OBJECT, \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE)) {
1456 $post_name = $original_slug . '-' . $counter;
1457 $counter++;
1458 }
1459
1460 // Update the post with the new slug
1461 wp_update_post([
1462 'ID' => $quote->getId(),
1463 'post_name' => $post_name
1464 ]);
1465
1466 $updated_count++;
1467 }
1468 }
1469
1470 wp_send_json_success([
1471 'message' => sprintf(__('Updated %d quotes with proper URLs.', 'easy-invoice'), $updated_count)
1472 ]);
1473 }
1474
1475 /**
1476 * Handle AJAX request to duplicate a quote
1477 *
1478 * @since 1.0.0
1479 */
1480 public function handleDuplicateQuote(): void {
1481 // Verify nonce
1482 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1483 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1484 }
1485
1486 // Check permissions
1487 if (!current_user_can('manage_options')) {
1488 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1489 }
1490
1491 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1492
1493 if ($quote_id <= 0) {
1494 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1495 }
1496
1497 $quote = $this->quote_repository->find($quote_id);
1498
1499 if (!$quote) {
1500 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1501 }
1502
1503 // Get global quote settings
1504 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
1505 $quote_terms = $settings_controller::getQuoteTermsConditions();
1506 $quote_footer = $settings_controller::getQuoteFooterText();
1507 $quote_accept_button = get_option('easy_invoice_quote_accept_button', 'yes');
1508 $quote_accept_action = get_option('easy_invoice_quote_accept_action', 'email');
1509 $quote_accept_text = get_option('easy_invoice_quote_accept_text', __('Accept Quote', 'easy-invoice'));
1510 $quote_accepted_message = get_option('easy_invoice_quote_accepted_message', __('Thank you for accepting our quote!', 'easy-invoice'));
1511 $quote_declined_message = get_option('easy_invoice_quote_declined_message', __('Thank you for your consideration.', 'easy-invoice'));
1512
1513 // Create the duplicate quote
1514 $duplicate_data = [
1515 'title' => $quote->getTitle() . ' (Copy)',
1516 'status' => 'draft',
1517 'number' => $this->generateInvoiceNumber(), // Use invoice number service for consistency
1518 'issue_date' => date('Y-m-d'),
1519 'expiry_date' => date('Y-m-d', strtotime('+30 days')),
1520 'items' => $this->convertQuoteItemsToInvoiceItems($quote->getItems()), // Use invoice item conversion
1521 'notes' => $quote->getNotes(),
1522 'description' => $quote->getDescription(),
1523 'terms' => $quote_terms,
1524 'internal_notes' => $quote->getInternalNotes(),
1525 'accept_button' => $quote_accept_button,
1526 'accept_action' => $quote_accept_action,
1527 'accept_text' => $quote_accept_text,
1528 'accepted_message' => $quote_accepted_message,
1529 'declined_message' => $quote_declined_message,
1530 ];
1531
1532 // Set client ID to 0 for a new quote
1533 $duplicate_data['client_id'] = 0;
1534
1535 $duplicate_quote = $this->quote_repository->create($duplicate_data);
1536
1537 if ($duplicate_quote) {
1538 $this->quote_log_service->logActivity($quote_id, 'duplicate', 'Quote duplicated', ['duplicate_id' => $duplicate_quote->getId()]);
1539 wp_send_json_success([
1540 'message' => __('Quote duplicated successfully.', 'easy-invoice'),
1541 'quote_id' => $duplicate_quote->getId(),
1542 'toast' => [
1543 'type' => 'success',
1544 'message' => __('Quote duplicated successfully.', 'easy-invoice')
1545 ]
1546 ]);
1547 } else {
1548 wp_send_json_error(['message' => __('Failed to duplicate quote.', 'easy-invoice')]);
1549 }
1550 }
1551
1552 /**
1553 * Handle regular POST form actions for quote accept/decline
1554 *
1555 * @since 1.0.0
1556 */
1557 public function handleQuoteFormActions(): void {
1558 // Only process on POST requests
1559 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
1560 return;
1561 }
1562
1563 // Handle accept quote
1564 if (isset($_POST['accept_quote']) && isset($_POST['quote_id'])) {
1565 $this->handleAcceptQuoteForm();
1566 }
1567
1568 // Handle decline quote
1569 if (isset($_POST['decline_quote']) && isset($_POST['quote_id'])) {
1570 $this->handleDeclineQuoteForm();
1571 }
1572 }
1573
1574 /**
1575 * Handle accept quote form submission
1576 *
1577 * @since 1.0.0
1578 */
1579 private function handleAcceptQuoteForm(): void {
1580 // Verify nonce
1581 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) {
1582 wp_die(__('Security check failed.', 'easy-invoice'));
1583 }
1584
1585 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1586
1587 if ($quote_id <= 0) {
1588 wp_die(__('Invalid quote ID.', 'easy-invoice'));
1589 }
1590
1591 // Get the quote
1592 $quote = $this->quote_repository->find($quote_id);
1593
1594 if (!$quote) {
1595 wp_die(__('Quote not found.', 'easy-invoice'));
1596 }
1597
1598 // Check if user has permission to accept this quote
1599 $current_user = wp_get_current_user();
1600 $is_admin = current_user_can('manage_options');
1601
1602 if (!$is_admin) {
1603 // For non-admins, check if they are the client
1604 if ($quote->getClientId()) {
1605 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1606 $client = $client_repository->find($quote->getClientId());
1607
1608 if (!$client || $client->getEmail() !== $current_user->user_email) {
1609 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice'));
1610 }
1611 } else {
1612 wp_die(__('You do not have permission to accept this quote.', 'easy-invoice'));
1613 }
1614 }
1615
1616 // Update quote status to accepted
1617 $quote->setStatus('accepted');
1618 $quote->setAcceptedDate(date('Y-m-d H:i:s'));
1619 $quote->setAcceptedBy($current_user->ID);
1620
1621 // Save the quote
1622 $saved = $quote->save();
1623
1624 if (!$saved) {
1625 wp_die(__('Failed to accept quote.', 'easy-invoice'));
1626 }
1627
1628 // Send notification email to admin
1629 if (!$is_admin) {
1630 $this->sendQuoteAcceptanceNotification($quote);
1631 }
1632
1633 // Redirect back to the quote page with success message
1634 $redirect_url = add_query_arg('action', 'accepted', get_permalink($quote_id));
1635 wp_redirect($redirect_url);
1636 exit;
1637 }
1638
1639 /**
1640 * Handle decline quote form submission
1641 *
1642 * @since 1.0.0
1643 */
1644 private function handleDeclineQuoteForm(): void {
1645 // Verify nonce
1646 if (!wp_verify_nonce($_POST['quote_nonce'] ?? '', 'easy_invoice_quote_action')) {
1647 wp_die(__('Security check failed.', 'easy-invoice'));
1648 }
1649
1650 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1651
1652 if ($quote_id <= 0) {
1653 wp_die(__('Invalid quote ID.', 'easy-invoice'));
1654 }
1655
1656 // Get the quote
1657 $quote = $this->quote_repository->find($quote_id);
1658
1659 if (!$quote) {
1660 wp_die(__('Quote not found.', 'easy-invoice'));
1661 }
1662
1663 // Check if user has permission to decline this quote
1664 $current_user = wp_get_current_user();
1665 $is_admin = current_user_can('manage_options');
1666
1667 if (!$is_admin) {
1668 // For non-admins, check if they are the client
1669 if ($quote->getClientId()) {
1670 $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
1671 $client = $client_repository->find($quote->getClientId());
1672
1673 if (!$client || $client->getEmail() !== $current_user->user_email) {
1674 wp_die(__('You do not have permission to decline this quote.', 'easy-invoice'));
1675 }
1676 } else {
1677 wp_die(__('You do not have permission to decline this quote.', 'easy-invoice'));
1678 }
1679 }
1680
1681 // Update quote status to declined
1682 $quote->setStatus('declined');
1683 $quote->setDeclinedDate(date('Y-m-d H:i:s'));
1684 $quote->setDeclinedBy($current_user->ID);
1685
1686 // Save the quote
1687 $saved = $quote->save();
1688
1689 if (!$saved) {
1690 wp_die(__('Failed to decline quote.', 'easy-invoice'));
1691 }
1692
1693 // Send notification email to admin
1694 if (!$is_admin) {
1695 $this->sendQuoteDeclineNotification($quote);
1696 }
1697
1698 // Redirect back to the quote page with success message
1699 $redirect_url = add_query_arg('action', 'declined', get_permalink($quote_id));
1700 wp_redirect($redirect_url);
1701 exit;
1702 }
1703
1704 /**
1705 * Handle AJAX request for bulk quote actions
1706 *
1707 * @since 1.0.0
1708 */
1709 public function handleBulkQuoteAction(): void {
1710 // Verify nonce
1711 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1712 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1713 }
1714
1715 // Check permissions
1716 if (!current_user_can('manage_options')) {
1717 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1718 }
1719
1720 $quote_ids = isset($_POST['quote_ids']) ? array_map('intval', $_POST['quote_ids']) : [];
1721 $bulk_action = sanitize_text_field($_POST['bulk_action'] ?? '');
1722
1723 if (empty($quote_ids)) {
1724 wp_send_json_error(['message' => __('No quotes selected.', 'easy-invoice')]);
1725 }
1726
1727 if (empty($bulk_action)) {
1728 wp_send_json_error(['message' => __('No action selected.', 'easy-invoice')]);
1729 }
1730
1731 $success_count = 0;
1732 $error_count = 0;
1733
1734 foreach ($quote_ids as $quote_id) {
1735 $quote = $this->quote_repository->find($quote_id);
1736
1737 if (!$quote) {
1738 $error_count++;
1739 continue;
1740 }
1741
1742 try {
1743 switch ($bulk_action) {
1744 case 'delete':
1745 if ($this->quote_repository->delete($quote_id)) {
1746 $this->quote_log_service->logDeletion($quote_id);
1747 $success_count++;
1748 } else {
1749 $error_count++;
1750 }
1751 break;
1752
1753 case 'trash':
1754 $old_status = $quote->getStatus();
1755 $quote->setStatus('cancelled'); // Using cancelled as trash status
1756 if ($quote->save()) {
1757 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled');
1758 $success_count++;
1759 } else {
1760 $error_count++;
1761 }
1762 break;
1763
1764 case 'draft':
1765 $old_status = $quote->getStatus();
1766 $quote->setStatus('draft');
1767 if ($quote->save()) {
1768 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft');
1769 $success_count++;
1770 } else {
1771 $error_count++;
1772 }
1773 break;
1774
1775 case 'restore':
1776 $old_status = $quote->getStatus();
1777 $quote->setStatus('draft');
1778 if ($quote->save()) {
1779 $this->quote_log_service->logRestoration($quote_id);
1780 $success_count++;
1781 } else {
1782 $error_count++;
1783 }
1784 break;
1785
1786 default:
1787 $error_count++;
1788 break;
1789 }
1790 } catch (\Exception $e) {
1791 $error_count++;
1792 // Error in bulk action
1793 }
1794 }
1795
1796 if ($error_count > 0) {
1797 wp_send_json_success([
1798 'message' => sprintf(__('Processed %d quotes successfully. %d failed.', 'easy-invoice'), $success_count, $error_count),
1799 'toast' => [
1800 'type' => 'warning',
1801 'message' => sprintf(__('Processed %d quotes successfully. %d failed.', 'easy-invoice'), $success_count, $error_count)
1802 ]
1803 ]);
1804 } else {
1805 wp_send_json_success([
1806 'message' => sprintf(__('Successfully processed %d quotes.', 'easy-invoice'), $success_count),
1807 'toast' => [
1808 'type' => 'success',
1809 'message' => sprintf(__('Successfully processed %d quotes.', 'easy-invoice'), $success_count)
1810 ]
1811 ]);
1812 }
1813 }
1814
1815 /**
1816 * Handle AJAX request to trash a quote
1817 *
1818 * @since 1.0.0
1819 */
1820 public function handleTrashQuote(): void {
1821 // Verify nonce
1822 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1823 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1824 }
1825
1826 // Check permissions
1827 if (!current_user_can('manage_options')) {
1828 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1829 }
1830
1831 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1832
1833 if ($quote_id <= 0) {
1834 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1835 }
1836
1837 $quote = $this->quote_repository->find($quote_id);
1838
1839 if (!$quote) {
1840 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1841 }
1842
1843 // Set status to cancelled before moving to trash
1844 $old_status = $quote->getStatus();
1845 $quote->setStatus('cancelled');
1846 $quote->save();
1847
1848 // Move the post to trash status
1849 $result = wp_trash_post($quote_id);
1850
1851 if ($result) {
1852 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'cancelled');
1853 wp_send_json_success([
1854 'message' => __('Quote moved to trash successfully.', 'easy-invoice'),
1855 'toast' => [
1856 'type' => 'success',
1857 'message' => __('Quote moved to trash successfully.', 'easy-invoice')
1858 ]
1859 ]);
1860 } else {
1861 wp_send_json_error(['message' => __('Failed to move quote to trash.', 'easy-invoice')]);
1862 }
1863 }
1864
1865 /**
1866 * Handle AJAX request to move a quote to draft
1867 *
1868 * @since 1.0.0
1869 */
1870 public function handleDraftQuote(): void {
1871 // Verify nonce
1872 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1873 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1874 }
1875
1876 // Check permissions
1877 if (!current_user_can('manage_options')) {
1878 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1879 }
1880
1881 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1882
1883 if ($quote_id <= 0) {
1884 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1885 }
1886
1887 $quote = $this->quote_repository->find($quote_id);
1888
1889 if (!$quote) {
1890 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1891 }
1892
1893 // Set status to draft
1894 $old_status = $quote->getStatus();
1895 $quote->setStatus('draft');
1896
1897 if ($quote->save()) {
1898 $this->quote_log_service->logStatusChange($quote_id, $old_status, 'draft');
1899 wp_send_json_success([
1900 'message' => __('Quote moved to draft successfully.', 'easy-invoice'),
1901 'toast' => [
1902 'type' => 'success',
1903 'message' => __('Quote moved to draft successfully.', 'easy-invoice')
1904 ]
1905 ]);
1906 } else {
1907 wp_send_json_error(['message' => __('Failed to move quote to draft.', 'easy-invoice')]);
1908 }
1909 }
1910
1911 /**
1912 * Handle AJAX request to restore a trashed quote
1913 *
1914 * @since 1.0.0
1915 */
1916 public function handleRestoreQuote(): void {
1917 // Verify nonce
1918 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
1919 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1920 }
1921
1922 // Check permissions
1923 if (!current_user_can('manage_options')) {
1924 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1925 }
1926
1927 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
1928
1929 if ($quote_id <= 0) {
1930 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
1931 }
1932
1933 $quote = $this->quote_repository->find($quote_id);
1934
1935 if (!$quote) {
1936 wp_send_json_error(['message' => __('Quote not found.', 'easy-invoice')]);
1937 }
1938
1939 // Restore the post from trash
1940 $result = wp_untrash_post($quote_id);
1941
1942 if ($result) {
1943 // After restoring from trash, set the meta status to available
1944 $quote->setStatus('available');
1945 $quote->save();
1946
1947 $this->quote_log_service->logRestoration($quote_id);
1948 wp_send_json_success([
1949 'message' => __('Quote restored successfully.', 'easy-invoice'),
1950 'toast' => [
1951 'type' => 'success',
1952 'message' => __('Quote restored successfully.', 'easy-invoice')
1953 ]
1954 ]);
1955 } else {
1956 wp_send_json_error(['message' => __('Failed to restore quote.', 'easy-invoice')]);
1957 }
1958 }
1959
1960 /**
1961 * Handle AJAX request to empty trash
1962 *
1963 * @since 1.0.0
1964 */
1965 public function handleEmptyTrash(): void {
1966 try {
1967 // Verify nonce
1968 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_nonce')) {
1969 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
1970 }
1971
1972 // Check permissions
1973 if (!current_user_can('manage_options')) {
1974 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
1975 }
1976
1977 // Get all quotes in trash (post_status = 'trash')
1978 global $wpdb;
1979 $quote_ids = $wpdb->get_col($wpdb->prepare(
1980 "SELECT ID FROM {$wpdb->posts}
1981 WHERE post_type = %s
1982 AND post_status = 'trash'",
1983 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
1984 ));
1985
1986 if (empty($quote_ids)) {
1987 wp_send_json_error(['message' => __('No quotes found in trash.', 'easy-invoice')]);
1988 }
1989
1990 $success_count = 0;
1991 $error_count = 0;
1992
1993 foreach ($quote_ids as $quote_id) {
1994 if (wp_delete_post($quote_id, true)) {
1995 $this->quote_log_service->logDeletion($quote_id);
1996 $success_count++;
1997 } else {
1998 $error_count++;
1999 }
2000 }
2001
2002 if ($error_count > 0) {
2003 wp_send_json_success([
2004 'message' => sprintf(__('Emptied trash: %d quotes deleted successfully, %d failed.', 'easy-invoice'), $success_count, $error_count),
2005 'success_count' => $success_count,
2006 'error_count' => $error_count,
2007 'toast' => [
2008 'type' => 'warning',
2009 'message' => sprintf(__('Emptied trash: %d quotes deleted successfully, %d failed.', 'easy-invoice'), $success_count, $error_count)
2010 ]
2011 ]);
2012 } else {
2013 wp_send_json_success([
2014 'message' => sprintf(__('Successfully emptied trash: %d quotes deleted.', 'easy-invoice'), $success_count),
2015 'success_count' => $success_count,
2016 'error_count' => 0,
2017 'toast' => [
2018 'type' => 'success',
2019 'message' => sprintf(__('Successfully emptied trash: %d quotes deleted.', 'easy-invoice'), $success_count)
2020 ]
2021 ]);
2022 }
2023
2024 } catch (\Exception $e) {
2025 error_log('Error emptying quote trash: ' . $e->getMessage());
2026 wp_send_json_error([
2027 'message' => __('Failed to empty trash.', 'easy-invoice'),
2028 'debug' => $e->getMessage()
2029 ]);
2030 }
2031 }
2032
2033 /**
2034 * Handle AJAX request to get quote logs
2035 *
2036 * @since 1.0.0
2037 */
2038 public function handleGetQuoteLogs(): void {
2039 // Verify nonce
2040 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_admin_nonce')) {
2041 wp_send_json_error(['message' => __('Security check failed.', 'easy-invoice')]);
2042 }
2043
2044 // Check permissions
2045 if (!current_user_can('manage_options')) {
2046 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
2047 }
2048
2049 $quote_id = isset($_POST['quote_id']) ? (int) $_POST['quote_id'] : 0;
2050
2051 if ($quote_id <= 0) {
2052 wp_send_json_error(['message' => __('Invalid quote ID.', 'easy-invoice')]);
2053 }
2054
2055 try {
2056 $logs = $this->quote_log_service->getLogs($quote_id);
2057
2058 // Convert QuoteLog objects to arrays for JSON response
2059 $logs_data = [];
2060 foreach ($logs as $log) {
2061 $logs_data[] = [
2062 'action' => $log->getAction(),
2063 'description' => $log->getDescription(),
2064 'user_id' => $log->getUserId(),
2065 'user_name' => $log->getUserName(),
2066 'ip_address' => $log->getIpAddress(),
2067 'user_agent' => $log->getUserAgent(),
2068 'additional_data' => $log->getAdditionalData(),
2069 'created_date' => $log->getCreatedDate(),
2070 ];
2071 }
2072
2073 wp_send_json_success([
2074 'logs' => $logs_data,
2075 'count' => count($logs_data)
2076 ]);
2077
2078 } catch (\Exception $e) {
2079 wp_send_json_error([
2080 'message' => __('Error retrieving quote logs.', 'easy-invoice'),
2081 'debug' => $e->getMessage()
2082 ]);
2083 }
2084 }
2085
2086 /**
2087 * Format currency amount using QuoteFormatter
2088 *
2089 * @param float $amount The amount to format
2090 * @param \EasyInvoice\Models\Quote|null $quote The quote object for currency settings
2091 * @return string Formatted currency string
2092 */
2093 private function formatCurrency(float $amount, $quote = null): string {
2094 $formatter = new \EasyInvoice\Helpers\QuoteFormatter($quote);
2095 return $formatter->format($amount);
2096 }
2097 }