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

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

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