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