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