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

1,703 lines 71.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Single Quote Template (Bare)
4 *
5 * @package Easy_Invoice
6 * @subpackage Templates
7 */
8
9
10
11 // Prevent direct access
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16
17 // Hide the admin bar for this view
18 add_filter('show_admin_bar', '__return_false');
19
20 // Get quote from WordPress query
21 global $post;
22 $quote = null;
23
24 if ($post && $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
25 // Load quote from post using the correct service provider
26 $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find($post->ID);
27
28 // Ensure quote has proper slug for pretty URLs
29 if ($quote) {
30 $quote->ensureProperSlug();
31 }
32 }
33
34 if (!$quote) {
35 wp_die(__('Quote not found.', 'easy-invoice'));
36 }
37
38 // Get the selected template for this quote
39 $current_template = $quote->getTemplate();
40 if (empty($current_template)) {
41 $current_template = 'standard';
42 }
43
44 // Initialize formatter for the quote templates
45 $formatter = new \EasyInvoice\Helpers\QuoteFormatter($quote);
46
47 // Check if the template file exists
48 $template_file = EASY_INVOICE_PLUGIN_DIR . 'templates/quote-templates/' . $current_template . '.php';
49
50 $template_file = file_exists($template_file) ? $template_file : EASY_INVOICE_PLUGIN_DIR . 'templates/quote-templates/default.php';
51
52 ?><!DOCTYPE html>
53 <html <?php language_attributes(); ?>>
54 <head>
55 <meta charset="<?php bloginfo('charset'); ?>">
56 <meta name="viewport" content="width=device-width, initial-scale=1.0">
57 <title><?php echo esc_html($quote->getTitle() ?: __('Quote', 'easy-invoice')); ?></title>
58
59 <script src="<?php echo esc_url(includes_url('js/jquery/jquery.min.js')); ?>"></script>
60 <script src="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/js/easy-invoice-toast.js'); ?>"></script>
61 <script src="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/js/confirmation-modal.js'); ?>"></script>
62
63 <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
64 <script src="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/js/document-pdf.js?ver=' . rawurlencode(EASY_INVOICE_VERSION)); ?>"></script>
65 <link rel="stylesheet" href="<?php echo esc_url(EASY_INVOICE_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css'); ?>">
66 <?php do_action('easy_invoice_head'); ?>
67 <script>
68 // Global functions for quote actions
69 function resetButtonLoading(button, originalText) {
70 if (button) {
71 button.innerHTML = originalText;
72 button.disabled = false;
73 }
74 }
75
76 // Handle Download PDF
77 function handleDownloadPDF(quoteId, button) {
78 const originalText = button.innerHTML;
79 button.innerHTML = '<?php _e('Generating PDF...', 'easy-invoice'); ?>';
80 button.disabled = true;
81
82 try {
83 // Use the unified PDF generator
84 if (typeof DocumentPdfGenerator !== 'undefined') {
85 const pdfGenerator = new DocumentPdfGenerator('quote');
86 pdfGenerator.generatePDF();
87 } else {
88 throw new Error('PDF generator not available');
89 }
90 } catch (error) {
91 console.error('PDF generation error:', error);
92 showMessage('<?php _e('Error generating PDF', 'easy-invoice'); ?>', 'error');
93 } finally {
94 button.innerHTML = originalText;
95 button.disabled = false;
96 }
97 }
98
99 // Handle Send Email
100 function handleSendEmail(quoteId, confirmBtn, originalText) {
101 // Fallback loading if modal helper is not yet defined
102 if (typeof showModalLoading === 'function') {
103 showModalLoading('<?php echo esc_js(__('Sending email...', 'easy-invoice')); ?>');
104 } else if (confirmBtn) {
105 confirmBtn.disabled = true;
106 confirmBtn.dataset._oldText = confirmBtn.innerHTML;
107 confirmBtn.innerHTML = '<?php echo esc_js(__('Sending...', 'easy-invoice')); ?>';
108 }
109 jQuery.ajax({
110 url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
111 type: 'POST',
112 data: {
113 action: 'easy_invoice_send_quote_email',
114 quote_id: quoteId,
115 nonce: '<?php echo wp_create_nonce('easy_invoice_send_quote_email'); ?>'
116 },
117 success: function(response) {
118 var msg = '';
119 if (response.success) {
120 msg = (response && response.data && (response.data.message || (response.data.toast && response.data.toast.message))) || '<?php _e('Email sent successfully', 'easy-invoice'); ?>';
121 showModalMessage('success', msg);
122 } else {
123 // Handle both response.data.message and response.data (direct string)
124 if (response && response.data) {
125 if (response.data.message) {
126 msg = response.data.message;
127 } else if (typeof response.data === 'string') {
128 msg = response.data;
129 } else if (response.data.toast && response.data.toast.message) {
130 msg = response.data.toast.message;
131 }
132 }
133 msg = msg || '<?php _e('Error sending email', 'easy-invoice'); ?>';
134
135 if (confirmBtn && confirmBtn.dataset._oldText) {
136 confirmBtn.innerHTML = confirmBtn.dataset._oldText;
137 confirmBtn.disabled = false;
138 delete confirmBtn.dataset._oldText;
139 } else {
140 resetButtonLoading(confirmBtn, originalText);
141 }
142 showModalMessage('error', msg);
143 }
144 },
145 error: function(jqXHR) {
146 var msg = (jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.data && (jqXHR.responseJSON.data.message || (jqXHR.responseJSON.data.toast && jqXHR.responseJSON.data.toast.message))) || '';
147 if (confirmBtn && confirmBtn.dataset._oldText) {
148 confirmBtn.innerHTML = confirmBtn.dataset._oldText;
149 confirmBtn.disabled = false;
150 delete confirmBtn.dataset._oldText;
151 } else {
152 resetButtonLoading(confirmBtn, originalText);
153 }
154 showModalMessage('error', msg || '<?php _e('Error connecting to server', 'easy-invoice'); ?>');
155 }
156 });
157 }
158
159 // Show message function
160 function showMessage(message, type) {
161 if (typeof EasyInvoiceToast !== 'undefined') {
162 if (type === 'success') {
163 EasyInvoiceToast.success(message);
164 } else {
165 EasyInvoiceToast.error(message);
166 }
167 } else {
168 alert(message);
169 }
170 }
171
172 // Generic confirmation modal (global) - modern ES6 syntax
173 function showConfirmationModal(title, message, confirmText, cancelText, confirmType, onConfirm) {
174 // Remove existing modal if any
175 const existingModal = document.querySelector('.ei-modal-overlay');
176 if (existingModal) {
177 existingModal.remove();
178 }
179
180 // Create modal overlay
181 const overlay = document.createElement('div');
182 overlay.className = 'ei-modal-overlay';
183
184 // Create modal content
185 const modal = document.createElement('div');
186 modal.className = 'ei-modal';
187
188 const confirmBtnClass = confirmType === 'danger' ? 'ei-modal-btn-danger' : 'ei-modal-btn-primary';
189 const iconClass = confirmType === 'danger' ? 'danger' : 'info';
190 const icon = confirmType === 'danger' ?
191 '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
192 '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 16v-4M12 8h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
193
194 modal.innerHTML = `
195 <div class="ei-modal-header">
196 <div class="ei-modal-icon ${iconClass}">
197 ${icon}
198 </div>
199 <h3 class="ei-modal-title">${title}</h3>
200 </div>
201 <div class="ei-modal-body">
202 <div class="ei-modal-message">${message}</div>
203 </div>
204 <div class="ei-modal-actions">
205 <button type="button" class="ei-modal-btn ei-modal-btn-secondary" id="modal-cancel">${cancelText}</button>
206 <button type="button" class="ei-modal-btn ${confirmBtnClass}" id="modal-confirm">${confirmText}</button>
207 </div>
208 `;
209
210 overlay.appendChild(modal);
211 document.body.appendChild(overlay);
212
213 // Show modal with animation
214 setTimeout(() => {
215 overlay.classList.add('show');
216 modal.classList.add('show');
217 }, 10);
218
219 // Handle button clicks
220 const confirmBtn = modal.querySelector('#modal-confirm');
221 const cancelBtn = modal.querySelector('#modal-cancel');
222
223 confirmBtn.addEventListener('click', function() {
224 const originalText = confirmBtn.innerHTML;
225 setButtonLoading(confirmBtn, confirmBtn.textContent.trim());
226 onConfirm(modal, confirmBtn, originalText);
227 });
228
229 cancelBtn.addEventListener('click', function() {
230 hideModal(overlay);
231 });
232
233 // Handle overlay click to close
234 overlay.addEventListener('click', function(e) {
235 if (e.target === overlay) {
236 hideModal(overlay);
237 }
238 });
239
240 // Handle escape key
241 const handleEscape = function(e) {
242 if (e.key === 'Escape') {
243 hideModal(overlay);
244 document.removeEventListener('keydown', handleEscape);
245 }
246 };
247 document.addEventListener('keydown', handleEscape);
248
249 // Focus on confirm button
250 setTimeout(() => {
251 confirmBtn.focus();
252 }, 100);
253 }
254
255 // Wrapper to confirm sending quote email
256 function confirmSendEmail(quoteId) {
257 const title = '<?php echo esc_js(__('Send Email', 'easy-invoice')); ?>';
258 const message = '<?php echo esc_js(__('Send this quote via email?', 'easy-invoice')); ?>\n\n<?php echo esc_js(__('This will send the quote to the client\'s email address.', 'easy-invoice')); ?>';
259 const confirmText = '<?php echo esc_js(__('Send Email', 'easy-invoice')); ?>';
260 const cancelText = '<?php echo esc_js(__('Cancel', 'easy-invoice')); ?>';
261 showConfirmationModal(title, message, confirmText, cancelText, 'primary', function(modal, confirmBtn, originalText) {
262 handleSendEmail(quoteId, confirmBtn, originalText);
263 });
264 }
265
266 // Global modal helpers
267 function hideModal(overlay) {
268 const modal = overlay.querySelector('.ei-modal');
269 modal.classList.remove('show');
270 overlay.classList.remove('show');
271
272 setTimeout(() => {
273 if (overlay.parentNode) {
274 overlay.remove();
275 }
276 }, 300);
277 }
278
279 // Utility: Show loading spinner in a button
280 function setButtonLoading(btn, loadingText) {
281 btn.disabled = true;
282 btn.innerHTML = `<span class='ei-btn-spinner' style='display:inline-block;vertical-align:middle;width:18px;height:18px;margin-right:8px;'>
283 <svg width='18' height='18' viewBox='0 0 50 50'><circle cx='25' cy='25' r='20' fill='none' stroke='#fff' stroke-width='5' stroke-linecap='round' stroke-dasharray='31.415, 31.415' transform='rotate(0 25 25)'><animateTransform attributeName='transform' type='rotate' from='0 25 25' to='360 25 25' dur='0.8s' repeatCount='indefinite'/></circle></svg>
284 </span>${loadingText}`;
285 }
286
287 function resetButtonLoading(btn, originalText) {
288 btn.disabled = false;
289 btn.innerHTML = originalText;
290 }
291
292 // Show message in modal (used for AJAX responses)
293 function showModalMessage(type, message, onClose) {
294 // Remove existing modal if any
295 const existingModal = document.querySelector('.ei-modal-overlay');
296 if (existingModal) {
297 existingModal.remove();
298 }
299 // Create modal overlay
300 const overlay = document.createElement('div');
301 overlay.className = 'ei-modal-overlay';
302 // Create modal content
303 const modal = document.createElement('div');
304 modal.className = 'ei-modal';
305 const iconClass = type === 'success' ? 'info' : 'danger';
306 const icon = type === 'success'
307 ? '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#10b981"/><path d="M7 13l3 3 7-7" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
308 : '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#ef4444"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
309 modal.innerHTML = `
310 <div class="ei-modal-header">
311 <div class="ei-modal-icon ${iconClass}">${icon}</div>
312 <h3 class="ei-modal-title">${type === 'success' ? '<?php echo esc_js(__('Success', 'easy-invoice')); ?>' : '<?php echo esc_js(__('Error', 'easy-invoice')); ?>'}</h3>
313 </div>
314 <div class="ei-modal-body">
315 <div class="ei-modal-message">${message}</div>
316 </div>
317 <div class="ei-modal-actions">
318 <button type="button" class="ei-modal-btn ei-modal-btn-primary" id="modal-close"><?php echo esc_js(__('Close', 'easy-invoice')); ?></button>
319 </div>
320 `;
321 overlay.appendChild(modal);
322 document.body.appendChild(overlay);
323 setTimeout(() => {
324 overlay.classList.add('show');
325 modal.classList.add('show');
326 }, 10);
327 const closeBtn = modal.querySelector('#modal-close');
328 closeBtn.addEventListener('click', function() {
329 hideModal(overlay);
330 if (onClose) onClose();
331 });
332 overlay.addEventListener('click', function(e) {
333 if (e.target === overlay) {
334 hideModal(overlay);
335 if (onClose) onClose();
336 }
337 });
338 const handleEscape = function(e) {
339 if (e.key === 'Escape') {
340 hideModal(overlay);
341 document.removeEventListener('keydown', handleEscape);
342 if (onClose) onClose();
343 }
344 };
345 document.addEventListener('keydown', handleEscape);
346 setTimeout(() => {
347 closeBtn.focus();
348 }, 100);
349 }
350
351 // Show loading state in modal
352 function showModalLoading(message) {
353 // Remove existing modal if any
354 const existingModal = document.querySelector('.ei-modal-overlay');
355 if (existingModal) {
356 existingModal.remove();
357 }
358 // Create modal overlay
359 const overlay = document.createElement('div');
360 overlay.className = 'ei-modal-overlay';
361 // Create modal content
362 const modal = document.createElement('div');
363 modal.className = 'ei-modal';
364 modal.innerHTML = `
365 <div class="ei-modal-header">
366 <div class="ei-modal-icon info">
367 <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#3b82f6"/><circle cx="12" cy="12" r="6" fill="#fff"/><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></svg>
368 </div>
369 <h3 class="ei-modal-title"><?php echo esc_js(__('Please wait...', 'easy-invoice')); ?></h3>
370 </div>
371 <div class="ei-modal-body">
372 <div class="ei-modal-message">${message}</div>
373 </div>
374 `;
375 overlay.appendChild(modal);
376 document.body.appendChild(overlay);
377 setTimeout(() => {
378 overlay.classList.add('show');
379 modal.classList.add('show');
380 }, 10);
381 }
382
383
384 </script>
385
386 <style>
387 body { margin: 0; padding: 0; font-family: sans-serif; background: #eaeaea; color: #222; }
388 .easy-invoice-quote-container { max-width: 800px; margin: 40px auto; }
389 h1, h2, h3 { margin-top: 0; }
390 .quote-actions { margin-top: 32px; text-align: center; }
391 .quote-actions button, .quote-actions a { margin: 0 8px; padding: 10px 24px; border: none; border-radius: 4px; background: #6366f1; color: #fff; font-size: 1rem; cursor: pointer; text-decoration: none; }
392 .quote-actions button.decline { background: #f87171; }
393 .quote-actions button:disabled { opacity: 0.6; cursor: not-allowed; }
394 .template-not-found { text-align: center; padding: 40px; background: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
395 .template-not-found-icon { font-size: 48px; color: #dc3545; margin-bottom: 20px; }
396 .template-not-found h3 { font-size: 24px; color: #32325d; margin-bottom: 10px; }
397 .template-not-found p { color: #8898aa; margin-bottom: 10px; }
398 .quote-content { flex: 1; min-width: 0; position: relative; }
399
400 /* Custom Modal Styles */
401 .ei-modal-overlay {
402 position: fixed;
403 top: 0;
404 left: 0;
405 width: 100%;
406 height: 100%;
407 background: rgba(0, 0, 0, 0.6);
408 display: flex;
409 justify-content: center;
410 align-items: center;
411 z-index: 10000;
412 opacity: 0;
413 visibility: hidden;
414 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
415 backdrop-filter: blur(4px);
416 }
417
418 .ei-modal-overlay.show {
419 opacity: 1;
420 visibility: visible;
421 }
422
423 .ei-modal {
424 background: white;
425 border-radius: 16px;
426 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
427 max-width: 480px;
428 width: 90%;
429 max-height: 85vh;
430 overflow: hidden;
431 transform: scale(0.95) translateY(20px);
432 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
433 border: 1px solid rgba(0, 0, 0, 0.05);
434 }
435
436 .ei-modal.show {
437 transform: scale(1) translateY(0);
438 }
439
440 .ei-modal-header {
441 padding: 32px 32px 0 32px;
442 text-align: center;
443 position: relative;
444 }
445
446 .ei-modal-icon {
447 width: 64px;
448 height: 64px;
449 border-radius: 50%;
450 margin: 0 auto 20px auto;
451 display: flex;
452 align-items: center;
453 justify-content: center;
454 font-size: 28px;
455 color: white;
456 }
457
458 .ei-modal-icon.success {
459 background: linear-gradient(135deg, #10b981, #059669);
460 }
461
462 .ei-modal-icon.warning {
463 background: linear-gradient(135deg, #f59e0b, #d97706);
464 }
465
466 .ei-modal-icon.danger {
467 background: linear-gradient(135deg, #ef4444, #dc2626);
468 }
469
470 .ei-modal-icon.info {
471 background: linear-gradient(135deg, #3b82f6, #2563eb);
472 }
473
474 .ei-modal-title {
475 font-size: 24px;
476 font-weight: 700;
477 color: #111827;
478 margin: 0 0 8px 0;
479 line-height: 1.3;
480 }
481
482 .ei-modal-subtitle {
483 font-size: 16px;
484 color: #6b7280;
485 margin: 0;
486 line-height: 1.5;
487 }
488
489 .ei-modal-body {
490 padding: 24px 32px 32px 32px;
491 }
492
493 .ei-modal-message {
494 font-size: 16px;
495 line-height: 1.6;
496 color: #374151;
497 margin: 0;
498 white-space: pre-line;
499 text-align: center;
500 }
501
502 .ei-modal-actions {
503 display: flex;
504 gap: 12px;
505 justify-content: center;
506 padding: 0 32px 32px 32px;
507 }
508
509 .ei-modal-btn {
510 padding: 12px 24px;
511 border: none;
512 border-radius: 8px;
513 font-size: 15px;
514 font-weight: 600;
515 cursor: pointer;
516 transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
517 min-width: 100px;
518 position: relative;
519 overflow: hidden;
520 }
521
522 .ei-modal-btn::before {
523 content: '';
524 position: absolute;
525 top: 0;
526 left: -100%;
527 width: 100%;
528 height: 100%;
529 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
530 transition: left 0.5s;
531 }
532
533 .ei-modal-btn:hover::before {
534 left: 100%;
535 }
536
537 .ei-modal-btn-secondary {
538 background: #f9fafb;
539 color: #374151;
540 border: 1px solid #e5e7eb;
541 }
542
543 .ei-modal-btn-secondary:hover {
544 background: #f3f4f6;
545 border-color: #d1d5db;
546 transform: translateY(-1px);
547 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
548 }
549
550 .ei-modal-btn-primary {
551 background: linear-gradient(135deg, #007cba, #005a87);
552 color: white;
553 box-shadow: 0 4px 12px rgba(0, 124, 186, 0.3);
554 }
555
556 .ei-modal-btn-primary:hover {
557 background: linear-gradient(135deg, #005a87, #004a6f);
558 transform: translateY(-1px);
559 box-shadow: 0 6px 16px rgba(0, 124, 186, 0.4);
560 }
561
562 .ei-modal-btn-danger {
563 background: linear-gradient(135deg, #ef4444, #dc2626);
564 color: white;
565 box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
566 }
567
568 .ei-modal-btn-danger:hover {
569 background: linear-gradient(135deg, #dc2626, #b91c1c);
570 transform: translateY(-1px);
571 box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
572 }
573
574 .ei-modal-btn:active {
575 transform: translateY(0);
576 }
577
578 .ei-modal-btn:disabled {
579 opacity: 0.6;
580 cursor: not-allowed;
581 transform: none !important;
582 }
583
584 /* Reason Field Styles */
585 .ei-modal-reason-field {
586 margin-top: 28px;
587 }
588 .ei-modal-label {
589 display: block;
590 font-size: 15px;
591 font-weight: 600;
592 color: #374151;
593 margin-bottom: 10px;
594 text-align: left;
595 }
596 .ei-modal-textarea {
597 width: 100%;
598 max-width: 100%;
599 box-sizing: border-box;
600 padding: 14px 18px;
601 border: 1.5px solid #d1d5db;
602 border-radius: 10px;
603 font-size: 15px;
604 line-height: 1.6;
605 color: #374151;
606 background: #f8fafc;
607 transition: border-color 0.2s, box-shadow 0.2s;
608 resize: vertical;
609 min-height: 90px;
610 font-family: inherit;
611 box-shadow: 0 1px 2px rgba(0,0,0,0.03);
612 }
613 .ei-modal-textarea:focus {
614 outline: none;
615 border-color: #ef4444;
616 background: #fff;
617 box-shadow: 0 0 0 2px #fee2e2;
618 }
619 .ei-modal-textarea::placeholder {
620 color: #9ca3af;
621 }
622 .ei-modal-textarea.error {
623 border-color: #ef4444;
624 background: #fef2f2;
625 }
626 .ei-modal-header {
627 padding: 36px 36px 0 36px;
628 text-align: center;
629 position: relative;
630 }
631 .ei-modal-icon.danger {
632 background: linear-gradient(135deg, #f87171, #dc2626);
633 width: 60px;
634 height: 60px;
635 border-radius: 50%;
636 display: flex;
637 align-items: center;
638 justify-content: center;
639 margin: 0 auto 18px auto;
640 box-shadow: 0 2px 8px rgba(239,68,68,0.10);
641 }
642 .ei-modal-icon.danger svg {
643 width: 32px;
644 height: 32px;
645 display: block;
646 }
647 .ei-modal-icon.info {
648 background: linear-gradient(135deg, #60a5fa, #3b82f6);
649 width: 60px;
650 height: 60px;
651 border-radius: 50%;
652 display: flex;
653 align-items: center;
654 justify-content: center;
655 margin: 0 auto 18px auto;
656 box-shadow: 0 2px 8px rgba(59,130,246,0.10);
657 }
658 .ei-modal-icon.info svg {
659 width: 32px;
660 height: 32px;
661 display: block;
662 }
663 .ei-modal-title {
664 font-size: 23px;
665 font-weight: 700;
666 color: #111827;
667 margin: 0 0 10px 0;
668 line-height: 1.3;
669 }
670 .ei-modal-body {
671 padding: 28px 36px 36px 36px;
672 }
673 .ei-modal-message {
674 font-size: 16px;
675 line-height: 1.7;
676 color: #374151;
677 margin: 0 0 18px 0;
678 white-space: pre-line;
679 text-align: center;
680 }
681 .ei-modal-actions {
682 display: flex;
683 gap: 16px;
684 justify-content: center;
685 padding: 0 36px 36px 36px;
686 margin-top: 18px;
687 }
688 .ei-modal {
689 background: white;
690 border-radius: 18px;
691 box-shadow: 0 30px 60px -10px rgba(0,0,0,0.22), 0 2px 8px rgba(0,0,0,0.08);
692 max-width: 420px;
693 width: 95%;
694 max-height: 90vh;
695 overflow: hidden;
696 transform: scale(0.95) translateY(20px);
697 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
698 border: 1px solid rgba(0, 0, 0, 0.04);
699 }
700 @media (max-width: 640px) {
701 .ei-modal {
702 width: 99%;
703 margin: 10px;
704 }
705 .ei-modal-header, .ei-modal-body, .ei-modal-actions {
706 padding-left: 12px;
707 padding-right: 12px;
708 }
709 }
710
711 @media print {
712 .quote-actions { display: none; }
713 body { background: white; }
714 .quote-content { box-shadow: none; }
715 }
716 </style>
717 </head>
718 <body>
719 <?php
720 // Get global quote settings
721 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
722 $accept_button_text = $settings_controller::getQuoteAcceptText();
723 $accept_action = $settings_controller::getQuoteAcceptAction();
724 $declined_message = $settings_controller::getDeclinedQuoteMessage();
725
726 // Determine what happens when quote is accepted based on global settings
727 $accept_action_description = '';
728 switch ($accept_action) {
729 case 'convert':
730 $accept_action_description = __('This will convert the quote to an invoice.', 'easy-invoice');
731 break;
732 case 'convert_send':
733 $accept_action_description = __('This will convert the quote to an invoice and send it to the client.', 'easy-invoice');
734 break;
735 case 'duplicate':
736 $accept_action_description = __('This will create a new invoice while keeping this quote unchanged.', 'easy-invoice');
737 break;
738 case 'duplicate_send':
739 $accept_action_description = __('This will create a new invoice and send it to the client, while keeping this quote unchanged.', 'easy-invoice');
740 break;
741 case 'do_nothing':
742 default:
743 $accept_action_description = __('This will mark the quote as accepted.', 'easy-invoice');
744 break;
745 }
746 ?>
747
748 <div class="easy-invoice-quote-container">
749 <?php
750 // Get text settings for quote actions
751 $text_settings = \EasyInvoice\Helpers\TemplateTextHelper::getQuoteTextSettings();
752 ?>
753
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 <div class="quote-actions" style="margin-bottom: 32px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
763 <?php if (in_array($quote->getStatus(), ['available', 'sent', 'draft'])): ?>
764 <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);">
765 <?php echo esc_html($text_settings['accept_quote']); ?>
766 </button>
767 <button type="button" class="decline" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
768 <?php echo esc_html($text_settings['decline_quote']); ?>
769 </button>
770 <?php endif; ?>
771
772 <button type="button" onclick="printQuoteContent()" style="background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
773 <?php echo esc_html($text_settings['print']); ?>
774 </button>
775
776 <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;">
777 <?php echo esc_html($text_settings['download_pdf']); ?>
778 </button>
779
780 <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;">
781 <?php echo esc_html($text_settings['send_email']); ?>
782 </button>
783 </div>
784
785 <div class="quote-content" id="quote-content" style="flex: 1; min-width: 0; position: relative;">
786 <?php
787 // Add Additional CSS button for admin users
788 if (is_user_logged_in() && current_user_can('manage_options')):
789 $additional_css = get_post_meta($quote->getId(), '_easy_invoice_additional_css', true) ?: '';
790 $has_css = !empty($additional_css);
791 ?>
792 <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)' : ''; ?>">
793 <i class="fas fa-code"></i>
794 <?php if ($has_css): ?>
795 <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>
796 <?php endif; ?>
797 </button>
798 <?php endif; ?>
799
800 <?php do_action('easy_invoice_quote_view_content_top', $quote); ?>
801 <?php if (file_exists($template_file)): ?>
802 <?php
803 // Include the selected template file
804 include $template_file;
805 ?>
806 <?php else: ?>
807 <div class="quote-header" style="position: relative;">
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 esc_js(wp_create_nonce('easy_invoice_quote_action_' . (int) $quote->getId())); ?>'
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 esc_js(wp_create_nonce('easy_invoice_quote_action_' . (int) $quote->getId())); ?>'
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
1533 <?php
1534 // Load additional CSS for all users (not just admin)
1535 $additional_css = get_post_meta($quote->getId(), '_easy_invoice_additional_css', true) ?: '';
1536 if (!empty($additional_css)):
1537 ?>
1538 <style id="custom-quote-css">
1539 <?php echo esc_html($additional_css); ?>
1540 </style>
1541 <?php endif; ?>
1542
1543 <?php
1544 // Add Additional CSS feature for admin users
1545 if (is_user_logged_in() && current_user_can('manage_options')):
1546 ?>
1547 <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;">
1548 <div style="padding: 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;">
1549 <h3 style="margin: 0; font-size: 18px; font-weight: 600;">Additional CSS</h3>
1550 <button type="button" id="close-css-panel" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280;">&times;</button>
1551 </div>
1552 <div style="padding: 20px;">
1553 <div style="margin-bottom: 15px;">
1554 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom CSS for this quote:</label>
1555 <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>
1556 </div>
1557 <div style="display: flex; gap: 10px;">
1558 <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>
1559 <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>
1560 <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>
1561 </div>
1562 <div id="css-status" style="margin-top: 15px; padding: 10px; border-radius: 4px; display: none;"></div>
1563 </div>
1564 </div>
1565
1566 <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>
1567
1568 <script>
1569 // Additional CSS functionality
1570 document.addEventListener('DOMContentLoaded', function() {
1571 const cssBtn = document.getElementById('additional-css-btn');
1572 const cssPanel = document.getElementById('css-panel');
1573 const closeBtn = document.getElementById('close-css-panel');
1574 const overlay = document.getElementById('css-overlay');
1575 const saveBtn = document.getElementById('save-css-btn');
1576 const previewBtn = document.getElementById('preview-css-btn');
1577 const clearBtn = document.getElementById('clear-css-btn');
1578 const textarea = document.getElementById('additional-css-textarea');
1579 const status = document.getElementById('css-status');
1580
1581 if (!cssBtn) return; // Exit if CSS button doesn't exist
1582
1583 let originalCSS = textarea.value;
1584
1585 // Toggle panel
1586 cssBtn.addEventListener('click', function() {
1587 cssPanel.style.display = 'block';
1588 overlay.style.display = 'block';
1589 });
1590
1591 // Close panel
1592 function closePanel() {
1593 cssPanel.style.display = 'none';
1594 overlay.style.display = 'none';
1595 }
1596
1597 closeBtn.addEventListener('click', closePanel);
1598 overlay.addEventListener('click', closePanel);
1599
1600 // Save CSS
1601 saveBtn.addEventListener('click', function() {
1602 const css = textarea.value;
1603
1604 // AJAX request to save CSS
1605 fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
1606 method: 'POST',
1607 headers: {
1608 'Content-Type': 'application/x-www-form-urlencoded',
1609 },
1610 body: new URLSearchParams({
1611 'action': 'save_additional_css',
1612 'post_id': <?php echo $quote->getId(); ?>,
1613 'css': css,
1614 'nonce': '<?php echo wp_create_nonce('save_additional_css_nonce'); ?>'
1615 })
1616 })
1617 .then(response => response.json())
1618 .then(data => {
1619 if (data.success) {
1620 originalCSS = css;
1621 status.textContent = 'CSS saved successfully!';
1622 status.style.background = '#d1fae5';
1623 status.style.color = '#065f46';
1624 status.style.display = 'block';
1625
1626 // Apply the saved CSS
1627 applyCustomCSS(css);
1628
1629 setTimeout(() => {
1630 status.style.display = 'none';
1631 }, 3000);
1632 } else {
1633 status.textContent = 'Error saving CSS: ' + data.message;
1634 status.style.background = '#fee2e2';
1635 status.style.color = '#991b1b';
1636 status.style.display = 'block';
1637 }
1638 })
1639 .catch(error => {
1640 status.textContent = 'Error saving CSS: ' + error.message;
1641 status.style.background = '#fee2e2';
1642 status.style.color = '#991b1b';
1643 status.style.display = 'block';
1644 });
1645 });
1646
1647 // Preview CSS
1648 previewBtn.addEventListener('click', function() {
1649 const css = textarea.value;
1650 applyCustomCSS(css);
1651
1652 status.textContent = 'CSS preview applied';
1653 status.style.background = '#dbeafe';
1654 status.style.color = '#1e40af';
1655 status.style.display = 'block';
1656
1657 setTimeout(() => {
1658 status.style.display = 'none';
1659 }, 2000);
1660 });
1661
1662 // Clear CSS
1663 clearBtn.addEventListener('click', function() {
1664 if (confirm('Are you sure you want to clear all custom CSS?')) {
1665 textarea.value = '';
1666 applyCustomCSS('');
1667
1668 status.textContent = 'CSS cleared';
1669 status.style.background = '#fee2e2';
1670 status.style.color = '#991b1b';
1671 status.style.display = 'block';
1672
1673 setTimeout(() => {
1674 status.style.display = 'none';
1675 }, 2000);
1676 }
1677 });
1678
1679 // Apply custom CSS function
1680 function applyCustomCSS(css) {
1681 // Remove existing custom CSS
1682 const existingStyle = document.getElementById('custom-quote-css');
1683 if (existingStyle) {
1684 existingStyle.remove();
1685 }
1686
1687 // Add new custom CSS if not empty
1688 if (css.trim()) {
1689 const style = document.createElement('style');
1690 style.id = 'custom-quote-css';
1691 style.textContent = css;
1692 document.head.appendChild(style);
1693 }
1694 }
1695
1696 // Apply saved CSS on page load
1697 applyCustomCSS(originalCSS);
1698 });
1699 </script>
1700 <?php endif; ?>
1701 </body>
1702 </html>
1703