PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
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 -1691 2.3.3 → 2.4.2 View file →
@@ -1,1702 +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 -
754 - <?php if (in_array($quote->getStatus(), ['available', 'sent', 'draft']) && $accept_action_description): ?>
755 - <div class="accept-action-info" style="margin-bottom: 16px; padding: 12px; background: #f8f9fa; border-left: 4px solid #007cba; border-radius: 4px; font-size: 0.9em; color: #666;">
756 - <i class="fas fa-info-circle" style="margin-right: 8px; color: #007cba;"></i>
757 - <strong><?php echo esc_html($text_settings['accept_quote']); ?>:</strong>
758 - <?php echo esc_html($accept_action_description); ?>
759 - </div>
760 - <?php endif; ?>
761 -
762 - <div class="quote-actions" style="margin-bottom: 32px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
763 - <?php if (in_array($quote->getStatus(), ['available', 'sent', 'draft'])): ?>
764 - <button type="button" class="accept" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500; min-width: 120px; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
765 - <?php echo esc_html($text_settings['accept_quote']); ?>
766 - </button>
767 - <button type="button" class="decline" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
768 - <?php echo esc_html($text_settings['decline_quote']); ?>
769 - </button>
770 - <?php endif; ?>
771 -
772 - <button type="button" onclick="printQuoteContent()" style="background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
773 - <?php echo esc_html($text_settings['print']); ?>
774 - </button>
775 -
776 - <button type="button" class="download-pdf-btn" style="background: #f59e0b; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
777 - <?php echo esc_html($text_settings['download_pdf']); ?>
778 - </button>
779 -
780 - <button type="button" onclick="confirmSendEmail(<?php echo esc_js($quote->getId()); ?>)" class="send-email-button" style="background: #8b5cf6; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">
781 - <?php echo esc_html($text_settings['send_email']); ?>
782 - </button>
783 - </div>
784 -
785 - <div class="quote-content" id="quote-content" style="flex: 1; min-width: 0; position: relative;">
786 - <?php
787 - // Add Additional CSS button for admin users
788 - if (is_user_logged_in() && current_user_can('manage_options')):
789 - $additional_css = get_post_meta($quote->getId(), '_easy_invoice_additional_css', true) ?: '';
790 - $has_css = !empty($additional_css);
791 - ?>
792 - <button type="button" id="additional-css-btn" style="position: absolute; top: 0; left: -40px; background: <?php echo $has_css ? '#10b981' : '#3b82f6'; ?>; color: white; border: none; padding: 6px 10px; border-radius: 3px; cursor: pointer; font-weight: 500; font-size: 11px; line-height: 1; z-index: 1000;" title="Additional CSS<?php echo $has_css ? ' (CSS already added)' : ''; ?>">
793 - <i class="fas fa-code"></i>
794 - <?php if ($has_css): ?>
795 - <span style="position: absolute; top: -4px; right: -4px; background: #ef4444; color: white; border-radius: 50%; width: 8px; height: 8px; font-size: 6px; display: flex; align-items: center; justify-content: center;">•</span>
796 - <?php endif; ?>
797 - </button>
798 - <?php endif; ?>
799 -
800 - <?php do_action('easy_invoice_quote_view_content_top', $quote); ?>
801 - <?php if (file_exists($template_file)): ?>
802 - <?php
803 - // Include the selected template file
804 - include $template_file;
805 - ?>
806 - <?php else: ?>
807 - <div class="quote-header" style="position: relative;">
808 - <h1><?php echo esc_html($quote->getTitle() ?: __('Quote', 'easy-invoice')); ?>
809 - <span class="quote-status"><?php echo esc_html(ucfirst($quote->getStatus())); ?></span>
810 - </h1>
811 - <div><?php echo esc_html($quote->getNumber()); ?> &bull; <?php echo esc_html(\EasyInvoice\Controllers\SettingsController::formatDate($quote->getCreatedDate())); ?></div>
812 - <?php if ($quote->getCustomerName()): ?>
813 - <div style="margin-top: 12px;">
814 - <strong><?php echo esc_html($quote->getCustomerName()); ?></strong><br>
815 - <?php echo esc_html($quote->getCustomerEmail()); ?><br>
816 - <?php echo esc_html($quote->getCustomerAddress()); ?>
817 - </div>
818 - <?php endif; ?>
819 - </div>
820 -
821 - <?php if ($quote->getNotes()): ?>
822 - <div style="margin-bottom: 24px; color: #555;">
823 - <?php echo nl2br(esc_html($quote->getNotes())); ?>
824 - </div>
825 - <?php endif; ?>
826 -
827 - <table class="quote-items">
828 - <thead>
829 - <tr>
830 - <th><?php _e('Item', 'easy-invoice'); ?></th>
831 - <th><?php _e('Description', 'easy-invoice'); ?></th>
832 - <th><?php _e('Qty', 'easy-invoice'); ?></th>
833 - <th><?php _e('Unit Price', 'easy-invoice'); ?></th>
834 - <th><?php _e('Adjust (%)', 'easy-invoice'); ?></th>
835 - <th><?php _e('Total', 'easy-invoice'); ?></th>
836 - </tr>
837 - </thead>
838 - <tbody>
839 - <?php foreach ($quote->getItems() as $item): ?>
840 - <tr>
841 - <td><?php echo esc_html($item->getName()); ?></td>
842 - <td><?php echo esc_html($item->getDescription()); ?></td>
843 - <td><?php echo esc_html($item->getQuantity()); ?></td>
844 - <td><?php echo esc_html(number_format_i18n($item->getPrice(), 2)); ?></td>
845 - <td><?php
846 - $adjust_percentage = $item->getAdjustPercentage();
847 - if ($adjust_percentage != 0) {
848 - echo esc_html($adjust_percentage > 0 ? '+' : '') . esc_html($adjust_percentage) . '%';
849 - } else {
850 - echo esc_html__('—', 'easy-invoice');
851 - }
852 - ?></td>
853 - <td><?php echo esc_html(number_format_i18n($item->getAmount(), 2)); ?></td>
854 - </tr>
855 - <?php endforeach; ?>
856 - </tbody>
857 - </table>
858 -
859 - <div class="quote-summary">
860 - <div><strong><?php _e('Subtotal', 'easy-invoice'); ?>:</strong> <?php echo esc_html(number_format_i18n($quote->getSubtotal(), 2)); ?></div>
861 - <?php if ($quote->getDiscountValue() > 0): ?>
862 - <div><strong><?php _e('Discount', 'easy-invoice'); ?>:</strong> -<?php echo esc_html(number_format_i18n($quote->getDiscountValue(), 2)); ?></div>
863 - <?php endif; ?>
864 - <?php if ($quote->getTaxRate() > 0): ?>
865 - <div><strong><?php _e('Tax', 'easy-invoice'); ?> (<?php echo esc_html($quote->getTaxRate()); ?>%):</strong> <?php echo esc_html(number_format_i18n($quote->getTaxAmount(), 2)); ?></div>
866 - <?php endif; ?>
867 - <?php do_action('easy_invoice_quote_totals_after_tax', $quote); ?>
868 - <div style="font-size: 1.2em; margin-top: 8px;"><strong><?php _e('Total', 'easy-invoice'); ?>:</strong> <?php echo esc_html(number_format_i18n($quote->getTotal(), 2)); ?></div>
869 - </div>
870 - <?php endif; ?>
871 -
872 - <?php do_action('easy_invoice_quote_content_after', $quote); ?>
873 - </div>
874 - </div>
875 -
876 - <script type="text/javascript">
877 - // Print Quote Content - Global function
878 - function printQuoteContent() {
879 - // Get the quote content
880 - const quoteContent = document.querySelector('.quote-content');
881 -
882 - if (!quoteContent) {
883 - alert('<?php _e('Quote content not found', 'easy-invoice'); ?>');
884 - return;
885 - }
886 -
887 - // Create a new window for printing
888 - const printWindow = window.open('', '_blank', 'width=800,height=600');
889 -
890 - // Create the print HTML with comprehensive styles
891 - const printHTML = `
892 - <!DOCTYPE html>
893 - <html>
894 - <head>
895 - <title><?php echo esc_js($quote->getNumber() ?: __('Quote', 'easy-invoice')); ?></title>
896 - <meta charset="utf-8">
897 - <style>
898 - /* Reset and base styles */
899 - * {
900 - margin: 0;
901 - padding: 0;
902 - box-sizing: border-box;
903 - }
904 -
905 - body {
906 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
907 - line-height: 1.6;
908 - color: #333;
909 - background: white;
910 - padding: 20px;
911 - }
912 -
913 - /* Quote container styles */
914 - .quote-container {
915 - max-width: 800px;
916 - margin: 0 auto;
917 - background: white;
918 - padding: 30px;
919 - border: 1px solid #e5e7eb;
920 - border-radius: 8px;
921 - }
922 -
923 - /* Header styles */
924 - .quote-header {
925 - margin-bottom: 30px;
926 - padding-bottom: 20px;
927 - border-bottom: 2px solid #e5e7eb;
928 - }
929 -
930 - .quote-title {
931 - font-size: 2em;
932 - font-weight: bold;
933 - color: #1f2937;
934 - margin-bottom: 10px;
935 - }
936 -
937 - .quote-number {
938 - font-size: 1.1em;
939 - color: #6b7280;
940 - margin-bottom: 15px;
941 - }
942 -
943 - .quote-date {
944 - color: #6b7280;
945 - font-size: 0.9em;
946 - }
947 -
948 - /* Customer info styles */
949 - .customer-info {
950 - margin-bottom: 30px;
951 - padding: 15px;
952 - background: #f9fafb;
953 - border-radius: 6px;
954 - }
955 -
956 - .customer-name {
957 - font-weight: bold;
958 - font-size: 1.1em;
959 - margin-bottom: 5px;
960 - }
961 -
962 - .customer-email, .customer-address {
963 - color: #6b7280;
964 - margin-bottom: 3px;
965 - }
966 -
967 - /* Items table styles */
968 - .quote-items {
969 - width: 100%;
970 - border-collapse: collapse;
971 - margin-bottom: 30px;
972 - }
973 -
974 - .quote-items th {
975 - background: #f9fafb;
976 - padding: 12px;
977 - text-align: left;
978 - font-weight: 600;
979 - border-bottom: 2px solid #e5e7eb;
980 - color: #374151;
981 - }
982 -
983 - .quote-items td {
984 - padding: 12px;
985 - border-bottom: 1px solid #e5e7eb;
986 - vertical-align: top;
987 - }
988 -
989 - .quote-items tr:nth-child(even) {
990 - background: #f9fafb;
991 - }
992 -
993 - /* Summary styles */
994 - .quote-summary {
995 - margin-top: 30px;
996 - padding: 20px;
997 - background: #f9fafb;
998 - border-radius: 6px;
999 - text-align: right;
1000 - }
1001 -
1002 - .quote-summary div {
1003 - margin-bottom: 8px;
1004 - font-size: 1em;
1005 - }
1006 -
1007 - .quote-summary .total {
1008 - font-size: 1.2em;
1009 - font-weight: bold;
1010 - color: #1f2937;
1011 - border-top: 2px solid #e5e7eb;
1012 - padding-top: 10px;
1013 - margin-top: 10px;
1014 - }
1015 -
1016 - /* Notes styles */
1017 - .quote-notes {
1018 - margin-top: 30px;
1019 - padding: 15px;
1020 - background: #f9fafb;
1021 - border-left: 4px solid #3b82f6;
1022 - border-radius: 4px;
1023 - }
1024 -
1025 - .quote-notes h4 {
1026 - margin-bottom: 10px;
1027 - color: #1f2937;
1028 - }
1029 -
1030 - /* Status styles */
1031 - .quote-status {
1032 - display: inline-block;
1033 - padding: 4px 12px;
1034 - border-radius: 20px;
1035 - font-size: 0.8em;
1036 - font-weight: 600;
1037 - text-transform: uppercase;
1038 - margin-left: 10px;
1039 - }
1040 -
1041 - .status-draft { background: #e5e7eb; color: #374151; }
1042 - .status-sent { background: #dbeafe; color: #1e40af; }
1043 - .status-accepted { background: #d1fae5; color: #065f46; }
1044 - .status-declined { background: #fee2e2; color: #991b1b; }
1045 - .status-available { background: #eff6ff; color: #1e40af; }
1046 -
1047 - /* Print-specific styles */
1048 - @media print {
1049 - body {
1050 - margin: 0;
1051 - padding: 0;
1052 - }
1053 -
1054 - .quote-container {
1055 - border: none;
1056 - padding: 0;
1057 - max-width: none;
1058 - }
1059 -
1060 - .quote-items th {
1061 - background: #f9fafb !important;
1062 - -webkit-print-color-adjust: exact;
1063 - color-adjust: exact;
1064 - }
1065 -
1066 - .quote-notes {
1067 - background: #f9fafb !important;
1068 - -webkit-print-color-adjust: exact;
1069 - color-adjust: exact;
1070 - }
1071 -
1072 - .status-draft { background: #e5e7eb !important; }
1073 - .status-sent { background: #dbeafe !important; }
1074 - .status-accepted { background: #d1fae5 !important; }
1075 - .status-declined { background: #fee2e2 !important; }
1076 - .status-available { background: #eff6ff !important; }
1077 -
1078 - /* Ensure proper page breaks */
1079 - .quote-content {
1080 - page-break-inside: avoid;
1081 - }
1082 -
1083 - table {
1084 - page-break-inside: avoid;
1085 - }
1086 -
1087 - tr {
1088 - page-break-inside: avoid;
1089 - }
1090 - }
1091 - </style>
1092 - </head>
1093 - <body>
1094 - ${quoteContent.outerHTML}
1095 - </body>
1096 - </html>
1097 - `;
1098 -
1099 - // Write the content to the new window
1100 - printWindow.document.write(printHTML);
1101 - printWindow.document.close();
1102 -
1103 - // Wait for content to load, then print
1104 - printWindow.onload = function() {
1105 - setTimeout(function() {
1106 - printWindow.print();
1107 - printWindow.close();
1108 - }, 500);
1109 - };
1110 - }
1111 -
1112 - document.addEventListener('DOMContentLoaded', function() {
1113 - const quoteId = '<?php echo esc_js($quote->getId()); ?>';
1114 - const acceptActionDescription = '<?php echo esc_js($accept_action_description); ?>';
1115 - const declinedMessage = '<?php echo esc_js($declined_message); ?>';
1116 - const isDeclineReasonRequired = <?php echo json_encode($settings_controller::isDeclineReasonRequired()); ?>;
1117 -
1118 - // Utility: Show loading spinner in a button
1119 - function setButtonLoading(btn, loadingText) {
1120 - btn.disabled = true;
1121 - btn.innerHTML = `<span class='ei-btn-spinner' style='display:inline-block;vertical-align:middle;width:18px;height:18px;margin-right:8px;'>
1122 - <svg width='18' height='18' viewBox='0 0 50 50'><circle cx='25' cy='25' r='20' fill='none' stroke='#fff' stroke-width='5' stroke-linecap='round' stroke-dasharray='31.415, 31.415' transform='rotate(0 25 25)'><animateTransform attributeName='transform' type='rotate' from='0 25 25' to='360 25 25' dur='0.8s' repeatCount='indefinite'/></circle></svg>
1123 - </span>${loadingText}`;
1124 - }
1125 - // resetButtonLoading function is now defined globally above
1126 -
1127 - // Accept Quote Button
1128 - const acceptButton = document.querySelector('.quote-actions .accept');
1129 - if (acceptButton) {
1130 - acceptButton.addEventListener('click', function() {
1131 - let message = '<?php echo esc_js(__('Accept this quote?', 'easy-invoice')); ?>';
1132 - if (acceptActionDescription) {
1133 - message += '\n\n' + acceptActionDescription;
1134 - }
1135 - showConfirmationModal(
1136 - '<?php echo esc_js($text_settings['accept_quote']); ?>',
1137 - message,
1138 - '<?php echo esc_js($text_settings['accept_quote']); ?>',
1139 - '<?php echo esc_js(__('Cancel', 'easy-invoice')); ?>',
1140 - 'primary',
1141 - function(modal, confirmBtn, originalText) {
1142 - handleAcceptQuote(quoteId, confirmBtn, originalText);
1143 - }
1144 - );
1145 - });
1146 - }
1147 -
1148 - // Decline Quote Button
1149 - const declineButton = document.querySelector('.quote-actions .decline');
1150 - if (declineButton) {
1151 - declineButton.addEventListener('click', function() {
1152 - let message = '<?php echo esc_js(__('Decline this quote?', 'easy-invoice')); ?>';
1153 - if (declinedMessage) {
1154 - message += '\n\n' + declinedMessage;
1155 - }
1156 -
1157 - if (isDeclineReasonRequired) {
1158 - showDeclineModalWithReason();
1159 - } else {
1160 - showConfirmationModal(
1161 - '<?php echo esc_js($text_settings['decline_quote']); ?>',
1162 - message,
1163 - '<?php echo esc_js($text_settings['decline_quote']); ?>',
1164 - '<?php echo esc_js(__('Cancel', 'easy-invoice')); ?>',
1165 - 'danger',
1166 - function(modal, confirmBtn, originalText) {
1167 - handleDeclineQuote(quoteId, confirmBtn, originalText);
1168 - }
1169 - );
1170 - }
1171 - });
1172 - }
1173 -
1174 -
1175 -
1176 - // Note: Download and Send Email buttons are handled via inline onclick attributes to avoid duplicate bindings
1177 -
1178 - // Custom Modal System
1179 - function showConfirmationModal(title, message, confirmText, cancelText, confirmType, onConfirm) {
1180 - // Remove existing modal if any
1181 - const existingModal = document.querySelector('.ei-modal-overlay');
1182 - if (existingModal) {
1183 - existingModal.remove();
1184 - }
1185 -
1186 - // Create modal overlay
1187 - const overlay = document.createElement('div');
1188 - overlay.className = 'ei-modal-overlay';
1189 -
1190 - // Create modal content
1191 - const modal = document.createElement('div');
1192 - modal.className = 'ei-modal';
1193 -
1194 - const confirmBtnClass = confirmType === 'danger' ? 'ei-modal-btn-danger' : 'ei-modal-btn-primary';
1195 - const iconClass = confirmType === 'danger' ? 'danger' : 'info';
1196 - const icon = confirmType === 'danger' ?
1197 - '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
1198 - '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 16v-4M12 8h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1199 -
1200 - modal.innerHTML = `
1201 - <div class="ei-modal-header">
1202 - <div class="ei-modal-icon ${iconClass}">
1203 - ${icon}
1204 - </div>
1205 - <h3 class="ei-modal-title">${title}</h3>
1206 - </div>
1207 - <div class="ei-modal-body">
1208 - <div class="ei-modal-message">${message}</div>
1209 - </div>
1210 - <div class="ei-modal-actions">
1211 - <button type="button" class="ei-modal-btn ei-modal-btn-secondary" id="modal-cancel">${cancelText}</button>
1212 - <button type="button" class="ei-modal-btn ${confirmBtnClass}" id="modal-confirm">${confirmText}</button>
1213 - </div>
1214 - `;
1215 -
1216 - overlay.appendChild(modal);
1217 - document.body.appendChild(overlay);
1218 -
1219 - // Show modal with animation
1220 - setTimeout(() => {
1221 - overlay.classList.add('show');
1222 - modal.classList.add('show');
1223 - }, 10);
1224 -
1225 - // Handle button clicks
1226 - const confirmBtn = modal.querySelector('#modal-confirm');
1227 - const cancelBtn = modal.querySelector('#modal-cancel');
1228 -
1229 - confirmBtn.addEventListener('click', function() {
1230 - const originalText = confirmBtn.innerHTML;
1231 - setButtonLoading(confirmBtn, confirmBtn.textContent.trim());
1232 - onConfirm(modal, confirmBtn, originalText);
1233 - });
1234 -
1235 - cancelBtn.addEventListener('click', function() {
1236 - hideModal(overlay);
1237 - });
1238 -
1239 - // Handle overlay click to close
1240 - overlay.addEventListener('click', function(e) {
1241 - if (e.target === overlay) {
1242 - hideModal(overlay);
1243 - }
1244 - });
1245 -
1246 - // Handle escape key
1247 - const handleEscape = function(e) {
1248 - if (e.key === 'Escape') {
1249 - hideModal(overlay);
1250 - document.removeEventListener('keydown', handleEscape);
1251 - }
1252 - };
1253 - document.addEventListener('keydown', handleEscape);
1254 -
1255 - // Focus on confirm button
1256 - setTimeout(() => {
1257 - confirmBtn.focus();
1258 - }, 100);
1259 - }
1260 -
1261 - // Decline Modal with Reason Field
1262 - function showDeclineModalWithReason() {
1263 - // Remove existing modal if any
1264 - const existingModal = document.querySelector('.ei-modal-overlay');
1265 - if (existingModal) {
1266 - existingModal.remove();
1267 - }
1268 - // Create modal overlay
1269 - const overlay = document.createElement('div');
1270 - overlay.className = 'ei-modal-overlay';
1271 - // Create modal content
1272 - const modal = document.createElement('div');
1273 - modal.className = 'ei-modal';
1274 - let message = '<?php echo esc_js(__('Decline this quote?', 'easy-invoice')); ?>';
1275 - if (declinedMessage) {
1276 - message += '\n\n' + declinedMessage;
1277 - }
1278 - modal.innerHTML = `
1279 - <div class="ei-modal-header">
1280 - <div class="ei-modal-icon danger">
1281 - <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="none"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1282 - </div>
1283 - <h3 class="ei-modal-title"><?php echo esc_js($text_settings['decline_quote']); ?></h3>
1284 - </div>
1285 - <div class="ei-modal-body">
1286 - <div class="ei-modal-message">${message}</div>
1287 - <div class="ei-modal-reason-field">
1288 - <label for="decline-reason" class="ei-modal-label"><?php echo esc_js($text_settings['decline_reason']); ?>:</label>
1289 - <textarea id="decline-reason" class="ei-modal-textarea" rows="4" placeholder="<?php echo esc_js(__('Please provide a reason for declining this quote...', 'easy-invoice')); ?>" required></textarea>
1290 - </div>
1291 - </div>
1292 - <div class="ei-modal-actions">
1293 - <button type="button" class="ei-modal-btn ei-modal-btn-secondary" id="modal-cancel"><?php echo esc_js(__('Cancel', 'easy-invoice')); ?></button>
1294 - <button type="button" class="ei-modal-btn ei-modal-btn-danger" id="modal-confirm"><?php echo esc_js($text_settings['decline_quote']); ?></button>
1295 - </div>
1296 - `;
1297 - overlay.appendChild(modal);
1298 - document.body.appendChild(overlay);
1299 - setTimeout(() => {
1300 - overlay.classList.add('show');
1301 - modal.classList.add('show');
1302 - }, 10);
1303 - // Handle button clicks
1304 - const confirmBtn = modal.querySelector('#modal-confirm');
1305 - const cancelBtn = modal.querySelector('#modal-cancel');
1306 - const reasonField = modal.querySelector('#decline-reason');
1307 - confirmBtn.addEventListener('click', function() {
1308 - const reason = reasonField.value.trim();
1309 - if (!reason) {
1310 - reasonField.focus();
1311 - reasonField.classList.add('error');
1312 - return;
1313 - }
1314 - const originalText = confirmBtn.innerHTML;
1315 - setButtonLoading(confirmBtn, confirmBtn.textContent.trim());
1316 - handleDeclineQuote(quoteId, confirmBtn, reason, originalText);
1317 - });
1318 - cancelBtn.addEventListener('click', function() {
1319 - hideModal(overlay);
1320 - });
1321 - overlay.addEventListener('click', function(e) {
1322 - if (e.target === overlay) {
1323 - hideModal(overlay);
1324 - }
1325 - });
1326 - const handleEscape = function(e) {
1327 - if (e.key === 'Escape') {
1328 - hideModal(overlay);
1329 - document.removeEventListener('keydown', handleEscape);
1330 - }
1331 - };
1332 - document.addEventListener('keydown', handleEscape);
1333 - setTimeout(() => {
1334 - reasonField.focus();
1335 - }, 100);
1336 - reasonField.addEventListener('input', function() {
1337 - this.classList.remove('error');
1338 - });
1339 - }
1340 -
1341 - function hideModal(overlay) {
1342 - const modal = overlay.querySelector('.ei-modal');
1343 - modal.classList.remove('show');
1344 - overlay.classList.remove('show');
1345 -
1346 - setTimeout(() => {
1347 - if (overlay.parentNode) {
1348 - overlay.remove();
1349 - }
1350 - }, 300);
1351 - }
1352 -
1353 - // Show message in modal (used for AJAX responses)
1354 - function showModalMessage(type, message, onClose) {
1355 - // Remove existing modal if any
1356 - const existingModal = document.querySelector('.ei-modal-overlay');
1357 - if (existingModal) {
1358 - existingModal.remove();
1359 - }
1360 - // Create modal overlay
1361 - const overlay = document.createElement('div');
1362 - overlay.className = 'ei-modal-overlay';
1363 - // Create modal content
1364 - const modal = document.createElement('div');
1365 - modal.className = 'ei-modal';
1366 - const iconClass = type === 'success' ? 'info' : 'danger';
1367 - const icon = type === 'success'
1368 - ? '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#10b981"/><path d="M7 13l3 3 7-7" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
1369 - : '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#ef4444"/><path d="M12 7v4.5M12 16h.01" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
1370 - modal.innerHTML = `
1371 - <div class="ei-modal-header">
1372 - <div class="ei-modal-icon ${iconClass}">${icon}</div>
1373 - <h3 class="ei-modal-title">${type === 'success' ? '<?php echo esc_js(__('Success', 'easy-invoice')); ?>' : '<?php echo esc_js(__('Error', 'easy-invoice')); ?>'}</h3>
1374 - </div>
1375 - <div class="ei-modal-body">
1376 - <div class="ei-modal-message">${message}</div>
1377 - </div>
1378 - <div class="ei-modal-actions">
1379 - <button type="button" class="ei-modal-btn ei-modal-btn-primary" id="modal-close"><?php echo esc_js(__('Close', 'easy-invoice')); ?></button>
1380 - </div>
1381 - `;
1382 - overlay.appendChild(modal);
1383 - document.body.appendChild(overlay);
1384 - setTimeout(() => {
1385 - overlay.classList.add('show');
1386 - modal.classList.add('show');
1387 - }, 10);
1388 - const closeBtn = modal.querySelector('#modal-close');
1389 - closeBtn.addEventListener('click', function() {
1390 - hideModal(overlay);
1391 - if (onClose) onClose();
1392 - });
1393 - overlay.addEventListener('click', function(e) {
1394 - if (e.target === overlay) {
1395 - hideModal(overlay);
1396 - if (onClose) onClose();
1397 - }
1398 - });
1399 - const handleEscape = function(e) {
1400 - if (e.key === 'Escape') {
1401 - hideModal(overlay);
1402 - document.removeEventListener('keydown', handleEscape);
1403 - if (onClose) onClose();
1404 - }
1405 - };
1406 - document.addEventListener('keydown', handleEscape);
1407 - setTimeout(() => {
1408 - closeBtn.focus();
1409 - }, 100);
1410 - }
1411 - // Show loading state in modal
1412 - function showModalLoading(message) {
1413 - // Remove existing modal if any
1414 - const existingModal = document.querySelector('.ei-modal-overlay');
1415 - if (existingModal) {
1416 - existingModal.remove();
1417 - }
1418 - // Create modal overlay
1419 - const overlay = document.createElement('div');
1420 - overlay.className = 'ei-modal-overlay';
1421 - // Create modal content
1422 - const modal = document.createElement('div');
1423 - modal.className = 'ei-modal';
1424 - modal.innerHTML = `
1425 - <div class="ei-modal-header">
1426 - <div class="ei-modal-icon info">
1427 - <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="12" fill="#3b82f6"/><circle cx="12" cy="12" r="6" fill="#fff"/><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></svg>
1428 - </div>
1429 - <h3 class="ei-modal-title"><?php echo esc_js(__('Please wait...', 'easy-invoice')); ?></h3>
1430 - </div>
1431 - <div class="ei-modal-body">
1432 - <div class="ei-modal-message">${message}</div>
1433 - </div>
1434 - `;
1435 - overlay.appendChild(modal);
1436 - document.body.appendChild(overlay);
1437 - setTimeout(() => {
1438 - overlay.classList.add('show');
1439 - modal.classList.add('show');
1440 - }, 10);
1441 - }
1442 -
1443 - // Handle Accept Quote
1444 - function handleAcceptQuote(quoteId, confirmBtn, originalText) {
1445 - showModalLoading('<?php echo esc_js(__('Accepting quote...', 'easy-invoice')); ?>');
1446 - jQuery.ajax({
1447 - url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
1448 - type: 'POST',
1449 - data: {
1450 - action: 'easy_invoice_accept_quote',
1451 - quote_id: quoteId,
1452 - nonce: '<?php echo esc_js(wp_create_nonce('easy_invoice_quote_action_' . (int) $quote->getId())); ?>'
1453 - },
1454 - success: function(response) {
1455 - if (response.success) {
1456 - // Check if an invoice was created and redirect to it
1457 - if (response.data && response.data.invoice_id) {
1458 - // Use the PHP-generated URL (secure URL first, then regular URL)
1459 - let invoiceUrl = response.data.secure_url || response.data.invoice_url;
1460 -
1461 - if (invoiceUrl) {
1462 - showModalMessage('success', response.data.message || '<?php _e('Quote accepted successfully! Redirecting to invoice...', 'easy-invoice'); ?>', function() {
1463 - window.location.href = invoiceUrl;
1464 - });
1465 - } else {
1466 - // Fallback to reload if no URL available
1467 - showModalMessage('success', response.data.message || '<?php _e('Quote accepted successfully', 'easy-invoice'); ?>', function() { location.reload(); });
1468 - }
1469 - } else {
1470 - // No invoice created, just show success message
1471 - showModalMessage('success', response.data && response.data.message ? response.data.message : '<?php _e('Quote accepted successfully', 'easy-invoice'); ?>', function() { location.reload(); });
1472 - }
1473 - } else {
1474 - resetButtonLoading(confirmBtn, originalText);
1475 - showModalMessage('error', response.data || '<?php _e('Error accepting quote', 'easy-invoice'); ?>');
1476 - }
1477 - },
1478 - error: function() {
1479 - resetButtonLoading(confirmBtn, originalText);
1480 - showModalMessage('error', '<?php _e('Error connecting to server', 'easy-invoice'); ?>');
1481 - }
1482 - });
1483 - }
1484 -
1485 - // Handle Decline Quote
1486 - function handleDeclineQuote(quoteId, confirmBtn, reason = '', originalText) {
1487 - showModalLoading('<?php echo esc_js(__('Declining quote...', 'easy-invoice')); ?>');
1488 - const ajaxData = {
1489 - action: 'easy_invoice_decline_quote',
1490 - quote_id: quoteId,
1491 - nonce: '<?php echo esc_js(wp_create_nonce('easy_invoice_quote_action_' . (int) $quote->getId())); ?>'
1492 - };
1493 - if (reason) {
1494 - ajaxData.decline_reason = reason;
1495 - }
1496 - jQuery.ajax({
1497 - url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
1498 - type: 'POST',
1499 - data: ajaxData,
1500 - success: function(response) {
1501 - if (response.success) {
1502 - showModalMessage('success', response.data && response.data.message ? response.data.message : '<?php _e('Quote declined successfully', 'easy-invoice'); ?>', function() { location.reload(); });
1503 - } else {
1504 - resetButtonLoading(confirmBtn, originalText);
1505 - showModalMessage('error', response.data || '<?php _e('Error declining quote', 'easy-invoice'); ?>');
1506 - }
1507 - },
1508 - error: function() {
1509 - resetButtonLoading(confirmBtn, originalText);
1510 - showModalMessage('error', '<?php _e('Error connecting to server', 'easy-invoice'); ?>');
1511 - }
1512 - });
1513 - }
1514 -
1515 - // Functions moved to global scope above
1516 - });
1517 - </script>
1518 -
1519 - <script>
1520 - document.addEventListener('DOMContentLoaded', function() {
1521 - // Auto-download PDF if ?auto_download_pdf=1 is present
1522 - if (window.location.search.indexOf('auto_download_pdf=1') !== -1) {
1523 - setTimeout(function() {
1524 - if (typeof DocumentPdfGenerator !== 'undefined') {
1525 - const pdfGenerator = new DocumentPdfGenerator('quote');
1526 - pdfGenerator.generatePDF();
1527 - }
1528 - }, 800);
1529 - }
1530 - });
1531 - </script>
1532 -
1533 - <?php
1534 - // Load additional CSS for all users (not just admin)
1535 - $additional_css = get_post_meta($quote->getId(), '_easy_invoice_additional_css', true) ?: '';
1536 - if (!empty($additional_css)):
1537 - ?>
1538 - <style id="custom-quote-css">
1539 - <?php echo esc_html($additional_css); ?>
1540 - </style>
1541 - <?php endif; ?>
1542 -
1543 - <?php
1544 - // Add Additional CSS feature for admin users
1545 - if (is_user_logged_in() && current_user_can('manage_options')):
1546 - ?>
1547 - <div id="css-panel" style="display: none; position: fixed; top: 0; left: 0; width: 400px; height: 100vh; background: white; box-shadow: 2px 0 10px rgba(0,0,0,0.1); z-index: 1001; overflow-y: auto;">
1548 - <div style="padding: 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;">
1549 - <h3 style="margin: 0; font-size: 18px; font-weight: 600;">Additional CSS</h3>
1550 - <button type="button" id="close-css-panel" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280;">&times;</button>
1551 - </div>
1552 - <div style="padding: 20px;">
1553 - <div style="margin-bottom: 15px;">
1554 - <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom CSS for this quote:</label>
1555 - <textarea id="additional-css-textarea" style="width: calc(100% - 24px); height: 300px; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; resize: vertical; box-sizing: border-box;" placeholder="Enter your custom CSS here..."><?php echo esc_textarea($additional_css); ?></textarea>
1556 - </div>
1557 - <div style="display: flex; gap: 10px;">
1558 - <button type="button" id="save-css-btn" style="background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">Save CSS</button>
1559 - <button type="button" id="preview-css-btn" style="background: #6366f1; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">Preview</button>
1560 - <button type="button" id="clear-css-btn" style="background: #ef4444; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 500;">Clear</button>
1561 - </div>
1562 - <div id="css-status" style="margin-top: 15px; padding: 10px; border-radius: 4px; display: none;"></div>
1563 - </div>
1564 - </div>
1565 -
1566 - <div id="css-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;"></div>
1567 -
1568 - <script>
1569 - // Additional CSS functionality
1570 - document.addEventListener('DOMContentLoaded', function() {
1571 - const cssBtn = document.getElementById('additional-css-btn');
1572 - const cssPanel = document.getElementById('css-panel');
1573 - const closeBtn = document.getElementById('close-css-panel');
1574 - const overlay = document.getElementById('css-overlay');
1575 - const saveBtn = document.getElementById('save-css-btn');
1576 - const previewBtn = document.getElementById('preview-css-btn');
1577 - const clearBtn = document.getElementById('clear-css-btn');
1578 - const textarea = document.getElementById('additional-css-textarea');
1579 - const status = document.getElementById('css-status');
1580 -
1581 - if (!cssBtn) return; // Exit if CSS button doesn't exist
1582 -
1583 - let originalCSS = textarea.value;
1584 -
1585 - // Toggle panel
1586 - cssBtn.addEventListener('click', function() {
1587 - cssPanel.style.display = 'block';
1588 - overlay.style.display = 'block';
1589 - });
1590 -
1591 - // Close panel
1592 - function closePanel() {
1593 - cssPanel.style.display = 'none';
1594 - overlay.style.display = 'none';
1595 - }
1596 -
1597 - closeBtn.addEventListener('click', closePanel);
1598 - overlay.addEventListener('click', closePanel);
1599 -
1600 - // Save CSS
1601 - saveBtn.addEventListener('click', function() {
1602 - const css = textarea.value;
1603 -
1604 - // AJAX request to save CSS
1605 - fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
1606 - method: 'POST',
1607 - headers: {
1608 - 'Content-Type': 'application/x-www-form-urlencoded',
1609 - },
1610 - body: new URLSearchParams({
1611 - 'action': 'save_additional_css',
1612 - 'post_id': <?php echo $quote->getId(); ?>,
1613 - 'css': css,
1614 - 'nonce': '<?php echo wp_create_nonce('save_additional_css_nonce'); ?>'
1615 - })
1616 - })
1617 - .then(response => response.json())
1618 - .then(data => {
1619 - if (data.success) {
1620 - originalCSS = css;
1621 - status.textContent = 'CSS saved successfully!';
1622 - status.style.background = '#d1fae5';
1623 - status.style.color = '#065f46';
1624 - status.style.display = 'block';
1625 -
1626 - // Apply the saved CSS
1627 - applyCustomCSS(css);
1628 -
1629 - setTimeout(() => {
1630 - status.style.display = 'none';
1631 - }, 3000);
1632 - } else {
1633 - status.textContent = 'Error saving CSS: ' + data.message;
1634 - status.style.background = '#fee2e2';
1635 - status.style.color = '#991b1b';
1636 - status.style.display = 'block';
1637 - }
1638 - })
1639 - .catch(error => {
1640 - status.textContent = 'Error saving CSS: ' + error.message;
1641 - status.style.background = '#fee2e2';
1642 - status.style.color = '#991b1b';
1643 - status.style.display = 'block';
1644 - });
1645 - });
1646 -
1647 - // Preview CSS
1648 - previewBtn.addEventListener('click', function() {
1649 - const css = textarea.value;
1650 - applyCustomCSS(css);
1651 -
1652 - status.textContent = 'CSS preview applied';
1653 - status.style.background = '#dbeafe';
1654 - status.style.color = '#1e40af';
1655 - status.style.display = 'block';
1656 -
1657 - setTimeout(() => {
1658 - status.style.display = 'none';
1659 - }, 2000);
1660 - });
1661 -
1662 - // Clear CSS
1663 - clearBtn.addEventListener('click', function() {
1664 - if (confirm('Are you sure you want to clear all custom CSS?')) {
1665 - textarea.value = '';
1666 - applyCustomCSS('');
1667 -
1668 - status.textContent = 'CSS cleared';
1669 - status.style.background = '#fee2e2';
1670 - status.style.color = '#991b1b';
1671 - status.style.display = 'block';
1672 -
1673 - setTimeout(() => {
1674 - status.style.display = 'none';
1675 - }, 2000);
1676 - }
1677 - });
1678 -
1679 - // Apply custom CSS function
1680 - function applyCustomCSS(css) {
1681 - // Remove existing custom CSS
1682 - const existingStyle = document.getElementById('custom-quote-css');
1683 - if (existingStyle) {
1684 - existingStyle.remove();
1685 - }
1686 -
1687 - // Add new custom CSS if not empty
1688 - if (css.trim()) {
1689 - const style = document.createElement('style');
1690 - style.id = 'custom-quote-css';
1691 - style.textContent = css;
1692 - document.head.appendChild(style);
1693 - }
1694 - }
1695 -
1696 - // Apply saved CSS on page load
1697 - applyCustomCSS(originalCSS);
1698 - });
1699 - </script>
1700 - <?php endif; ?>
1701 -</body>
1702 -</html>