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

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