PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.7
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.7
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 / templates / quotes / single.php

single.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.7, at templates/quotes/single.php

1,747 lines 74.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Single Quote Template (Bare)
4 *
5 * @package Easy_Invoice
6 * @subpackage Templates
7 */
8
9
10
11 // Prevent direct access
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16
17 // Hide the admin bar for this view
18 add_filter('show_admin_bar', '__return_false');
19
20 // Get quote from WordPress query
21 global $post;
22 $quote = null;
23
24 if ($post && $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
25 // Load quote from post using the correct service provider
26 $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find($post->ID);
27
28 // Ensure quote has proper slug for pretty URLs
29 if ($quote) {
30 $quote->ensureProperSlug();
31 }
32 }
33
34 if (!$quote) {
35 wp_die(__('Quote not found.', 'easy-invoice'));
36 }
37
38 // Get the selected template for this quote
39 $current_template = $quote->getTemplate();
40 if (empty($current_template)) {
41 $current_template = 'standard';
42 }
43
44 // Initialize formatter for the quote templates
45 $formatter = new \EasyInvoice\Helpers\QuoteFormatter($quote);
46
47 // Check if the template file exists
48 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/quote-templates/' . $current_template . '.php';
49
50 $template_file = file_exists($template_file) ? $template_file : EASY_INVOICE_PLUGIN_DIR . 'templates/quote-templates/default.php';
51
52 ?><!DOCTYPE html>
53 <html <?php language_attributes(); ?>>
54 <head>
55 <meta charset="<?php bloginfo('charset'); ?>">
56 <meta name="viewport" content="width=device-width, initial-scale=1.0">
57 <title><?php echo esc_html($quote->getTitle() ?: __('Quote', 'easy-invoice')); ?></title>
58
59 <script src="<?php echo esc_url(includes_url('js/jquery/jquery.min.js')); ?>"></script>
60 <script src="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/js/easy-invoice-toast.js'); ?>"></script>
61 <script src="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/js/confirmation-modal.js'); ?>"></script>
62
63 <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
64 <script src="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/js/document-pdf.js?ver=' . rawurlencode(EASY_INVOICE_VERSION)); ?>"></script>
65 <link rel="stylesheet" href="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css'); ?>">
66 <?php do_action('easy_invoice_head'); ?>
67 <script>
68 // Global functions for quote actions
69 function resetButtonLoading(button, originalText) {
70 if (button) {
71 button.innerHTML = originalText;
72 button.disabled = false;
73 }
74 }
75
76 // Handle Download PDF
77 function handleDownloadPDF(quoteId, button) {
78 const originalText = button.innerHTML;
79 button.innerHTML = '<?php _e('Generating PDF...', 'easy-invoice'); ?>';
80 button.disabled = true;
81
82 try {
83 // Use the unified PDF generator
84 if (typeof DocumentPdfGenerator !== 'undefined') {
85 const pdfGenerator = new DocumentPdfGenerator('quote');
86 pdfGenerator.generatePDF();
87 } else {
88 throw new Error('PDF generator not available');
89 }
90 } catch (error) {
91 console.error('PDF generation error:', error);
92 showMessage('<?php _e('Error generating PDF', 'easy-invoice'); ?>', 'error');
93 } finally {
94 button.innerHTML = originalText;
95 button.disabled = false;
96 }
97 }
98
99 // Handle Send Email
100 function handleSendEmail(quoteId, confirmBtn, originalText) {
101 // Fallback loading if modal helper is not yet defined
102 if (typeof showModalLoading === 'function') {
103 showModalLoading('<?php echo esc_js(__('Sending email...', 'easy-invoice')); ?>');
104 } else if (confirmBtn) {
105 confirmBtn.disabled = true;
106 confirmBtn.dataset._oldText = confirmBtn.innerHTML;
107 confirmBtn.innerHTML = '<?php echo esc_js(__('Sending...', 'easy-invoice')); ?>';
108 }
109 jQuery.ajax({
110 url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
111 type: 'POST',
112 data: {
113 action: 'easy_invoice_send_quote_email',
114 quote_id: quoteId,
115 nonce: '<?php echo wp_create_nonce('easy_invoice_send_quote_email'); ?>'
116 },
117 success: function(response) {
118 var msg = '';
119 if (response.success) {
120 msg = (response && response.data && (response.data.message || (response.data.toast && response.data.toast.message))) || '<?php _e('Email sent successfully', 'easy-invoice'); ?>';
121 showModalMessage('success', msg);
122 } else {
123 // Handle both response.data.message and response.data (direct string)
124 if (response && response.data) {
125 if (response.data.message) {
126 msg = response.data.message;
127 } else if (typeof response.data === 'string') {
128 msg = response.data;
129 } else if (response.data.toast && response.data.toast.message) {
130 msg = response.data.toast.message;
131 }
132 }
133 msg = msg || '<?php _e('Error sending email', 'easy-invoice'); ?>';
134
135 if (confirmBtn && confirmBtn.dataset._oldText) {
136 confirmBtn.innerHTML = confirmBtn.dataset._oldText;
137 confirmBtn.disabled = false;
138 delete confirmBtn.dataset._oldText;
139 } else {
140 resetButtonLoading(confirmBtn, originalText);
141 }
142 showModalMessage('error', msg);
143 }
144 },
145 error: function(jqXHR) {
146 var msg = (jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.data && (jqXHR.responseJSON.data.message || (jqXHR.responseJSON.data.toast && jqXHR.responseJSON.data.toast.message))) || '';
147 if (confirmBtn && confirmBtn.dataset._oldText) {
148 confirmBtn.innerHTML = confirmBtn.dataset._oldText;
149 confirmBtn.disabled = false;
150 delete confirmBtn.dataset._oldText;
151 } else {
152 resetButtonLoading(confirmBtn, originalText);
153 }
154 showModalMessage('error', msg || '<?php _e('Error connecting to server', 'easy-invoice'); ?>');
155 }
156 });
157 }
158
159 // Show message function
160 function showMessage(message, type) {
161 if (typeof EasyInvoiceToast !== 'undefined') {
162 if (type === 'success') {
163 EasyInvoiceToast.success(message);
164 } else {
165 EasyInvoiceToast.error(message);
166 }
167 } else {
168 alert(message);
169 }
170 }
171
172 // Generic confirmation modal (global) - modern ES6 syntax
173 function showConfirmationModal(title, message, confirmText, cancelText, confirmType, onConfirm) {
174 // Remove existing modal if any
175 const existingModal = document.querySelector('.ei-modal-overlay');
176 if (existingModal) {
177 existingModal.remove();
178 }
179
180 // Create modal overlay
181 const overlay = document.createElement('div');
182 overlay.className = 'ei-modal-overlay';
183
184 // Create modal content
185 const modal = document.createElement('div');
186 modal.className = 'ei-modal';
187
188 const confirmBtnClass = confirmType === 'danger' ? 'ei-modal-btn-danger' : 'ei-modal-btn-primary';
189 const iconClass = confirmType === 'danger' ? 'danger' : 'info';
190 const icon = confirmType === 'danger' ?
191 '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
192 '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 16v-4M12 8h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
193
194 modal.innerHTML = `
195 <div class="ei-modal-header">
196 <div class="ei-modal-icon ${iconClass}">
197 ${icon}
198 </div>
199 <h3 class="ei-modal-title">${title}</h3>
200 </div>
201 <div class="ei-modal-body">
202 <div class="ei-modal-message">${message}</div>
203 </div>
204 <div class="ei-modal-actions">
205 <button type="button" class="ei-modal-btn ei-modal-btn-secondary" id="modal-cancel">${cancelText}</button>
206 <button type="button" class="ei-modal-btn ${confirmBtnClass}" id="modal-confirm">${confirmText}</button>
207 </div>
208 `;
209
210 overlay.appendChild(modal);
211 document.body.appendChild(overlay);
212
213 // Show modal with animation
214 setTimeout(() => {
215 overlay.classList.add('show');
216 modal.classList.add('show');
217 }, 10);
218
219 // Handle button clicks
220 const confirmBtn = modal.querySelector('#modal-confirm');
221 const cancelBtn = modal.querySelector('#modal-cancel');
222
223 confirmBtn.addEventListener('click', function() {
224 const originalText = confirmBtn.innerHTML;
225 setButtonLoading(confirmBtn, confirmBtn.textContent.trim());
226 onConfirm(modal, confirmBtn, originalText);
227 });
228
229 cancelBtn.addEventListener('click', function() {
230 hideModal(overlay);
231 });
232
233 // Handle overlay click to close
234 overlay.addEventListener('click', function(e) {
235 if (e.target === overlay) {
236 hideModal(overlay);
237 }
238 });
239
240 // Handle escape key
241 const handleEscape = function(e) {
242 if (e.key === 'Escape') {
243 hideModal(overlay);
244 document.removeEventListener('keydown', handleEscape);
245 }
246 };
247 document.addEventListener('keydown', handleEscape);
248
249 // Focus on confirm button
250 setTimeout(() => {
251 confirmBtn.focus();
252 }, 100);
253 }
254
255 // Wrapper to confirm sending quote email
256 function confirmSendEmail(quoteId) {
257 const title = '<?php echo esc_js(__('Send Email', 'easy-invoice')); ?>';
258 const message = '<?php echo esc_js(__('Send this quote via email?', 'easy-invoice')); ?>\n\n<?php echo esc_js(__('This will send the quote to the client\'s email address.', 'easy-invoice')); ?>';
259 const confirmText = '<?php echo esc_js(__('Send Email', 'easy-invoice')); ?>';
260 const cancelText = '<?php echo esc_js(__('Cancel', 'easy-invoice')); ?>';
261 showConfirmationModal(title, message, confirmText, cancelText, 'primary', function(modal, confirmBtn, originalText) {
262 handleSendEmail(quoteId, confirmBtn, originalText);
263 });
264 }
265
266 // Global modal helpers
267 function hideModal(overlay) {
268 const modal = overlay.querySelector('.ei-modal');
269 modal.classList.remove('show');
270 overlay.classList.remove('show');
271
272 setTimeout(() => {
273 if (overlay.parentNode) {
274 overlay.remove();
275 }
276 }, 300);
277 }
278
279 // Utility: Show loading spinner in a button
280 function setButtonLoading(btn, loadingText) {
281 btn.disabled = true;
282 btn.innerHTML = `<span class='ei-btn-spinner' style='display:inline-block;vertical-align:middle;width:18px;height:18px;margin-right:8px;'>
283 <svg width='18' height='18' viewBox='0 0 50 50'><circle cx='25' cy='25' r='20' fill='none' stroke='#fff' stroke-width='5' stroke-linecap='round' stroke-dasharray='31.415, 31.415' transform='rotate(0 25 25)'><animateTransform attributeName='transform' type='rotate' from='0 25 25' to='360 25 25' dur='0.8s' repeatCount='indefinite'/></circle></svg>
284 </span>${loadingText}`;
285 }
286
287 function resetButtonLoading(btn, originalText) {
288 btn.disabled = false;
289 btn.innerHTML = originalText;
290 }
291
292 // Show message in modal (used for AJAX responses)
293 function showModalMessage(type, message, onClose) {
294 // Remove existing modal if any
295 const existingModal = document.querySelector('.ei-modal-overlay');
296 if (existingModal) {
297 existingModal.remove();
298 }
299 // Create modal overlay
300 const overlay = document.createElement('div');
301 overlay.className = 'ei-modal-overlay';
302 // Create modal content
303 const modal = document.createElement('div');
304 modal.className = 'ei-modal';
305 const iconClass = type === 'success' ? 'info' : 'danger';
306 const icon = type === 'success'
307 ? '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#10b981"/><path d="M7 13l3 3 7-7" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
308 : '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#ef4444"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
309 modal.innerHTML = `
310 <div class="ei-modal-header">
311 <div class="ei-modal-icon ${iconClass}">${icon}</div>
312 <h3 class="ei-modal-title">${type === 'success' ? '<?php echo esc_js(__('Success', 'easy-invoice')); ?>' : '<?php echo esc_js(__('Error', 'easy-invoice')); ?>'}</h3>
313 </div>
314 <div class="ei-modal-body">
315 <div class="ei-modal-message">${message}</div>
316 </div>
317 <div class="ei-modal-actions">
318 <button type="button" class="ei-modal-btn ei-modal-btn-primary" id="modal-close"><?php echo esc_js(__('Close', 'easy-invoice')); ?></button>
319 </div>
320 `;
321 overlay.appendChild(modal);
322 document.body.appendChild(overlay);
323 setTimeout(() => {
324 overlay.classList.add('show');
325 modal.classList.add('show');
326 }, 10);
327 const closeBtn = modal.querySelector('#modal-close');
328 closeBtn.addEventListener('click', function() {
329 hideModal(overlay);
330 if (onClose) onClose();
331 });
332 overlay.addEventListener('click', function(e) {
333 if (e.target === overlay) {
334 hideModal(overlay);
335 if (onClose) onClose();
336 }
337 });
338 const handleEscape = function(e) {
339 if (e.key === 'Escape') {
340 hideModal(overlay);
341 document.removeEventListener('keydown', handleEscape);
342 if (onClose) onClose();
343 }
344 };
345 document.addEventListener('keydown', handleEscape);
346 setTimeout(() => {
347 closeBtn.focus();
348 }, 100);
349 }
350
351 // Show loading state in modal
352 function showModalLoading(message) {
353 // Remove existing modal if any
354 const existingModal = document.querySelector('.ei-modal-overlay');
355 if (existingModal) {
356 existingModal.remove();
357 }
358 // Create modal overlay
359 const overlay = document.createElement('div');
360 overlay.className = 'ei-modal-overlay';
361 // Create modal content
362 const modal = document.createElement('div');
363 modal.className = 'ei-modal';
364 modal.innerHTML = `
365 <div class="ei-modal-header">
366 <div class="ei-modal-icon info">
367 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#3b82f6"/><circle cx="12" cy="12" r="6" fill="#fff"/><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></svg>
368 </div>
369 <h3 class="ei-modal-title"><?php echo esc_js(__('Please wait...', 'easy-invoice')); ?></h3>
370 </div>
371 <div class="ei-modal-body">
372 <div class="ei-modal-message">${message}</div>
373 </div>
374 `;
375 overlay.appendChild(modal);
376 document.body.appendChild(overlay);
377 setTimeout(() => {
378 overlay.classList.add('show');
379 modal.classList.add('show');
380 }, 10);
381 }
382
383
384 </script>
385
386 <style>
387 body { margin: 0; padding: 0; font-family: sans-serif; background: #eaeaea; color: #222; }
388 .easy-invoice-quote-container { max-width: 800px; margin: 40px auto; }
389 h1, h2, h3 { margin-top: 0; }
390 .quote-actions { margin-top: 32px; text-align: center; }
391 .quote-actions button, .quote-actions a { margin: 0 8px; padding: 10px 24px; border: none; border-radius: 4px; background: #6366f1; color: #fff; font-size: 1rem; cursor: pointer; text-decoration: none; }
392 .quote-actions button.decline { background: #f87171; }
393 .quote-actions button:disabled { opacity: 0.6; cursor: not-allowed; }
394 .template-not-found { text-align: center; padding: 40px; background: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
395 .template-not-found-icon { font-size: 48px; color: #dc3545; margin-bottom: 20px; }
396 .template-not-found h3 { font-size: 24px; color: #32325d; margin-bottom: 10px; }
397 .template-not-found p { color: #8898aa; margin-bottom: 10px; }
398 .quote-content { flex: 1; min-width: 0; position: relative; }
399
400 /* Custom Modal Styles */
401 .ei-modal-overlay {
402 position: fixed;
403 top: 0;
404 left: 0;
405 width: 100%;
406 height: 100%;
407 background: rgba(0, 0, 0, 0.6);
408 display: flex;
409 justify-content: center;
410 align-items: center;
411 z-index: 10000;
412 opacity: 0;
413 visibility: hidden;
414 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
415 backdrop-filter: blur(4px);
416 }
417
418 .ei-modal-overlay.show {
419 opacity: 1;
420 visibility: visible;
421 }
422
423 .ei-modal {
424 background: white;
425 border-radius: 16px;
426 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
427 max-width: 480px;
428 width: 90%;
429 max-height: 85vh;
430 overflow: hidden;
431 transform: scale(0.95) translateY(20px);
432 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
433 border: 1px solid rgba(0, 0, 0, 0.05);
434 }
435
436 .ei-modal.show {
437 transform: scale(1) translateY(0);
438 }
439
440 .ei-modal-header {
441 padding: 32px 32px 0 32px;
442 text-align: center;
443 position: relative;
444 }
445
446 .ei-modal-icon {
447 width: 64px;
448 height: 64px;
449 border-radius: 50%;
450 margin: 0 auto 20px auto;
451 display: flex;
452 align-items: center;
453 justify-content: center;
454 font-size: 28px;
455 color: white;
456 }
457
458 .ei-modal-icon.success {
459 background: linear-gradient(135deg, #10b981, #059669);
460 }
461
462 .ei-modal-icon.warning {
463 background: linear-gradient(135deg, #f59e0b, #d97706);
464 }
465
466 .ei-modal-icon.danger {
467 background: linear-gradient(135deg, #ef4444, #dc2626);
468 }
469
470 .ei-modal-icon.info {
471 background: linear-gradient(135deg, #3b82f6, #2563eb);
472 }
473
474 .ei-modal-title {
475 font-size: 24px;
476 font-weight: 700;
477 color: #111827;
478 margin: 0 0 8px 0;
479 line-height: 1.3;
480 }
481
482 .ei-modal-subtitle {
483 font-size: 16px;
484 color: #6b7280;
485 margin: 0;
486 line-height: 1.5;
487 }
488
489 .ei-modal-body {
490 padding: 24px 32px 32px 32px;
491 }
492
493 .ei-modal-message {
494 font-size: 16px;
495 line-height: 1.6;
496 color: #374151;
497 margin: 0;
498 white-space: pre-line;
499 text-align: center;
500 }
501
502 .ei-modal-actions {
503 display: flex;
504 gap: 12px;
505 justify-content: center;
506 padding: 0 32px 32px 32px;
507 }
508
509 .ei-modal-btn {
510 padding: 12px 24px;
511 border: none;
512 border-radius: 8px;
513 font-size: 15px;
514 font-weight: 600;
515 cursor: pointer;
516 transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
517 min-width: 100px;
518 position: relative;
519 overflow: hidden;
520 }
521
522 .ei-modal-btn::before {
523 content: '';
524 position: absolute;
525 top: 0;
526 left: -100%;
527 width: 100%;
528 height: 100%;
529 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
530 transition: left 0.5s;
531 }
532
533 .ei-modal-btn:hover::before {
534 left: 100%;
535 }
536
537 .ei-modal-btn-secondary {
538 background: #f9fafb;
539 color: #374151;
540 border: 1px solid #e5e7eb;
541 }
542
543 .ei-modal-btn-secondary:hover {
544 background: #f3f4f6;
545 border-color: #d1d5db;
546 transform: translateY(-1px);
547 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
548 }
549
550 .ei-modal-btn-primary {
551 background: linear-gradient(135deg, #007cba, #005a87);
552 color: white;
553 box-shadow: 0 4px 12px rgba(0, 124, 186, 0.3);
554 }
555
556 .ei-modal-btn-primary:hover {
557 background: linear-gradient(135deg, #005a87, #004a6f);
558 transform: translateY(-1px);
559 box-shadow: 0 6px 16px rgba(0, 124, 186, 0.4);
560 }
561
562 .ei-modal-btn-danger {
563 background: linear-gradient(135deg, #ef4444, #dc2626);
564 color: white;
565 box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
566 }
567
568 .ei-modal-btn-danger:hover {
569 background: linear-gradient(135deg, #dc2626, #b91c1c);
570 transform: translateY(-1px);
571 box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
572 }
573
574 .ei-modal-btn:active {
575 transform: translateY(0);
576 }
577
578 .ei-modal-btn:disabled {
579 opacity: 0.6;
580 cursor: not-allowed;
581 transform: none !important;
582 }
583
584 /* Reason Field Styles */
585 .ei-modal-reason-field {
586 margin-top: 28px;
587 }
588 .ei-modal-label {
589 display: block;
590 font-size: 15px;
591 font-weight: 600;
592 color: #374151;
593 margin-bottom: 10px;
594 text-align: left;
595 }
596 .ei-modal-textarea {
597 width: 100%;
598 max-width: 100%;
599 box-sizing: border-box;
600 padding: 14px 18px;
601 border: 1.5px solid #d1d5db;
602 border-radius: 10px;
603 font-size: 15px;
604 line-height: 1.6;
605 color: #374151;
606 background: #f8fafc;
607 transition: border-color 0.2s, box-shadow 0.2s;
608 resize: vertical;
609 min-height: 90px;
610 font-family: inherit;
611 box-shadow: 0 1px 2px rgba(0,0,0,0.03);
612 }
613 .ei-modal-textarea:focus {
614 outline: none;
615 border-color: #ef4444;
616 background: #fff;
617 box-shadow: 0 0 0 2px #fee2e2;
618 }
619 .ei-modal-textarea::placeholder {
620 color: #9ca3af;
621 }
622 .ei-modal-textarea.error {
623 border-color: #ef4444;
624 background: #fef2f2;
625 }
626 .ei-modal-header {
627 padding: 36px 36px 0 36px;
628 text-align: center;
629 position: relative;
630 }
631 .ei-modal-icon.danger {
632 background: linear-gradient(135deg, #f87171, #dc2626);
633 width: 60px;
634 height: 60px;
635 border-radius: 50%;
636 display: flex;
637 align-items: center;
638 justify-content: center;
639 margin: 0 auto 18px auto;
640 box-shadow: 0 2px 8px rgba(239,68,68,0.10);
641 }
642 .ei-modal-icon.danger svg {
643 width: 32px;
644 height: 32px;
645 display: block;
646 }
647 .ei-modal-icon.info {
648 background: linear-gradient(135deg, #60a5fa, #3b82f6);
649 width: 60px;
650 height: 60px;
651 border-radius: 50%;
652 display: flex;
653 align-items: center;
654 justify-content: center;
655 margin: 0 auto 18px auto;
656 box-shadow: 0 2px 8px rgba(59,130,246,0.10);
657 }
658 .ei-modal-icon.info svg {
659 width: 32px;
660 height: 32px;
661 display: block;
662 }
663 .ei-modal-title {
664 font-size: 23px;
665 font-weight: 700;
666 color: #111827;
667 margin: 0 0 10px 0;
668 line-height: 1.3;
669 }
670 .ei-modal-body {
671 padding: 28px 36px 36px 36px;
672 }
673 .ei-modal-message {
674 font-size: 16px;
675 line-height: 1.7;
676 color: #374151;
677 margin: 0 0 18px 0;
678 white-space: pre-line;
679 text-align: center;
680 }
681 .ei-modal-actions {
682 display: flex;
683 gap: 16px;
684 justify-content: center;
685 padding: 0 36px 36px 36px;
686 margin-top: 18px;
687 }
688 .ei-modal {
689 background: white;
690 border-radius: 18px;
691 box-shadow: 0 30px 60px -10px rgba(0,0,0,0.22), 0 2px 8px rgba(0,0,0,0.08);
692 max-width: 420px;
693 width: 95%;
694 max-height: 90vh;
695 overflow: hidden;
696 transform: scale(0.95) translateY(20px);
697 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
698 border: 1px solid rgba(0, 0, 0, 0.04);
699 }
700 @media (max-width: 640px) {
701 .ei-modal {
702 width: 99%;
703 margin: 10px;
704 }
705 .ei-modal-header, .ei-modal-body, .ei-modal-actions {
706 padding-left: 12px;
707 padding-right: 12px;
708 }
709 }
710
711 @media print {
712 .quote-actions { display: none; }
713 body { background: white; }
714 .quote-content { box-shadow: none; }
715 }
716 </style>
717 </head>
718 <body>
719 <?php
720 // Get global quote settings
721 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
722 $accept_button_text = $settings_controller::getQuoteAcceptText();
723 $accept_action = $settings_controller::getQuoteAcceptAction();
724 $declined_message = $settings_controller::getDeclinedQuoteMessage();
725
726 // Determine what happens when quote is accepted based on global settings
727 $accept_action_description = '';
728 switch ($accept_action) {
729 case 'convert':
730 $accept_action_description = __('This will convert the quote to an invoice.', 'easy-invoice');
731 break;
732 case 'convert_send':
733 $accept_action_description = __('This will convert the quote to an invoice and send it to the client.', 'easy-invoice');
734 break;
735 case 'duplicate':
736 $accept_action_description = __('This will create a new invoice while keeping this quote unchanged.', 'easy-invoice');
737 break;
738 case 'duplicate_send':
739 $accept_action_description = __('This will create a new invoice and send it to the client, while keeping this quote unchanged.', 'easy-invoice');
740 break;
741 case 'do_nothing':
742 default:
743 $accept_action_description = __('This will mark the quote as accepted.', 'easy-invoice');
744 break;
745 }
746 ?>
747
748 <div class="easy-invoice-quote-container">
749 <?php
750 // Get text settings for quote actions
751 $text_settings = \EasyInvoice\Helpers\TemplateTextHelper::getQuoteTextSettings();
752
753 // SECURITY (CVE-2026-9021): Only render the accept/decline UI +
754 // the per-quote nonce when the viewer is actually authorised to
755 // act on this quote. Authorisation is delegated to
756 // QuoteController::canActOnQuote which checks (in order):
757 //
758 // 1. valid `?qk=...` access-token query parameter (the
759 // emailed-link recipient path — unauthenticated OK), OR
760 // 2. logged-in admin (manage_options), OR
761 // 3. logged-in user whose email matches the quote's bound
762 // client email (the previously off-by-default ownership
763 // check, now unconditional).
764 //
765 // Casual / hostile visitors to the public single-quote URL who
766 // don't satisfy any of these three see the quote read-only —
767 // no Accept/Decline buttons, no nonce embedded in the page DOM,
768 // so the previous attack of "harvest nonce → POST to admin-ajax"
769 // has nothing to harvest.
770 $ei_can_act_on_quote = \EasyInvoice\Controllers\QuoteController::canActOnQuote(
771 (int) $quote->getId(),
772 $quote
773 );
774
775 // For passing back to the AJAX handler — only embed if the
776 // viewer arrived with a valid token in the URL. Logged-in
777 // admin / client paths don't need it (server resolves them
778 // from the session).
779 $ei_quote_access_token_in_url = isset($_GET['qk'])
780 ? sanitize_text_field(wp_unslash($_GET['qk']))
781 : '';
782 ?>
783
784 <?php if ($ei_can_act_on_quote && in_array($quote->getStatus(), ['available', 'sent', 'draft']) && $accept_action_description): ?>
785 <div class="accept-action-info" style="margin-bottom: 16px; padding: 12px; background: #f8f9fa; border-left: 4px solid #007cba; border-radius: 4px; font-size: 0.9em; color: #666;">
786 <i class="fas fa-info-circle" style="margin-right: 8px; color: #007cba;"></i>
787 <strong><?php echo esc_html($text_settings['accept_quote']); ?>:</strong>
788 <?php echo esc_html($accept_action_description); ?>
789 </div>
790 <?php endif; ?>
791
792 <div class="quote-actions" style="margin-bottom: 32px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
793 <?php
794 // SECURITY (CVE-2026-9021): gate on $ei_can_act_on_quote in addition
795 // to the status check so unauthorised visitors don't see (or click)
796 // the buttons. The buttons render JS that POSTs to admin-ajax with
797 // a quote-action nonce — without the auth gate, anyone could harvest
798 // that nonce from page HTML and accept/decline arbitrary quotes.
799 if ($ei_can_act_on_quote && in_array($quote->getStatus(), ['available', 'sent', 'draft'])):
800 ?>
801 <button type="button" class="accept" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500; min-width: 120px; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
802 <?php echo esc_html($text_settings['accept_quote']); ?>
803 </button>
804 <button type="button" class="decline" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
805 <?php echo esc_html($text_settings['decline_quote']); ?>
806 </button>
807 <?php endif; ?>
808
809 <button type="button" onclick="printQuoteContent()" style="background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
810 <?php echo esc_html($text_settings['print']); ?>
811 </button>
812
813 <button type="button" class="download-pdf-btn" style="background: #f59e0b; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
814 <?php echo esc_html($text_settings['download_pdf']); ?>
815 </button>
816
817 <button type="button" onclick="confirmSendEmail(<?php echo esc_js($quote->getId()); ?>)" class="send-email-button" style="background: #8b5cf6; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
818 <?php echo esc_html($text_settings['send_email']); ?>
819 </button>
820 </div>
821
822 <div class="quote-content" id="quote-content" style="flex: 1; min-width: 0; position: relative;">
823 <?php
824 // Add Additional CSS button for admin users
825 if (is_user_logged_in() && current_user_can('manage_options')):
826 $additional_css = get_post_meta($quote->getId(), '_easy_invoice_additional_css', true) ?: '';
827 $has_css = !empty($additional_css);
828 ?>
829 <button type="button" id="additional-css-btn" style="position: absolute; top: 0; left: -40px; background: <?php echo $has_css ? '#10b981' : '#3b82f6'; ?>; color: white; border: none; padding: 6px 10px; border-radius: 3px; cursor: pointer; font-weight: 500; font-size: 11px; line-height: 1; z-index: 1000;" title="Additional CSS<?php echo $has_css ? ' (CSS already added)' : ''; ?>">
830 <i class="fas fa-code"></i>
831 <?php if ($has_css): ?>
832 <span style="position: absolute; top: -4px; right: -4px; background: #ef4444; color: white; border-radius: 50%; width: 8px; height: 8px; font-size: 6px; display: flex; align-items: center; justify-content: center;">•</span>
833 <?php endif; ?>
834 </button>
835 <?php endif; ?>
836
837 <?php do_action('easy_invoice_quote_view_content_top', $quote); ?>
838 <?php if (file_exists($template_file)): ?>
839 <?php
840 // Include the selected template file
841 include $template_file;
842 ?>
843 <?php else: ?>
844 <div class="quote-header" style="position: relative;">
845 <h1><?php echo esc_html($quote->getTitle() ?: __('Quote', 'easy-invoice')); ?>
846 <span class="quote-status"><?php echo esc_html(ucfirst($quote->getStatus())); ?></span>
847 </h1>
848 <div><?php echo esc_html($quote->getNumber()); ?> &bull; <?php echo esc_html(\EasyInvoice\Controllers\SettingsController::formatDate($quote->getCreatedDate())); ?></div>
849 <?php if ($quote->getCustomerName()): ?>
850 <div style="margin-top: 12px;">
851 <strong><?php echo esc_html($quote->getCustomerName()); ?></strong><br>
852 <?php echo esc_html($quote->getCustomerEmail()); ?><br>
853 <?php echo esc_html($quote->getCustomerAddress()); ?>
854 </div>
855 <?php endif; ?>
856 </div>
857
858 <?php if ($quote->getNotes()): ?>
859 <div style="margin-bottom: 24px; color: #555;">
860 <?php echo nl2br(esc_html($quote->getNotes())); ?>
861 </div>
862 <?php endif; ?>
863
864 <table class="quote-items">
865 <thead>
866 <tr>
867 <th><?php _e('Item', 'easy-invoice'); ?></th>
868 <th><?php _e('Description', 'easy-invoice'); ?></th>
869 <th><?php _e('Qty', 'easy-invoice'); ?></th>
870 <th><?php _e('Unit Price', 'easy-invoice'); ?></th>
871 <th><?php _e('Adjust (%)', 'easy-invoice'); ?></th>
872 <th><?php _e('Total', 'easy-invoice'); ?></th>
873 </tr>
874 </thead>
875 <tbody>
876 <?php foreach ($quote->getItems() as $item): ?>
877 <tr>
878 <td><?php echo esc_html($item->getName()); ?></td>
879 <td><?php echo esc_html($item->getDescription()); ?></td>
880 <td><?php echo esc_html($item->getQuantity()); ?></td>
881 <td><?php echo esc_html(number_format_i18n($item->getPrice(), 2)); ?></td>
882 <td><?php
883 $adjust_percentage = $item->getAdjustPercentage();
884 if ($adjust_percentage != 0) {
885 echo esc_html($adjust_percentage > 0 ? '+' : '') . esc_html($adjust_percentage) . '%';
886 } else {
887 echo esc_html__('—', 'easy-invoice');
888 }
889 ?></td>
890 <td><?php echo esc_html(number_format_i18n($item->getAmount(), 2)); ?></td>
891 </tr>
892 <?php endforeach; ?>
893 </tbody>
894 </table>
895
896 <div class="quote-summary">
897 <div><strong><?php _e('Subtotal', 'easy-invoice'); ?>:</strong> <?php echo esc_html(number_format_i18n($quote->getSubtotal(), 2)); ?></div>
898 <?php if ($quote->getDiscountValue() > 0): ?>
899 <div><strong><?php _e('Discount', 'easy-invoice'); ?>:</strong> -<?php echo esc_html(number_format_i18n($quote->getDiscountValue(), 2)); ?></div>
900 <?php endif; ?>
901 <?php if ($quote->getTaxRate() > 0): ?>
902 <div><strong><?php _e('Tax', 'easy-invoice'); ?> (<?php echo esc_html($quote->getTaxRate()); ?>%):</strong> <?php echo esc_html(number_format_i18n($quote->getTaxAmount(), 2)); ?></div>
903 <?php endif; ?>
904 <?php do_action('easy_invoice_quote_totals_after_tax', $quote); ?>
905 <div style="font-size: 1.2em; margin-top: 8px;"><strong><?php _e('Total', 'easy-invoice'); ?>:</strong> <?php echo esc_html(number_format_i18n($quote->getTotal(), 2)); ?></div>
906 </div>
907 <?php endif; ?>
908
909 <?php do_action('easy_invoice_quote_content_after', $quote); ?>
910 </div>
911 </div>
912
913 <script type="text/javascript">
914 // Print Quote Content - Global function
915 function printQuoteContent() {
916 // Get the quote content
917 const quoteContent = document.querySelector('.quote-content');
918
919 if (!quoteContent) {
920 alert('<?php _e('Quote content not found', 'easy-invoice'); ?>');
921 return;
922 }
923
924 // Create a new window for printing
925 const printWindow = window.open('', '_blank', 'width=800,height=600');
926
927 // Create the print HTML with comprehensive styles
928 const printHTML = `
929 <!DOCTYPE html>
930 <html>
931 <head>
932 <title><?php echo esc_js($quote->getNumber() ?: __('Quote', 'easy-invoice')); ?></title>
933 <meta charset="utf-8">
934 <style>
935 /* Reset and base styles */
936 * {
937 margin: 0;
938 padding: 0;
939 box-sizing: border-box;
940 }
941
942 body {
943 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
944 line-height: 1.6;
945 color: #333;
946 background: white;
947 padding: 20px;
948 }
949
950 /* Quote container styles */
951 .quote-container {
952 max-width: 800px;
953 margin: 0 auto;
954 background: white;
955 padding: 30px;
956 border: 1px solid #e5e7eb;
957 border-radius: 8px;
958 }
959
960 /* Header styles */
961 .quote-header {
962 margin-bottom: 30px;
963 padding-bottom: 20px;
964 border-bottom: 2px solid #e5e7eb;
965 }
966
967 .quote-title {
968 font-size: 2em;
969 font-weight: bold;
970 color: #1f2937;
971 margin-bottom: 10px;
972 }
973
974 .quote-number {
975 font-size: 1.1em;
976 color: #6b7280;
977 margin-bottom: 15px;
978 }
979
980 .quote-date {
981 color: #6b7280;
982 font-size: 0.9em;
983 }
984
985 /* Customer info styles */
986 .customer-info {
987 margin-bottom: 30px;
988 padding: 15px;
989 background: #f9fafb;
990 border-radius: 6px;
991 }
992
993 .customer-name {
994 font-weight: bold;
995 font-size: 1.1em;
996 margin-bottom: 5px;
997 }
998
999 .customer-email, .customer-address {
1000 color: #6b7280;
1001 margin-bottom: 3px;
1002 }
1003
1004 /* Items table styles */
1005 .quote-items {
1006 width: 100%;
1007 border-collapse: collapse;
1008 margin-bottom: 30px;
1009 }
1010
1011 .quote-items th {
1012 background: #f9fafb;
1013 padding: 12px;
1014 text-align: left;
1015 font-weight: 600;
1016 border-bottom: 2px solid #e5e7eb;
1017 color: #374151;
1018 }
1019
1020 .quote-items td {
1021 padding: 12px;
1022 border-bottom: 1px solid #e5e7eb;
1023 vertical-align: top;
1024 }
1025
1026 .quote-items tr:nth-child(even) {
1027 background: #f9fafb;
1028 }
1029
1030 /* Summary styles */
1031 .quote-summary {
1032 margin-top: 30px;
1033 padding: 20px;
1034 background: #f9fafb;
1035 border-radius: 6px;
1036 text-align: right;
1037 }
1038
1039 .quote-summary div {
1040 margin-bottom: 8px;
1041 font-size: 1em;
1042 }
1043
1044 .quote-summary .total {
1045 font-size: 1.2em;
1046 font-weight: bold;
1047 color: #1f2937;
1048 border-top: 2px solid #e5e7eb;
1049 padding-top: 10px;
1050 margin-top: 10px;
1051 }
1052
1053 /* Notes styles */
1054 .quote-notes {
1055 margin-top: 30px;
1056 padding: 15px;
1057 background: #f9fafb;
1058 border-left: 4px solid #3b82f6;
1059 border-radius: 4px;
1060 }
1061
1062 .quote-notes h4 {
1063 margin-bottom: 10px;
1064 color: #1f2937;
1065 }
1066
1067 /* Status styles */
1068 .quote-status {
1069 display: inline-block;
1070 padding: 4px 12px;
1071 border-radius: 20px;
1072 font-size: 0.8em;
1073 font-weight: 600;
1074 text-transform: uppercase;
1075 margin-left: 10px;
1076 }
1077
1078 .status-draft { background: #e5e7eb; color: #374151; }
1079 .status-sent { background: #dbeafe; color: #1e40af; }
1080 .status-accepted { background: #d1fae5; color: #065f46; }
1081 .status-declined { background: #fee2e2; color: #991b1b; }
1082 .status-available { background: #eff6ff; color: #1e40af; }
1083
1084 /* Print-specific styles */
1085 @media print {
1086 body {
1087 margin: 0;
1088 padding: 0;
1089 }
1090
1091 .quote-container {
1092 border: none;
1093 padding: 0;
1094 max-width: none;
1095 }
1096
1097 .quote-items th {
1098 background: #f9fafb !important;
1099 -webkit-print-color-adjust: exact;
1100 color-adjust: exact;
1101 }
1102
1103 .quote-notes {
1104 background: #f9fafb !important;
1105 -webkit-print-color-adjust: exact;
1106 color-adjust: exact;
1107 }
1108
1109 .status-draft { background: #e5e7eb !important; }
1110 .status-sent { background: #dbeafe !important; }
1111 .status-accepted { background: #d1fae5 !important; }
1112 .status-declined { background: #fee2e2 !important; }
1113 .status-available { background: #eff6ff !important; }
1114
1115 /* Ensure proper page breaks */
1116 .quote-content {
1117 page-break-inside: avoid;
1118 }
1119
1120 table {
1121 page-break-inside: avoid;
1122 }
1123
1124 tr {
1125 page-break-inside: avoid;
1126 }
1127 }
1128 </style>
1129 </head>
1130 <body>
1131 ${quoteContent.outerHTML}
1132 </body>
1133 </html>
1134 `;
1135
1136 // Write the content to the new window
1137 printWindow.document.write(printHTML);
1138 printWindow.document.close();
1139
1140 // Wait for content to load, then print
1141 printWindow.onload = function() {
1142 setTimeout(function() {
1143 printWindow.print();
1144 printWindow.close();
1145 }, 500);
1146 };
1147 }
1148
1149 document.addEventListener('DOMContentLoaded', function() {
1150 const quoteId = '<?php echo esc_js($quote->getId()); ?>';
1151 const acceptActionDescription = '<?php echo esc_js($accept_action_description); ?>';
1152 const declinedMessage = '<?php echo esc_js($declined_message); ?>';
1153 const isDeclineReasonRequired = <?php echo json_encode($settings_controller::isDeclineReasonRequired()); ?>;
1154
1155 // Utility: Show loading spinner in a button
1156 function setButtonLoading(btn, loadingText) {
1157 btn.disabled = true;
1158 btn.innerHTML = `<span class='ei-btn-spinner' style='display:inline-block;vertical-align:middle;width:18px;height:18px;margin-right:8px;'>
1159 <svg width='18' height='18' viewBox='0 0 50 50'><circle cx='25' cy='25' r='20' fill='none' stroke='#fff' stroke-width='5' stroke-linecap='round' stroke-dasharray='31.415, 31.415' transform='rotate(0 25 25)'><animateTransform attributeName='transform' type='rotate' from='0 25 25' to='360 25 25' dur='0.8s' repeatCount='indefinite'/></circle></svg>
1160 </span>${loadingText}`;
1161 }
1162 // resetButtonLoading function is now defined globally above
1163
1164 // Accept Quote Button
1165 const acceptButton = document.querySelector('.quote-actions .accept');
1166 if (acceptButton) {
1167 acceptButton.addEventListener('click', function() {
1168 let message = '<?php echo esc_js(__('Accept this quote?', 'easy-invoice')); ?>';
1169 if (acceptActionDescription) {
1170 message += '\n\n' + acceptActionDescription;
1171 }
1172 showConfirmationModal(
1173 '<?php echo esc_js($text_settings['accept_quote']); ?>',
1174 message,
1175 '<?php echo esc_js($text_settings['accept_quote']); ?>',
1176 '<?php echo esc_js(__('Cancel', 'easy-invoice')); ?>',
1177 'primary',
1178 function(modal, confirmBtn, originalText) {
1179 handleAcceptQuote(quoteId, confirmBtn, originalText);
1180 }
1181 );
1182 });
1183 }
1184
1185 // Decline Quote Button
1186 const declineButton = document.querySelector('.quote-actions .decline');
1187 if (declineButton) {
1188 declineButton.addEventListener('click', function() {
1189 let message = '<?php echo esc_js(__('Decline this quote?', 'easy-invoice')); ?>';
1190 if (declinedMessage) {
1191 message += '\n\n' + declinedMessage;
1192 }
1193
1194 if (isDeclineReasonRequired) {
1195 showDeclineModalWithReason();
1196 } else {
1197 showConfirmationModal(
1198 '<?php echo esc_js($text_settings['decline_quote']); ?>',
1199 message,
1200 '<?php echo esc_js($text_settings['decline_quote']); ?>',
1201 '<?php echo esc_js(__('Cancel', 'easy-invoice')); ?>',
1202 'danger',
1203 function(modal, confirmBtn, originalText) {
1204 handleDeclineQuote(quoteId, confirmBtn, originalText);
1205 }
1206 );
1207 }
1208 });
1209 }
1210
1211
1212
1213 // Note: Download and Send Email buttons are handled via inline onclick attributes to avoid duplicate bindings
1214
1215 // Custom Modal System
1216 function showConfirmationModal(title, message, confirmText, cancelText, confirmType, onConfirm) {
1217 // Remove existing modal if any
1218 const existingModal = document.querySelector('.ei-modal-overlay');
1219 if (existingModal) {
1220 existingModal.remove();
1221 }
1222
1223 // Create modal overlay
1224 const overlay = document.createElement('div');
1225 overlay.className = 'ei-modal-overlay';
1226
1227 // Create modal content
1228 const modal = document.createElement('div');
1229 modal.className = 'ei-modal';
1230
1231 const confirmBtnClass = confirmType === 'danger' ? 'ei-modal-btn-danger' : 'ei-modal-btn-primary';
1232 const iconClass = confirmType === 'danger' ? 'danger' : 'info';
1233 const icon = confirmType === 'danger' ?
1234 '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
1235 '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 16v-4M12 8h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1236
1237 modal.innerHTML = `
1238 <div class="ei-modal-header">
1239 <div class="ei-modal-icon ${iconClass}">
1240 ${icon}
1241 </div>
1242 <h3 class="ei-modal-title">${title}</h3>
1243 </div>
1244 <div class="ei-modal-body">
1245 <div class="ei-modal-message">${message}</div>
1246 </div>
1247 <div class="ei-modal-actions">
1248 <button type="button" class="ei-modal-btn ei-modal-btn-secondary" id="modal-cancel">${cancelText}</button>
1249 <button type="button" class="ei-modal-btn ${confirmBtnClass}" id="modal-confirm">${confirmText}</button>
1250 </div>
1251 `;
1252
1253 overlay.appendChild(modal);
1254 document.body.appendChild(overlay);
1255
1256 // Show modal with animation
1257 setTimeout(() => {
1258 overlay.classList.add('show');
1259 modal.classList.add('show');
1260 }, 10);
1261
1262 // Handle button clicks
1263 const confirmBtn = modal.querySelector('#modal-confirm');
1264 const cancelBtn = modal.querySelector('#modal-cancel');
1265
1266 confirmBtn.addEventListener('click', function() {
1267 const originalText = confirmBtn.innerHTML;
1268 setButtonLoading(confirmBtn, confirmBtn.textContent.trim());
1269 onConfirm(modal, confirmBtn, originalText);
1270 });
1271
1272 cancelBtn.addEventListener('click', function() {
1273 hideModal(overlay);
1274 });
1275
1276 // Handle overlay click to close
1277 overlay.addEventListener('click', function(e) {
1278 if (e.target === overlay) {
1279 hideModal(overlay);
1280 }
1281 });
1282
1283 // Handle escape key
1284 const handleEscape = function(e) {
1285 if (e.key === 'Escape') {
1286 hideModal(overlay);
1287 document.removeEventListener('keydown', handleEscape);
1288 }
1289 };
1290 document.addEventListener('keydown', handleEscape);
1291
1292 // Focus on confirm button
1293 setTimeout(() => {
1294 confirmBtn.focus();
1295 }, 100);
1296 }
1297
1298 // Decline Modal with Reason Field
1299 function showDeclineModalWithReason() {
1300 // Remove existing modal if any
1301 const existingModal = document.querySelector('.ei-modal-overlay');
1302 if (existingModal) {
1303 existingModal.remove();
1304 }
1305 // Create modal overlay
1306 const overlay = document.createElement('div');
1307 overlay.className = 'ei-modal-overlay';
1308 // Create modal content
1309 const modal = document.createElement('div');
1310 modal.className = 'ei-modal';
1311 let message = '<?php echo esc_js(__('Decline this quote?', 'easy-invoice')); ?>';
1312 if (declinedMessage) {
1313 message += '\n\n' + declinedMessage;
1314 }
1315 modal.innerHTML = `
1316 <div class="ei-modal-header">
1317 <div class="ei-modal-icon danger">
1318 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1319 </div>
1320 <h3 class="ei-modal-title"><?php echo esc_js($text_settings['decline_quote']); ?></h3>
1321 </div>
1322 <div class="ei-modal-body">
1323 <div class="ei-modal-message">${message}</div>
1324 <div class="ei-modal-reason-field">
1325 <label for="decline-reason" class="ei-modal-label"><?php echo esc_js($text_settings['decline_reason']); ?>:</label>
1326 <textarea id="decline-reason" class="ei-modal-textarea" rows="4" placeholder="<?php echo esc_js(__('Please provide a reason for declining this quote...', 'easy-invoice')); ?>" required></textarea>
1327 </div>
1328 </div>
1329 <div class="ei-modal-actions">
1330 <button type="button" class="ei-modal-btn ei-modal-btn-secondary" id="modal-cancel"><?php echo esc_js(__('Cancel', 'easy-invoice')); ?></button>
1331 <button type="button" class="ei-modal-btn ei-modal-btn-danger" id="modal-confirm"><?php echo esc_js($text_settings['decline_quote']); ?></button>
1332 </div>
1333 `;
1334 overlay.appendChild(modal);
1335 document.body.appendChild(overlay);
1336 setTimeout(() => {
1337 overlay.classList.add('show');
1338 modal.classList.add('show');
1339 }, 10);
1340 // Handle button clicks
1341 const confirmBtn = modal.querySelector('#modal-confirm');
1342 const cancelBtn = modal.querySelector('#modal-cancel');
1343 const reasonField = modal.querySelector('#decline-reason');
1344 confirmBtn.addEventListener('click', function() {
1345 const reason = reasonField.value.trim();
1346 if (!reason) {
1347 reasonField.focus();
1348 reasonField.classList.add('error');
1349 return;
1350 }
1351 const originalText = confirmBtn.innerHTML;
1352 setButtonLoading(confirmBtn, confirmBtn.textContent.trim());
1353 handleDeclineQuote(quoteId, confirmBtn, reason, originalText);
1354 });
1355 cancelBtn.addEventListener('click', function() {
1356 hideModal(overlay);
1357 });
1358 overlay.addEventListener('click', function(e) {
1359 if (e.target === overlay) {
1360 hideModal(overlay);
1361 }
1362 });
1363 const handleEscape = function(e) {
1364 if (e.key === 'Escape') {
1365 hideModal(overlay);
1366 document.removeEventListener('keydown', handleEscape);
1367 }
1368 };
1369 document.addEventListener('keydown', handleEscape);
1370 setTimeout(() => {
1371 reasonField.focus();
1372 }, 100);
1373 reasonField.addEventListener('input', function() {
1374 this.classList.remove('error');
1375 });
1376 }
1377
1378 function hideModal(overlay) {
1379 const modal = overlay.querySelector('.ei-modal');
1380 modal.classList.remove('show');
1381 overlay.classList.remove('show');
1382
1383 setTimeout(() => {
1384 if (overlay.parentNode) {
1385 overlay.remove();
1386 }
1387 }, 300);
1388 }
1389
1390 // Show message in modal (used for AJAX responses)
1391 function showModalMessage(type, message, onClose) {
1392 // Remove existing modal if any
1393 const existingModal = document.querySelector('.ei-modal-overlay');
1394 if (existingModal) {
1395 existingModal.remove();
1396 }
1397 // Create modal overlay
1398 const overlay = document.createElement('div');
1399 overlay.className = 'ei-modal-overlay';
1400 // Create modal content
1401 const modal = document.createElement('div');
1402 modal.className = 'ei-modal';
1403 const iconClass = type === 'success' ? 'info' : 'danger';
1404 const icon = type === 'success'
1405 ? '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#10b981"/><path d="M7 13l3 3 7-7" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
1406 : '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#ef4444"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1407 modal.innerHTML = `
1408 <div class="ei-modal-header">
1409 <div class="ei-modal-icon ${iconClass}">${icon}</div>
1410 <h3 class="ei-modal-title">${type === 'success' ? '<?php echo esc_js(__('Success', 'easy-invoice')); ?>' : '<?php echo esc_js(__('Error', 'easy-invoice')); ?>'}</h3>
1411 </div>
1412 <div class="ei-modal-body">
1413 <div class="ei-modal-message">${message}</div>
1414 </div>
1415 <div class="ei-modal-actions">
1416 <button type="button" class="ei-modal-btn ei-modal-btn-primary" id="modal-close"><?php echo esc_js(__('Close', 'easy-invoice')); ?></button>
1417 </div>
1418 `;
1419 overlay.appendChild(modal);
1420 document.body.appendChild(overlay);
1421 setTimeout(() => {
1422 overlay.classList.add('show');
1423 modal.classList.add('show');
1424 }, 10);
1425 const closeBtn = modal.querySelector('#modal-close');
1426 closeBtn.addEventListener('click', function() {
1427 hideModal(overlay);
1428 if (onClose) onClose();
1429 });
1430 overlay.addEventListener('click', function(e) {
1431 if (e.target === overlay) {
1432 hideModal(overlay);
1433 if (onClose) onClose();
1434 }
1435 });
1436 const handleEscape = function(e) {
1437 if (e.key === 'Escape') {
1438 hideModal(overlay);
1439 document.removeEventListener('keydown', handleEscape);
1440 if (onClose) onClose();
1441 }
1442 };
1443 document.addEventListener('keydown', handleEscape);
1444 setTimeout(() => {
1445 closeBtn.focus();
1446 }, 100);
1447 }
1448 // Show loading state in modal
1449 function showModalLoading(message) {
1450 // Remove existing modal if any
1451 const existingModal = document.querySelector('.ei-modal-overlay');
1452 if (existingModal) {
1453 existingModal.remove();
1454 }
1455 // Create modal overlay
1456 const overlay = document.createElement('div');
1457 overlay.className = 'ei-modal-overlay';
1458 // Create modal content
1459 const modal = document.createElement('div');
1460 modal.className = 'ei-modal';
1461 modal.innerHTML = `
1462 <div class="ei-modal-header">
1463 <div class="ei-modal-icon info">
1464 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#3b82f6"/><circle cx="12" cy="12" r="6" fill="#fff"/><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></svg>
1465 </div>
1466 <h3 class="ei-modal-title"><?php echo esc_js(__('Please wait...', 'easy-invoice')); ?></h3>
1467 </div>
1468 <div class="ei-modal-body">
1469 <div class="ei-modal-message">${message}</div>
1470 </div>
1471 `;
1472 overlay.appendChild(modal);
1473 document.body.appendChild(overlay);
1474 setTimeout(() => {
1475 overlay.classList.add('show');
1476 modal.classList.add('show');
1477 }, 10);
1478 }
1479
1480 // Handle Accept Quote — only defined when the viewer can act on
1481 // this quote. When the viewer can't (e.g. unauthenticated visitor
1482 // with no access token), the function is not emitted and the
1483 // per-quote nonce never reaches the DOM (CVE-2026-9021).
1484 <?php if ($ei_can_act_on_quote): ?>
1485 function handleAcceptQuote(quoteId, confirmBtn, originalText) {
1486 showModalLoading('<?php echo esc_js(__('Accepting quote...', 'easy-invoice')); ?>');
1487 jQuery.ajax({
1488 url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
1489 type: 'POST',
1490 data: {
1491 action: 'easy_invoice_accept_quote',
1492 quote_id: quoteId,
1493 nonce: '<?php echo esc_js(wp_create_nonce('easy_invoice_quote_action_' . (int) $quote->getId())); ?>',
1494 access_token: '<?php echo esc_js($ei_quote_access_token_in_url); ?>'
1495 },
1496 success: function(response) {
1497 if (response.success) {
1498 // Check if an invoice was created and redirect to it
1499 if (response.data && response.data.invoice_id) {
1500 // Use the PHP-generated URL (secure URL first, then regular URL)
1501 let invoiceUrl = response.data.secure_url || response.data.invoice_url;
1502
1503 if (invoiceUrl) {
1504 showModalMessage('success', response.data.message || '<?php _e('Quote accepted successfully! Redirecting to invoice...', 'easy-invoice'); ?>', function() {
1505 window.location.href = invoiceUrl;
1506 });
1507 } else {
1508 // Fallback to reload if no URL available
1509 showModalMessage('success', response.data.message || '<?php _e('Quote accepted successfully', 'easy-invoice'); ?>', function() { location.reload(); });
1510 }
1511 } else {
1512 // No invoice created, just show success message
1513 showModalMessage('success', response.data && response.data.message ? response.data.message : '<?php _e('Quote accepted successfully', 'easy-invoice'); ?>', function() { location.reload(); });
1514 }
1515 } else {
1516 resetButtonLoading(confirmBtn, originalText);
1517 showModalMessage('error', response.data || '<?php _e('Error accepting quote', 'easy-invoice'); ?>');
1518 }
1519 },
1520 error: function() {
1521 resetButtonLoading(confirmBtn, originalText);
1522 showModalMessage('error', '<?php _e('Error connecting to server', 'easy-invoice'); ?>');
1523 }
1524 });
1525 }
1526
1527 // Handle Decline Quote
1528 function handleDeclineQuote(quoteId, confirmBtn, reason = '', originalText) {
1529 showModalLoading('<?php echo esc_js(__('Declining quote...', 'easy-invoice')); ?>');
1530 const ajaxData = {
1531 action: 'easy_invoice_decline_quote',
1532 quote_id: quoteId,
1533 nonce: '<?php echo esc_js(wp_create_nonce('easy_invoice_quote_action_' . (int) $quote->getId())); ?>',
1534 access_token: '<?php echo esc_js($ei_quote_access_token_in_url); ?>'
1535 };
1536 if (reason) {
1537 ajaxData.decline_reason = reason;
1538 }
1539 jQuery.ajax({
1540 url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
1541 type: 'POST',
1542 data: ajaxData,
1543 success: function(response) {
1544 if (response.success) {
1545 showModalMessage('success', response.data && response.data.message ? response.data.message : '<?php _e('Quote declined successfully', 'easy-invoice'); ?>', function() { location.reload(); });
1546 } else {
1547 resetButtonLoading(confirmBtn, originalText);
1548 showModalMessage('error', response.data || '<?php _e('Error declining quote', 'easy-invoice'); ?>');
1549 }
1550 },
1551 error: function() {
1552 resetButtonLoading(confirmBtn, originalText);
1553 showModalMessage('error', '<?php _e('Error connecting to server', 'easy-invoice'); ?>');
1554 }
1555 });
1556 }
1557 <?php endif; // $ei_can_act_on_quote — closes the accept+decline JS handler gate ?>
1558
1559 // Functions moved to global scope above
1560 });
1561 </script>
1562
1563 <script>
1564 document.addEventListener('DOMContentLoaded', function() {
1565 // Auto-download PDF if ?auto_download_pdf=1 is present
1566 if (window.location.search.indexOf('auto_download_pdf=1') !== -1) {
1567 setTimeout(function() {
1568 if (typeof DocumentPdfGenerator !== 'undefined') {
1569 const pdfGenerator = new DocumentPdfGenerator('quote');
1570 pdfGenerator.generatePDF();
1571 }
1572 }, 800);
1573 }
1574 });
1575 </script>
1576
1577 <?php
1578 // Load additional CSS for all users (not just admin)
1579 $additional_css = get_post_meta($quote->getId(), '_easy_invoice_additional_css', true) ?: '';
1580 if (!empty($additional_css)):
1581 ?>
1582 <style id="custom-quote-css">
1583 <?php echo esc_html($additional_css); ?>
1584 </style>
1585 <?php endif; ?>
1586
1587 <?php
1588 // Add Additional CSS feature for admin users
1589 if (is_user_logged_in() && current_user_can('manage_options')):
1590 ?>
1591 <div id="css-panel" style="display: none; position: fixed; top: 0; left: 0; width: 400px; height: 100vh; background: white; box-shadow: 2px 0 10px rgba(0,0,0,0.1); z-index: 1001; overflow-y: auto;">
1592 <div style="padding: 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;">
1593 <h3 style="margin: 0; font-size: 18px; font-weight: 600;">Additional CSS</h3>
1594 <button type="button" id="close-css-panel" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280;">&times;</button>
1595 </div>
1596 <div style="padding: 20px;">
1597 <div style="margin-bottom: 15px;">
1598 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom CSS for this quote:</label>
1599 <textarea id="additional-css-textarea" style="width: calc(100% - 24px); height: 300px; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; resize: vertical; box-sizing: border-box;" placeholder="Enter your custom CSS here..."><?php echo esc_textarea($additional_css); ?></textarea>
1600 </div>
1601 <div style="display: flex; gap: 10px;">
1602 <button type="button" id="save-css-btn" style="background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">Save CSS</button>
1603 <button type="button" id="preview-css-btn" style="background: #6366f1; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">Preview</button>
1604 <button type="button" id="clear-css-btn" style="background: #ef4444; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">Clear</button>
1605 </div>
1606 <div id="css-status" style="margin-top: 15px; padding: 10px; border-radius: 4px; display: none;"></div>
1607 </div>
1608 </div>
1609
1610 <div id="css-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;"></div>
1611
1612 <script>
1613 // Additional CSS functionality
1614 document.addEventListener('DOMContentLoaded', function() {
1615 const cssBtn = document.getElementById('additional-css-btn');
1616 const cssPanel = document.getElementById('css-panel');
1617 const closeBtn = document.getElementById('close-css-panel');
1618 const overlay = document.getElementById('css-overlay');
1619 const saveBtn = document.getElementById('save-css-btn');
1620 const previewBtn = document.getElementById('preview-css-btn');
1621 const clearBtn = document.getElementById('clear-css-btn');
1622 const textarea = document.getElementById('additional-css-textarea');
1623 const status = document.getElementById('css-status');
1624
1625 if (!cssBtn) return; // Exit if CSS button doesn't exist
1626
1627 let originalCSS = textarea.value;
1628
1629 // Toggle panel
1630 cssBtn.addEventListener('click', function() {
1631 cssPanel.style.display = 'block';
1632 overlay.style.display = 'block';
1633 });
1634
1635 // Close panel
1636 function closePanel() {
1637 cssPanel.style.display = 'none';
1638 overlay.style.display = 'none';
1639 }
1640
1641 closeBtn.addEventListener('click', closePanel);
1642 overlay.addEventListener('click', closePanel);
1643
1644 // Save CSS
1645 saveBtn.addEventListener('click', function() {
1646 const css = textarea.value;
1647
1648 // AJAX request to save CSS
1649 fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
1650 method: 'POST',
1651 headers: {
1652 'Content-Type': 'application/x-www-form-urlencoded',
1653 },
1654 body: new URLSearchParams({
1655 'action': 'save_additional_css',
1656 'post_id': <?php echo $quote->getId(); ?>,
1657 'css': css,
1658 'nonce': '<?php echo wp_create_nonce('save_additional_css_nonce'); ?>'
1659 })
1660 })
1661 .then(response => response.json())
1662 .then(data => {
1663 if (data.success) {
1664 originalCSS = css;
1665 status.textContent = 'CSS saved successfully!';
1666 status.style.background = '#d1fae5';
1667 status.style.color = '#065f46';
1668 status.style.display = 'block';
1669
1670 // Apply the saved CSS
1671 applyCustomCSS(css);
1672
1673 setTimeout(() => {
1674 status.style.display = 'none';
1675 }, 3000);
1676 } else {
1677 status.textContent = 'Error saving CSS: ' + data.message;
1678 status.style.background = '#fee2e2';
1679 status.style.color = '#991b1b';
1680 status.style.display = 'block';
1681 }
1682 })
1683 .catch(error => {
1684 status.textContent = 'Error saving CSS: ' + error.message;
1685 status.style.background = '#fee2e2';
1686 status.style.color = '#991b1b';
1687 status.style.display = 'block';
1688 });
1689 });
1690
1691 // Preview CSS
1692 previewBtn.addEventListener('click', function() {
1693 const css = textarea.value;
1694 applyCustomCSS(css);
1695
1696 status.textContent = 'CSS preview applied';
1697 status.style.background = '#dbeafe';
1698 status.style.color = '#1e40af';
1699 status.style.display = 'block';
1700
1701 setTimeout(() => {
1702 status.style.display = 'none';
1703 }, 2000);
1704 });
1705
1706 // Clear CSS
1707 clearBtn.addEventListener('click', function() {
1708 if (confirm('Are you sure you want to clear all custom CSS?')) {
1709 textarea.value = '';
1710 applyCustomCSS('');
1711
1712 status.textContent = 'CSS cleared';
1713 status.style.background = '#fee2e2';
1714 status.style.color = '#991b1b';
1715 status.style.display = 'block';
1716
1717 setTimeout(() => {
1718 status.style.display = 'none';
1719 }, 2000);
1720 }
1721 });
1722
1723 // Apply custom CSS function
1724 function applyCustomCSS(css) {
1725 // Remove existing custom CSS
1726 const existingStyle = document.getElementById('custom-quote-css');
1727 if (existingStyle) {
1728 existingStyle.remove();
1729 }
1730
1731 // Add new custom CSS if not empty
1732 if (css.trim()) {
1733 const style = document.createElement('style');
1734 style.id = 'custom-quote-css';
1735 style.textContent = css;
1736 document.head.appendChild(style);
1737 }
1738 }
1739
1740 // Apply saved CSS on page load
1741 applyCustomCSS(originalCSS);
1742 });
1743 </script>
1744 <?php endif; ?>
1745 </body>
1746 </html>
1747