PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / assets / js / settings.js

settings.js in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at assets/js/settings.js

654 lines 26.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(function($) {
2 // Add CSS for drag and drop functionality
3 $('<style>')
4 .prop('type', 'text/css')
5 .html(`
6 .gateway-handle {
7 cursor: move;
8 transition: color 0.2s ease;
9 }
10 .gateway-handle:hover {
11 color: #6b7280 !important;
12 }
13 .gateway-item.dragging {
14 opacity: 0.8;
15 transform: rotate(2deg);
16 box-shadow: 0 10px 25px rgba(0,0,0,0.15);
17 z-index: 1000;
18 }
19 .gateway-item-placeholder {
20 background: #f3f4f6;
21 border: 2px dashed #d1d5db;
22 border-radius: 0.5rem;
23 height: 6rem;
24 margin: 1.25rem 0;
25 }
26 body.dragging-gateway .gateway-item:not(.dragging) {
27 transition: transform 0.2s ease;
28 }
29 body.dragging-gateway .gateway-item:not(.dragging):hover {
30 transform: translateY(-2px);
31 }
32 `)
33 .appendTo('head');
34
35
36 // Initialize Select2 for multiselect fields if Select2 is available
37 function initializeSelect2() {
38 if ($.fn.select2) {
39 $('select[multiple="multiple"]').each(function() {
40 if (!$(this).hasClass('select2-hidden-accessible')) {
41
42 var $field = $(this);
43 var fieldLabel = $.trim($('label[for="' + $field.attr('id') + '"]').first().text())
44 || $field.attr('placeholder')
45 || 'Select options';
46
47 $field.select2({
48 width: '100%',
49 placeholder: $field.attr('placeholder') || 'Select options',
50 closeOnSelect: false,
51 allowClear: true
52 });
53
54 // Name the search box Select2 builds for itself. Select2 4.0.13
55 // renders `.select2-search__field` with no label and no aria-label,
56 // so a screen reader announces it as an unnamed text field. On a
57 // multiselect that box lives inside the selection control and is
58 // present from the moment Select2 initialises — not only while the
59 // dropdown is open — so it is labelled here, and again on open to
60 // cover the search field Select2 renders into the dropdown itself.
61 var labelSearchFields = function ($root) {
62 $root.find('.select2-search__field')
63 .filter(function () { return !this.getAttribute('aria-label'); })
64 .attr('aria-label', fieldLabel);
65 };
66
67 labelSearchFields($field.next('.select2-container'));
68 $field.on('select2:open', function () {
69 labelSearchFields($('.select2-container--open'));
70 });
71 }
72 });
73 }
74 }
75
76 // Initialize Select2 on page load
77 initializeSelect2();
78
79 // Reinitialize Select2 after settings save
80 $(document).on('settingsSaved', function() {
81 setTimeout(function() {
82 initializeSelect2();
83 }, 500);
84 });
85
86 // Make gateways sortable
87 if ($("#sortable-gateways").length) {
88 $("#sortable-gateways").sortable({
89 handle: ".gateway-handle",
90 axis: "y",
91 placeholder: "gateway-item-placeholder bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg h-24",
92 tolerance: "pointer",
93 opacity: 0.8,
94 update: function(event, ui) {
95 // Update the order inputs when items are reordered
96 $('#sortable-gateways .gateway-item').each(function(index) {
97 $(this).find('input.gateway-order-input').val($(this).data('gateway-id'));
98 });
99
100 // Show a subtle indication that order was updated
101 if (typeof EasyInvoiceToast !== 'undefined') {
102 EasyInvoiceToast.info('Payment method order updated. Save settings to apply changes.', { duration: 3000 });
103 }
104 },
105 start: function(event, ui) {
106 ui.item.addClass('dragging');
107 $('body').addClass('dragging-gateway');
108 },
109 stop: function(event, ui) {
110 ui.item.removeClass('dragging');
111 $('body').removeClass('dragging-gateway');
112 }
113 }).disableSelection();
114 }
115
116 // Toggle gateway settings visibility based on checkbox
117 $('#settings-form').on('change', '.gateway-enable-checkbox', function() {
118 var settingsDiv = $(this).closest('.gateway-item').find('.gateway-settings');
119 if ($(this).is(':checked')) {
120 settingsDiv.removeClass('hidden');
121 } else {
122 settingsDiv.addClass('hidden');
123 }
124 });
125
126 // Initialize gateway settings visibility on page load
127 $('.gateway-enable-checkbox').each(function() {
128 var settingsDiv = $(this).closest('.gateway-item').find('.gateway-settings');
129 if ($(this).is(':checked')) {
130 settingsDiv.removeClass('hidden');
131 } else {
132 settingsDiv.addClass('hidden');
133 }
134 });
135
136 // Generic function to handle all conditional fields
137 function updateConditionalFields() {
138 $('.conditional-field').each(function() {
139 const field = $(this);
140 let shouldShow = true;
141
142 // Check all dependencies
143 field.find('.depends-on-*').each(function() {
144 const className = $(this).attr('class');
145 const matches = className.match(/depends-on-([^-]+)-([^-]+)/);
146 if (matches) {
147 const dependsKey = matches[1];
148 const dependsValue = matches[2];
149 const dependsField = $('select[name="settings[' + dependsKey + ']"], input[name="settings[' + dependsKey + ']"]');
150
151 if (dependsField.length && dependsField.val() !== dependsValue) {
152 shouldShow = false;
153 }
154 }
155 });
156
157 if (shouldShow) {
158 field.removeClass('hidden');
159 } else {
160 field.addClass('hidden');
161 }
162 });
163 }
164
165 // Initialize conditional field visibility on page load
166 updateConditionalFields();
167
168 // Update fields when any dependency field changes
169 $(document).on('change', 'select[name*="settings["], input[name*="settings["]', function() {
170 updateConditionalFields();
171 });
172
173 // Handle logo upload
174 $('.upload-logo-button').on('click', function(e) {
175 e.preventDefault();
176
177
178 const button = $(this);
179 const fieldId = button.attr('id').replace('upload_image_button_', '');
180 const previewId = '#' + fieldId + '-preview';
181 const inputId = '#' + fieldId;
182
183
184
185 if (typeof wp === 'undefined' || typeof wp.media === 'undefined') {
186 console.error('WordPress media library not loaded');
187 if (typeof EasyInvoiceToast !== 'undefined') {
188 EasyInvoiceToast.show('error', 'Media library not available. Please refresh the page and try again.');
189 }
190 return;
191 }
192
193 const frame = wp.media({
194 title: 'Select Company Logo',
195 button: { text: 'Use this logo' },
196 multiple: false
197 });
198
199 frame.on('select', function() {
200 const attachment = frame.state().get('selection').first().toJSON();
201 $(previewId).attr('src', attachment.url).removeClass('hidden');
202 $(inputId).val(attachment.url);
203 });
204
205 frame.open();
206 });
207
208 // Handle form submission
209 $('#save-settings, #save-settings-bottom').on('click', function(e) {
210 e.preventDefault();
211 const button = $(this);
212 const originalText = button.html();
213
214 // Sync wp_editor content before submission
215 if (typeof tinyMCE !== 'undefined') {
216 tinyMCE.triggerSave();
217 }
218
219 button.prop('disabled', true)
220 .html('<i class="fas fa-spinner fa-spin mr-2"></i>Saving...');
221 const formData = new FormData($('#settings-form')[0]);
222 formData.append('action', 'easy_invoice_save_settings');
223 formData.append('nonce', easyInvoiceSettings.nonce);
224 if (!$('input[name="settings[easy_invoice_payment_methods][]"]:checked').length) {
225 formData.append('settings[easy_invoice_payment_methods]', '');
226 }
227
228
229 $.ajax({
230 url: easyInvoiceSettings.ajaxurl,
231 type: 'POST',
232 data: formData,
233 processData: false,
234 contentType: false,
235 success: function(response) {
236
237 if (response.success) {
238 if (typeof EasyInvoiceToast !== 'undefined') {
239 EasyInvoiceToast.success('Settings saved successfully');
240 } else {
241 // Simple fallback if toast system is not available
242 alert('Settings saved successfully');
243 }
244
245 // Trigger settingsSaved event to reinitialize Select2
246 $(document).trigger('settingsSaved');
247 } else {
248 // Check for error message in different possible locations
249 let errorMessage = 'Error saving settings';
250 if (response.message) {
251 errorMessage = response.message;
252 } else if (response.data && response.data.message) {
253 errorMessage = response.data.message;
254 }
255
256 if (typeof EasyInvoiceToast !== 'undefined') {
257 EasyInvoiceToast.error(errorMessage);
258 } else {
259 alert(errorMessage);
260 }
261 }
262 button.prop('disabled', false).html(originalText);
263 },
264 error: function(xhr, status, error) {
265 console.error('Settings save AJAX error:', {xhr, status, error});
266
267 let errorMessage = 'Error saving settings';
268 if (xhr.responseJSON && xhr.responseJSON.message) {
269 errorMessage = xhr.responseJSON.message;
270 } else if (xhr.responseText) {
271 try {
272 const response = JSON.parse(xhr.responseText);
273 if (response.message) {
274 errorMessage = response.message;
275 }
276 } catch (e) {
277 console.error('Failed to parse error response:', e);
278 }
279 }
280
281 if (typeof EasyInvoiceToast !== 'undefined') {
282 EasyInvoiceToast.error(errorMessage);
283 } else {
284 alert(errorMessage);
285 }
286 button.prop('disabled', false).html(originalText);
287 }
288 });
289 });
290
291 // Handle regenerate quote numbers functionality
292 $(document).on('click', '.regenerate-quote-numbers-button', function(e) {
293 e.preventDefault();
294 const button = $(this);
295 const originalText = button.html();
296
297 // Check if EasyInvoiceConfirmation is available
298 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.confirmAction) {
299
300 try {
301 EasyInvoiceConfirmation.confirmAction(
302 'regenerate',
303 'all quote numbers',
304 function() {
305 // User confirmed, proceed with regeneration
306 performQuoteRegeneration(button, originalText);
307 }
308 );
309 } catch (error) {
310 console.error('Error calling EasyInvoiceConfirmation.confirmAction:', error);
311 }
312 } else {
313 // Fallback to browser confirm
314 if (confirm('This will regenerate all quote numbers starting from the Next Quote Number. This action cannot be undone. Are you sure you want to continue?')) {
315 performQuoteRegeneration(button, originalText);
316 }
317 }
318 });
319
320 function performQuoteRegeneration(button, originalText) {
321 button.prop('disabled', true).html('Regenerating...');
322
323 $.ajax({
324 url: easyInvoiceSettings.ajaxurl,
325 type: 'POST',
326 data: {
327 action: 'regenerate_quote_numbers',
328 nonce: easyInvoiceSettings.nonce,
329 easy_invoice_settings_nonce: $('input[name="easy_invoice_settings_nonce"]').val()
330 },
331 success: function(response) {
332 if (response.success) {
333 if (typeof EasyInvoiceToast !== 'undefined') {
334 EasyInvoiceToast.success(response.data.message || 'Quote numbers regenerated successfully');
335 } else {
336 alert(response.data.message || 'Quote numbers regenerated successfully');
337 }
338
339 // Update the Next Quote Number field if provided
340 if (response.data.next_number) {
341 $('input[name="settings[easy_invoice_next_quote_number]"]').val(response.data.next_number);
342 }
343
344 // Reload the page after a short delay to show the updated numbers
345 setTimeout(function() {
346 location.reload();
347 }, 2000);
348 } else {
349 if (typeof EasyInvoiceToast !== 'undefined') {
350 EasyInvoiceToast.error(response.data.message || 'Failed to regenerate quote numbers');
351 } else {
352 alert(response.data.message || 'Failed to regenerate quote numbers');
353 }
354 }
355 button.prop('disabled', false).html(originalText);
356 },
357 error: function(xhr, status, error) {
358 console.error('AJAX Error:', error);
359 console.error('Status:', status);
360 console.error('Response:', xhr.responseText);
361 if (typeof EasyInvoiceToast !== 'undefined') {
362 EasyInvoiceToast.error('Failed to regenerate quote numbers. Please try again.');
363 } else {
364 alert('Failed to regenerate quote numbers. Please try again.');
365 }
366 button.prop('disabled', false).html(originalText);
367 }
368 });
369 }
370
371 // Handle test email functionality
372 $(document).on('click', '.test-email-button', function(e) {
373 e.preventDefault();
374 const button = $(this);
375 const originalText = button.html();
376 const emailInput = button.closest('.mt-1').find('input[type="email"]');
377 const resultDiv = button.closest('.mt-1').find('[id$="_result"]');
378 const testEmail = emailInput.val().trim();
379
380 if (!testEmail) {
381 if (typeof EasyInvoiceToast !== 'undefined') {
382 EasyInvoiceToast.show('error', 'Please enter a valid email address');
383 }
384 return;
385 }
386
387 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(testEmail)) {
388 if (typeof EasyInvoiceToast !== 'undefined') {
389 EasyInvoiceToast.show('error', 'Please enter a valid email address');
390 }
391 return;
392 }
393
394 button.prop('disabled', true)
395 .html('<i class="fas fa-spinner fa-spin mr-2"></i>Sending...');
396
397 $.ajax({
398 url: easyInvoiceSettings.ajaxurl,
399 type: 'POST',
400 data: {
401 action: 'easy_invoice_test_email',
402 test_email: testEmail,
403 nonce: easyInvoiceSettings.nonce
404 },
405 success: function(response) {
406 if (response.success) {
407 resultDiv.html('<div class="text-green-600 text-sm"><i class="fas fa-check-circle mr-1"></i>' + response.data.message + '</div>')
408 .show();
409 } else {
410 resultDiv.html('<div class="text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i>' + (response.data && response.data.message ? response.data.message : 'Failed to send test email') + '</div>')
411 .show();
412 }
413 button.prop('disabled', false).html(originalText);
414
415 // Hide result after 5 seconds
416 setTimeout(function() {
417 resultDiv.fadeOut();
418 }, 5000);
419 },
420 error: function() {
421 resultDiv.html('<div class="text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i>Error sending test email</div>')
422 .show();
423 button.prop('disabled', false).html(originalText);
424
425 // Hide result after 5 seconds
426 setTimeout(function() {
427 resultDiv.fadeOut();
428 }, 5000);
429 }
430 });
431 });
432
433 // Handle test template email functionality
434 $(document).on('click', '.test-template-email-button', function(e) {
435 e.preventDefault();
436 const button = $(this);
437 const originalText = button.html();
438 const emailInput = button.closest('.mt-1').find('input[type="email"]');
439 const resultDiv = button.closest('.mt-1').find('[id$="_result"]');
440 const testEmail = emailInput.val().trim();
441 const templateType = button.data('template-type');
442
443 if (!testEmail) {
444 if (typeof EasyInvoiceToast !== 'undefined') {
445 EasyInvoiceToast.show('error', 'Please enter a valid email address');
446 }
447 return;
448 }
449
450 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(testEmail)) {
451 if (typeof EasyInvoiceToast !== 'undefined') {
452 EasyInvoiceToast.show('error', 'Please enter a valid email address');
453 }
454 return;
455 }
456
457 button.prop('disabled', true)
458 .html('<i class="fas fa-spinner fa-spin mr-2"></i>Testing...');
459
460 $.ajax({
461 url: easyInvoiceSettings.ajaxurl,
462 type: 'POST',
463 data: {
464 action: 'easy_invoice_test_template_email',
465 test_email: testEmail,
466 template_type: templateType,
467 nonce: easyInvoiceSettings.nonce
468 },
469 success: function(response) {
470 if (response.success) {
471 resultDiv.html('<div class="text-green-600 text-sm"><i class="fas fa-check-circle mr-1"></i>' + response.data.message + '</div>')
472 .show();
473 } else {
474 resultDiv.html('<div class="text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i>' + (response.data && response.data.message ? response.data.message : 'Failed to send template test email') + '</div>')
475 .show();
476 }
477 button.prop('disabled', false).html(originalText);
478
479 // Hide result after 5 seconds
480 setTimeout(function() {
481 resultDiv.fadeOut();
482 }, 5000);
483 },
484 error: function() {
485 resultDiv.html('<div class="text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i>Error sending template test email</div>')
486 .show();
487 button.prop('disabled', false).html(originalText);
488
489 // Hide result after 5 seconds
490 setTimeout(function() {
491 resultDiv.fadeOut();
492 }, 5000);
493 }
494 });
495 });
496
497 // Handle test payment reminder email functionality
498 $(document).on('click', '.test-payment-reminder-email-button', function(e) {
499 e.preventDefault();
500 const button = $(this);
501 const originalText = button.html();
502 const emailInput = button.closest('.mt-1').find('input[type="email"]');
503 const resultDiv = button.closest('.mt-1').find('[id$="_result"]');
504 const testEmail = emailInput.val().trim();
505
506 if (!testEmail) {
507 if (typeof EasyInvoiceToast !== 'undefined') {
508 EasyInvoiceToast.show('error', 'Please enter a valid email address');
509 }
510 return;
511 }
512
513 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(testEmail)) {
514 if (typeof EasyInvoiceToast !== 'undefined') {
515 EasyInvoiceToast.show('error', 'Please enter a valid email address');
516 }
517 return;
518 }
519
520 button.prop('disabled', true)
521 .html('<i class="fas fa-spinner fa-spin mr-2"></i>Testing...');
522
523 $.ajax({
524 url: easyInvoiceSettings.ajaxurl,
525 type: 'POST',
526 data: {
527 action: 'easy_invoice_test_payment_reminder_email',
528 test_email: testEmail,
529 nonce: easyInvoiceSettings.nonce
530 },
531 success: function(response) {
532 if (response.success) {
533 resultDiv.html('<div class="text-green-600 text-sm"><i class="fas fa-check-circle mr-1"></i>' + response.data.message + '</div>')
534 .show();
535 } else {
536 resultDiv.html('<div class="text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i>' + (response.data && response.data.message ? response.data.message : 'Failed to send test payment reminder email') + '</div>')
537 .show();
538 }
539 button.prop('disabled', false).html(originalText);
540
541 // Hide result after 5 seconds
542 setTimeout(function() {
543 resultDiv.fadeOut();
544 }, 5000);
545 },
546 error: function() {
547 resultDiv.html('<div class="text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i>Error sending test payment reminder email</div>')
548 .show();
549 button.prop('disabled', false).html(originalText);
550
551 // Hide result after 5 seconds
552 setTimeout(function() {
553 resultDiv.fadeOut();
554 }, 5000);
555 }
556 });
557 });
558
559 // Handle regenerate invoice numbers functionality
560 $(document).on('click', '.regenerate-invoice-numbers-button', function(e) {
561 e.preventDefault();
562 const button = $(this);
563 const originalText = button.html();
564
565
566 // Show confirmation dialog
567 if (typeof EasyInvoiceConfirmation !== 'undefined') {
568 try {
569 EasyInvoiceConfirmation.confirmAction(
570 'regenerate',
571 'all invoice numbers',
572 function() {
573 // User confirmed, proceed with regeneration
574 performInvoiceRegeneration(button, originalText);
575 }
576 );
577 } catch (error) {
578 console.error('Error calling EasyInvoiceConfirmation.confirmAction:', error);
579 }
580 } else {
581 // Fallback to browser confirm
582 if (confirm('This will regenerate all invoice numbers starting from the Last Invoice Number. This action cannot be undone. Are you sure you want to continue?')) {
583 performInvoiceRegeneration(button, originalText);
584 }
585 }
586 });
587
588 // Function to perform the actual regeneration
589 function performInvoiceRegeneration(button, originalText) {
590 button.prop('disabled', true)
591 .html('<i class="fas fa-spinner fa-spin mr-2"></i>Regenerating...');
592
593
594 $.ajax({
595 url: easyInvoiceSettings.ajaxurl,
596 type: 'POST',
597 data: {
598 action: 'regenerate_invoice_numbers',
599 nonce: easyInvoiceSettings.nonce,
600 easy_invoice_settings_nonce: $('input[name="easy_invoice_settings_nonce"]').val()
601 },
602 success: function(response) {
603 if (response.success) {
604 if (typeof EasyInvoiceToast !== 'undefined') {
605 EasyInvoiceToast.success(response.data.message);
606 } else {
607 alert(response.data.message);
608 }
609
610 // Update the Next Invoice Number field if provided
611 if (response.data.next_number) {
612 $('input[name="settings[easy_invoice_next_invoice_number]"]').val(response.data.next_number);
613 }
614
615 // Refresh the page to show updated values
616 setTimeout(function() {
617 location.reload();
618 }, 2000);
619 } else {
620 if (typeof EasyInvoiceToast !== 'undefined') {
621 EasyInvoiceToast.error(response.data.message || 'Failed to regenerate invoice numbers');
622 } else {
623 alert(response.data.message || 'Failed to regenerate invoice numbers');
624 }
625 }
626 button.prop('disabled', false).html(originalText);
627 },
628 error: function(xhr, status, error) {
629 console.error('Invoice regeneration AJAX error:', {xhr, status, error});
630
631 let errorMessage = 'Error regenerating invoice numbers';
632 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
633 errorMessage = xhr.responseJSON.data.message;
634 } else if (xhr.responseText) {
635 try {
636 const response = JSON.parse(xhr.responseText);
637 if (response.data && response.data.message) {
638 errorMessage = response.data.message;
639 }
640 } catch (e) {
641 console.error('Failed to parse error response:', e);
642 }
643 }
644
645 if (typeof EasyInvoiceToast !== 'undefined') {
646 EasyInvoiceToast.error(errorMessage);
647 } else {
648 alert(errorMessage);
649 }
650 button.prop('disabled', false).html(originalText);
651 }
652 });
653 }
654 });