PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.14
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.14
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 / invoice-builder.js

invoice-builder.js in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.1.14, at assets/js/invoice-builder.js

1,398 lines 62.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Invoice Builder for Easy Invoice
3 * Handles invoice items, client selection, and form management
4 */
5
6 (function($) {
7 'use strict';
8
9 // Check if we're on an invoice page
10 const isInvoicePage = window.location.href.includes('easy-invoice-builder') ||
11 window.location.href.includes('easy-invoice-new') ||
12 $('.invoice-item').length > 0;
13
14 if (!isInvoicePage) {
15 return;
16 }
17
18 // Invoice Builder object
19 window.EasyInvoiceBuilder = {
20 // Default values
21 settings: {
22 itemCounter: 0,
23 items: [],
24 editMode: false,
25 invoiceId: 0
26 },
27
28 // Initialize the invoice builder
29 init: function() {
30 // Initialize field helpers for dynamic field handling
31 this.initFieldHelpers();
32
33 // Set up event handlers
34 this.setupEventHandlers();
35
36 // Initialize existing items
37 this.initializeExistingItems();
38
39 // Set up payment manager
40 if (window.EasyInvoicePayment) {
41 window.EasyInvoicePayment.init();
42 }
43 },
44
45 // Set up event handlers for invoice-related elements
46 setupEventHandlers: function() {
47 var self = this;
48
49 // Add item button
50 // First unbind any existing click handlers to prevent duplication
51 $('.add-item-button').off('click').on('click', function(e) {
52 e.preventDefault();
53 self.addNewItem();
54 });
55
56 // Collapse all items button - use event delegation since it might be in a hidden tab
57 $(document).off('click', '#collapse-all-items').on('click', '#collapse-all-items', function(e) {
58 e.preventDefault();
59 e.stopPropagation(); // Prevent event bubbling
60 self.collapseAllItems();
61 });
62
63 // Add sample items button
64 // $('#add-sample-items').off('click').on('click', function(e) {
65 // e.preventDefault();
66 // e.stopPropagation(); // Prevent event bubbling
67 // self.addSampleItems();
68 // });
69
70 // Individual item sample data buttons (delegate to handle dynamically added buttons)
71 $(document).off('click', '.fill-sample-data-btn').on('click', '.fill-sample-data-btn', function(e) {
72 e.preventDefault();
73 e.stopPropagation();
74 var $item = $(this).closest('.invoice-item');
75 self.fillItemWithSampleData($item);
76 });
77
78 // Individual item collapse toggles - use event delegation as fallback
79 $(document).off('click', '.item-collapse-toggle').on('click', '.item-collapse-toggle', function(e) {
80 e.preventDefault();
81 e.stopPropagation();
82
83 var $item = $(this).closest('.invoice-item');
84
85 var itemContent = $item.find('.item-content');
86 var summaryElement = $item.find('.item-collapsed-summary');
87 var sampleButton = $item.find('.fill-sample-data-btn');
88 var icon = $(this).find('i');
89
90 if (itemContent.is(':visible')) {
91 // Collapsing - update summary and change icon
92 self.updateItemSummary($item);
93 itemContent.slideUp(200);
94 summaryElement.slideDown(200);
95 sampleButton.hide(); // Hide sample button when collapsed
96 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
97 // Add compact styling to the collapsed item
98 $item.addClass('collapsed-item');
99 } else {
100 // Expanding - hide summary and change icon
101 itemContent.slideDown(200);
102 summaryElement.slideUp(200);
103 sampleButton.show(); // Show sample button when expanded
104 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
105 // Remove compact styling from the expanded item
106 $item.removeClass('collapsed-item');
107 }
108
109 // Update the preview to reflect changes
110 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
111 window.EasyInvoicePayment.updateTotals();
112 } else if (typeof updatePreview === 'function') {
113 updatePreview();
114 }
115 });
116
117 // Send invoice button
118 $('#send_invoice').off('click').on('click', function(e) {
119 e.preventDefault();
120 self.sendInvoice();
121 });
122
123 // Reset form button
124 $('#reset_form').off('click').on('click', function(e) {
125 e.preventDefault();
126 if (confirm('Are you sure you want to reset the form? All unsaved changes will be lost.')) {
127 self.resetForm();
128 }
129 });
130
131 // Handle tab navigation
132 $('.tab-button').off('click').on('click', function(e) {
133 e.preventDefault();
134 var targetTab = $(this).data('tab');
135
136 // Hide all tabs
137 $('.tab-content').removeClass('active').addClass('hidden');
138
139 // Remove active class and reset border styling for all tabs
140 $('.tab-button').removeClass('active')
141 .removeClass('border-indigo-500 text-indigo-600')
142 .addClass('border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300');
143
144 // Show the target tab
145 $('#' + targetTab).removeClass('hidden').addClass('active');
146
147 // Add active class and update border styling to clicked tab
148 $(this).addClass('active')
149 .removeClass('border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300')
150 .addClass('border-indigo-500 text-indigo-600');
151
152 // If switching to items tab, ensure collapse button is properly bound
153 if (targetTab === 'items-tab') {
154 setTimeout(function() {
155 if ($('#collapse-all-items').length > 0) {
156 // Re-attach event handler to ensure it works
157 $('#collapse-all-items').off('click').on('click', function(e) {
158 e.preventDefault();
159 e.stopPropagation();
160 EasyInvoiceBuilder.collapseAllItems();
161 });
162 } else {
163 // Collapse button not found in items tab
164 }
165 }, 100);
166 }
167 });
168 },
169
170 // Set up proper styling for the initially active tab
171 setupInitialTabState: function() {
172 // Find the tab that has the 'active' class
173 var $activeTab = $('.tab-button.active');
174
175 // If no active tab is found, default to the first tab
176 if ($activeTab.length === 0) {
177 $activeTab = $('.tab-button').first();
178 $activeTab.addClass('active');
179 }
180
181 // Apply the correct styling to the active tab
182 $activeTab.removeClass('border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300')
183 .addClass('border-indigo-500 text-indigo-600');
184
185 // Show the corresponding tab content
186 var targetTab = $activeTab.data('tab');
187 $('.tab-content').removeClass('active').addClass('hidden');
188 $('#' + targetTab).removeClass('hidden').addClass('active');
189 },
190
191 // Initialize invoice items from saved data or create a default empty item
192 // REMOVED: Items should be initialized from PHP/HTML, not JavaScript
193
194 // Add a new empty item
195 addNewItem: function() {
196
197 // Get the template
198 var template = document.getElementById('invoice-item-template');
199 if (!template) {
200 // Invoice item template not found
201 return null;
202 }
203
204 // Clone the template
205 var clone = template.content.cloneNode(true);
206 var newItem = $(clone);
207
208 // Get the next item index
209 var itemIndex = this.settings.itemCounter++;
210
211 // Generate a unique ID for the item
212 var itemId = 'item_' + Date.now() + '_' + itemIndex;
213
214 // Update item ID
215 newItem.find('.invoice-item').attr('id', itemId);
216
217 // Update field indices to use the correct item index
218 this.updateItemFieldIndices(newItem, itemIndex);
219
220 // Set header to 'New Item' for newly added items
221 newItem.find('h3').text('New Item');
222
223 // Add the item to the container
224 $('.invoice-items-container').append(newItem);
225
226 // Set up event handlers
227 this.setupItemEvents($('#' + itemId));
228
229 this.updateItemNumbers();
230
231 // Update totals
232 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
233 window.EasyInvoicePayment.updateTotals();
234 }
235
236 return $('#' + itemId);
237 },
238
239 // Update field indices in an item to use the correct item index
240 updateItemFieldIndices: function($item, itemIndex) {
241
242 // Update all input fields - match both items[0][fieldname] and items[-1][fieldname]
243 $item.find('input[name*="items["]').each(function() {
244 var oldName = $(this).attr('name');
245 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + itemIndex + '][');
246 $(this).attr('name', newName);
247 });
248
249 // Update all textarea fields - match both items[0][fieldname] and items[-1][fieldname]
250 $item.find('textarea[name*="items["]').each(function() {
251 var oldName = $(this).attr('name');
252 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + itemIndex + '][');
253 $(this).attr('name', newName);
254 });
255
256 // Update all select fields - match both items[0][fieldname] and items[-1][fieldname]
257 $item.find('select[name*="items["]').each(function() {
258 var oldName = $(this).attr('name');
259 var newName = oldName.replace(/items\[(?:-1|0)\]\[/, 'items[' + itemIndex + '][');
260 $(this).attr('name', newName);
261 });
262
263 // Update field IDs to be unique (handle both _-1 and _0)
264 $item.find('[id*="_-1"], [id*="_0"]').each(function() {
265 var oldId = $(this).attr('id');
266 var newId = oldId.replace(/_(?:-1|0)/, '_' + itemIndex);
267 $(this).attr('id', newId);
268 // Update corresponding label for attribute
269 var $label = $item.find('label[for="' + oldId + '"]');
270 if ($label.length) {
271 $label.attr('for', newId);
272 }
273 });
274 },
275
276 // Set up event handlers for a specific item
277 setupItemEvents: function($item) {
278 var self = this;
279
280 // Handle quantity, price, and adjustment percentage changes
281 $item.find('input[name*="[quantity]"], input[name*="[price]"], input[name*="[adjust_percentage]"]').off('input').on('input', function() {
282 self.calculateItemTotal($item);
283 });
284
285 // Remove item button
286 $item.find('.remove-item').off('click').on('click', function() {
287 self.removeItem($item);
288 });
289
290 // Handle taxable checkbox
291 $item.find('input[name*="[taxable]"]').off('change').on('change', function() {
292 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
293 window.EasyInvoicePayment.updateTotals();
294 }
295 });
296
297 // Handle collapse/expand - unbind previous handlers first
298 var $collapseToggle = $item.find('.item-collapse-toggle');
299
300 if ($collapseToggle.length === 0) {
301 // No collapse toggle found for item
302 return;
303 }
304
305 $collapseToggle.off('click').on('click', function(e) {
306 e.preventDefault();
307 e.stopPropagation(); // Prevent event bubbling
308
309 var itemContent = $item.find('.item-content');
310 var summaryElement = $item.find('.item-collapsed-summary');
311 var sampleButton = $item.find('.fill-sample-data-btn');
312 var icon = $(this).find('i');
313
314 if (itemContent.is(':visible')) {
315 // Collapsing - update summary and change icon
316 self.updateItemSummary($item);
317 itemContent.slideUp(200);
318 summaryElement.slideDown(200);
319 sampleButton.hide(); // Hide sample button when collapsed
320 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
321 // Add compact styling to the collapsed item
322 $item.addClass('collapsed-item');
323 } else {
324 // Expanding - hide summary and change icon
325 itemContent.slideDown(200);
326 summaryElement.slideUp(200);
327 sampleButton.show(); // Show sample button when expanded
328 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
329 // Remove compact styling from the expanded item
330 $item.removeClass('collapsed-item');
331 }
332
333 // Update the preview to reflect changes
334 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
335 window.EasyInvoicePayment.updateTotals();
336 } else if (typeof updatePreview === 'function') {
337 updatePreview();
338 }
339 });
340
341 // Update the title in the header when title field changes
342 $item.find('input[name*="[title]"]').off('input').on('input', function() {
343 var title = $(this).val() || 'Invoice Item';
344 var shortTitle = title.length > 30 ? title.substring(0, 30) + '...' : title;
345 $item.find('h3').text(shortTitle);
346 });
347 },
348
349 // Calculate total for a specific item
350 calculateItemTotal: function($item) {
351 // Use dynamic field helpers for calculation
352 if (window.EasyInvoiceFieldHelpers && window.EasyInvoiceFieldHelpers.calculateFieldValue) {
353 var total = window.EasyInvoiceFieldHelpers.calculateFieldValue('total', $item);
354 if (total !== null) {
355 window.EasyInvoiceFieldHelpers.setFieldValue('total', total, $item);
356
357 // Update summary fields directly
358 var quantity = window.EasyInvoiceFieldHelpers.getFieldValue('quantity', $item);
359 var price = window.EasyInvoiceFieldHelpers.getFieldValue('price', $item);
360
361 $item.find('.quantity-summary').text(quantity || '0');
362 $item.find('.price-summary').text((parseFloat(price) || 0).toFixed(2));
363 $item.find('.total-summary').text((parseFloat(total) || 0).toFixed(2));
364 }
365 } else {
366 // Fallback to hardcoded calculation
367 var quantity = parseFloat($item.find('input[name*="[quantity]"]').val()) || 0;
368 var price = parseFloat($item.find('input[name*="[price]"]').val()) || 0;
369 var adjustPercentage = parseFloat($item.find('input[name*="[adjust_percentage]"]').val()) || 0;
370 var baseTotal = quantity * price;
371 var total = baseTotal * (1 + adjustPercentage / 100);
372
373 // Update the total field
374 var $totalField = $item.find('input[name*="[total]"]');
375 $totalField.val(total.toFixed(2));
376
377 // Update the collapsed summary
378 $item.find('.quantity-summary').text(quantity);
379 $item.find('.price-summary').text(price.toFixed(2));
380 $item.find('.total-summary').text(total.toFixed(2));
381 }
382 },
383
384 // Update the collapsed summary of an item
385 updateItemSummary: function($item) {
386 // Use dynamic field helpers for summary updates
387 if (window.EasyInvoiceFieldHelpers) {
388 var quantity = window.EasyInvoiceFieldHelpers.getFieldValue('quantity', $item);
389 var price = window.EasyInvoiceFieldHelpers.getFieldValue('price', $item);
390 var total = window.EasyInvoiceFieldHelpers.getFieldValue('total', $item);
391
392 // Update summary fields directly since updateSummaryFields doesn't exist
393 $item.find('.quantity-summary').text(quantity || '0');
394 $item.find('.price-summary').text((parseFloat(price) || 0).toFixed(2));
395 $item.find('.total-summary').text((parseFloat(total) || 0).toFixed(2));
396
397 } else {
398 // Fallback to hardcoded summary
399 var quantity = parseFloat($item.find('input[name*="[quantity]"]').val()) || 0;
400 var price = parseFloat($item.find('input[name*="[price]"]').val()) || 0;
401 var adjustPercentage = parseFloat($item.find('input[name*="[adjust_percentage]"]').val()) || 0;
402 var baseTotal = quantity * price;
403 var total = baseTotal * (1 + adjustPercentage / 100);
404
405 $item.find('.quantity-summary').text(quantity);
406 $item.find('.price-summary').text(price.toFixed(2));
407 $item.find('.total-summary').text(total.toFixed(2));
408
409 }
410 },
411
412 // Remove an item from the invoice
413 removeItem: function($item) {
414 var self = this;
415 $item.addClass('opacity-0');
416 setTimeout(function() {
417 $item.remove();
418
419 // Ensure at least one item remains
420 if ($('.invoice-item').length === 0) {
421 self.addNewItem();
422 } else {
423 self.updateItemNumbers();
424 }
425
426 // Update totals
427 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
428 window.EasyInvoicePayment.updateTotals();
429 }
430 }.bind(this), 200);
431 },
432
433 updateItemNumbers: function() {
434 var self = this;
435 $('.invoice-items-container .invoice-item').each(function(index) {
436 var itemNumber = index + 1;
437 var title = $(this).find('input[name*="[title]"]').val();
438 var shortTitle = title ? (title.length > 30 ? title.substring(0, 30) + '...' : title) : 'New Item';
439 $(this).find('h3').text(shortTitle);
440
441 // Only update field indices if they don't already match the current index
442 var $item = $(this);
443 var firstField = $item.find('input[name*="items["]').first();
444 if (firstField.length > 0) {
445 var fieldName = firstField.attr('name');
446 var currentIndex = fieldName.match(/items\[(\d+)\]/);
447 if (currentIndex && parseInt(currentIndex[1]) !== index) {
448 // Field index doesn't match, update it
449 self.updateItemFieldIndices($item, index);
450 }
451 }
452 });
453 self.settings.itemCounter = $('.invoice-items-container .invoice-item').length;
454 },
455
456 // Format currency value
457 formatCurrency: function(value) {
458 var symbol = '$';
459
460 // Use the currency symbol from payment manager if available
461 if (window.EasyInvoicePayment && window.EasyInvoicePayment.settings.currencySymbol) {
462 symbol = window.EasyInvoicePayment.settings.currencySymbol;
463 }
464
465 return symbol + parseFloat(value).toFixed(2);
466 },
467
468 // Get all current invoice items
469 getItems: function() {
470 var items = [];
471
472 $('.invoice-item').each(function(index) {
473 var $item = $(this);
474 var item = {
475 id: $item.attr('id')
476 };
477
478 // Use dynamic field helpers to get all field values
479 if (window.EasyInvoiceFieldHelpers && window.EasyInvoiceFieldConfig) {
480 Object.keys(window.EasyInvoiceFieldConfig).forEach(function(fieldName) {
481 var value = window.EasyInvoiceFieldHelpers.getFieldValue(fieldName, $item);
482 item[fieldName] = value;
483 });
484 } else {
485 // Fallback to hardcoded field names
486 item.name = $item.find('input[name*="[title]"]').val() || '';
487 item.description = $item.find('textarea[name*="[description]"]').val() || '';
488 item.quantity = parseFloat($item.find('input[name*="[quantity]"]').val()) || 0;
489 item.price = parseFloat($item.find('input[name*="[price]"]').val()) || 0;
490 item.taxable = $item.find('input[name*="[taxable]"]').is(':checked');
491 }
492
493 items.push(item);
494 });
495
496 return items;
497 },
498
499 // Set up client selection functionality
500 setupClientSelection: function() {
501 var self = this;
502 var isInitialLoad = true; // Flag to prevent AJAX calls on initial load
503
504 // Client dropdown change
505 $('#client_id').on('change', function() {
506 var clientId = $(this).val();
507
508 // Skip AJAX call if this is the initial load
509 if (isInitialLoad) {
510 isInitialLoad = false;
511 return;
512 }
513
514 if (clientId === 'new') {
515 // Show new client modal
516 $('#add_client_modal').show();
517 } else if (clientId !== '') {
518 // Load client data (ajax call or from already available data)
519 self.loadClientData(clientId);
520 }
521 });
522
523 // Close modal button
524 $('.close-modal').on('click', function() {
525 $('#add_client_modal').hide();
526
527 // Reset client dropdown if no client was selected
528 if ($('#client_id').val() === 'new') {
529 $('#client_id').val('');
530 }
531 });
532
533 // Submit new client form
534 $('#add_client_form').on('submit', function(e) {
535 e.preventDefault();
536 self.addNewClient();
537 });
538
539 // Reset the flag after a short delay to allow for user interactions
540 setTimeout(function() {
541 isInitialLoad = false;
542 }, 500);
543 },
544
545 // Load client data when a client is selected
546 loadClientData: function(clientId) {
547 // First check if we have client data already loaded from PHP
548 if (typeof easyInvoice !== 'undefined' && easyInvoice.clientData && easyInvoice.clientData.id == clientId) {
549 var client = easyInvoice.clientData;
550
551 // Update client info fields
552 $('#client_name_display').text(client.name || '');
553 $('#client_email_display').text(client.email || '');
554 $('#client_phone_display').text(client.phone || '');
555 $('#client_address_display').html((client.address || '').replace(/\n/g, '<br>'));
556
557 // Show client info section
558 $('#client_info').show();
559 return;
560 }
561
562 // For dynamic client selection (not initial load), make an AJAX call to get client data
563 $.ajax({
564 url: easyInvoice.ajaxUrl,
565 type: 'POST',
566 data: {
567 action: 'easy_invoice_get_client',
568 nonce: easyInvoice.nonce,
569 client_id: clientId
570 },
571 success: function(response) {
572 if (response.success) {
573 var client = response.data;
574
575 // Update client info fields
576 $('#client_name_display').text(client.name);
577 $('#client_email_display').text(client.email);
578 $('#client_phone_display').text(client.phone || '');
579 $('#client_address_display').html(client.address.replace(/\n/g, '<br>') || '');
580
581 // Show client info section
582 $('#client_info').show();
583 } else {
584 // Error loading client data
585 }
586 },
587 error: function(xhr, status, error) {
588 // AJAX error loading client data
589 }
590 });
591 },
592
593 // Add a new client via AJAX
594 addNewClient: function() {
595 var self = this;
596 var clientData = {
597 name: $('#new_client_name').val(),
598 email: $('#new_client_email').val(),
599 phone: $('#new_client_phone').val(),
600 address: $('#new_client_address').val(),
601 notes: $('#new_client_notes').val()
602 };
603
604 // Validate required fields
605 if (!clientData.name || !clientData.email) {
606 if (typeof EasyInvoiceToast !== 'undefined') {
607 EasyInvoiceToast.show('error', 'Client name and email are required.');
608 }
609 return;
610 }
611
612 // Send AJAX request to add client
613 $.ajax({
614 url: easyInvoice.ajaxUrl,
615 type: 'POST',
616 data: {
617 action: 'easy_invoice_add_client',
618 nonce: easyInvoice.nonce,
619 client_data: clientData
620 },
621 success: function(response) {
622 if (response.success) {
623 var newClient = response.data.client;
624 var newClientId = response.data.client_id;
625
626 // Add new client to dropdown
627 $('#client_id').append($('<option>', {
628 value: newClientId,
629 text: newClient.name
630 }));
631
632 // Select the new client
633 $('#client_id').val(newClientId);
634
635 // Load the client data
636 self.loadClientData(newClientId);
637
638 // Hide modal
639 $('#add_client_modal').hide();
640
641 // Clear form
642 $('#add_client_form')[0].reset();
643
644 // Show success message
645 if (typeof EasyInvoiceToast !== 'undefined') {
646 EasyInvoiceToast.show('success', 'Client added successfully!');
647 }
648 } else {
649 if (typeof EasyInvoiceToast !== 'undefined') {
650 EasyInvoiceToast.show('error', 'Error adding client: ' + response.data);
651 }
652 }
653 },
654 error: function(xhr, status, error) {
655 // AJAX error adding client
656 if (typeof EasyInvoiceToast !== 'undefined') {
657 EasyInvoiceToast.show('error', 'Error adding client. Please try again.');
658 }
659 }
660 });
661 },
662
663 // Save the invoice
664 saveInvoice: function() {
665 var self = this;
666
667 // Collect all form data at once
668 var formData = {};
669
670 // Get all form fields using serializeArray
671 $('#invoice-form').serializeArray().forEach(function(item) {
672 formData[item.name] = item.value;
673 });
674
675 // Add items data
676 formData.items = this.getItems();
677
678 // Only exclude invoice number for updates, not for new invoices
679 if (this.settings.editMode && this.settings.invoiceId > 0) {
680 delete formData['invoice-number'];
681 delete formData.invoice_number;
682 }
683
684 // Add invoice ID if in edit mode
685 if (this.settings.editMode && this.settings.invoiceId > 0) {
686 formData.invoice_id = this.settings.invoiceId;
687 }
688
689 // Add client ID if selected
690 var clientId = $('#client_id').val();
691 if (clientId && clientId !== '') {
692 formData.client_id = clientId;
693 }
694
695 // Show loading state
696 var $saveBtn = $('.save-invoice-btn');
697 var originalText = $saveBtn.text();
698 $saveBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-2"></i>Saving...');
699
700 // Send AJAX request to save invoice
701 $.ajax({
702 url: easyInvoice.ajaxUrl,
703 type: 'POST',
704 data: {
705 action: 'easy_invoice_save_invoice',
706 nonce: easyInvoice.nonce,
707 invoice_data: formData
708 },
709 success: function(response) {
710 // Restore button state
711 $saveBtn.prop('disabled', false).text(originalText);
712
713 if (response.success) {
714 // Show success message without page reload
715 self.showNotification('Invoice saved successfully!', 'success');
716
717 // Update invoice ID if it changed (for new invoices)
718 if (response.data.invoice_id) {
719 self.settings.invoiceId = response.data.invoice_id;
720 self.settings.editMode = true;
721
722 // Update the URL to reflect the invoice ID
723 if (window.history && window.history.pushState) {
724 var newUrl = window.location.href.split('?')[0] + '?invoice_id=' + response.data.invoice_id;
725 window.history.pushState({}, '', newUrl);
726 }
727 }
728
729 // Update any UI elements that depend on edit mode
730 self.updateUIForEditMode();
731
732 } else {
733 self.showNotification('Error saving invoice: ' + response.data, 'error');
734 }
735 },
736 error: function(xhr, status, error) {
737 // Restore button state
738 $saveBtn.prop('disabled', false).text(originalText);
739
740 // AJAX error saving invoice
741 self.showNotification('Error saving invoice. Please try again.', 'error');
742 }
743 });
744 },
745
746 // Send the invoice to the client
747 sendInvoice: function() {
748 // First save the invoice, then send it
749 var self = this;
750
751 // Collect all form data at once
752 var formData = {};
753
754 // Get all form fields using serializeArray
755 $('#invoice-form').serializeArray().forEach(function(item) {
756 formData[item.name] = item.value;
757 });
758
759 // Add items data
760 formData.items = this.getItems();
761
762 // Only exclude invoice number for updates, not for new invoices
763 if (this.settings.editMode && this.settings.invoiceId > 0) {
764 delete formData['invoice-number'];
765 delete formData.invoice_number;
766 }
767
768 // Add invoice ID if in edit mode
769 if (this.settings.editMode && this.settings.invoiceId > 0) {
770 formData.invoice_id = this.settings.invoiceId;
771 }
772
773 // Add client ID if selected
774 var clientId = $('#client_id').val();
775 if (clientId && clientId !== '') {
776 formData.client_id = clientId;
777 }
778
779 // Show loading state
780 var $sendBtn = $('.send-invoice-btn');
781 var originalText = $sendBtn.text();
782 $sendBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-2"></i>Sending...');
783
784 // Send AJAX request to save and send invoice
785 $.ajax({
786 url: easyInvoice.ajaxUrl,
787 type: 'POST',
788 data: {
789 action: 'easy_invoice_save_and_send_invoice',
790 nonce: easyInvoice.nonce,
791 invoice_data: formData
792 },
793 success: function(response) {
794 // Restore button state
795 $sendBtn.prop('disabled', false).text(originalText);
796
797 if (response.success) {
798 // Show success message without page reload
799 self.showNotification('Invoice sent successfully!', 'success');
800
801 // Update invoice ID if it changed (for new invoices)
802 if (response.data.invoice_id) {
803 self.settings.invoiceId = response.data.invoice_id;
804 self.settings.editMode = true;
805
806 // Update the URL to reflect the invoice ID
807 if (window.history && window.history.pushState) {
808 var newUrl = window.location.href.split('?')[0] + '?invoice_id=' + response.data.invoice_id;
809 window.history.pushState({}, '', newUrl);
810 }
811 }
812
813 // Update any UI elements that depend on edit mode
814 self.updateUIForEditMode();
815
816 } else {
817 self.showNotification('Error sending invoice: ' + response.data, 'error');
818 }
819 },
820 error: function(xhr, status, error) {
821 // Restore button state
822 $sendBtn.prop('disabled', false).text(originalText);
823
824 // AJAX error sending invoice
825 self.showNotification('Error sending invoice. Please try again.', 'error');
826 }
827 });
828 },
829
830 // Reset the form
831 resetForm: function() {
832 // Reset form fields
833 $('#invoice-form')[0].reset();
834
835 // Clear items
836 $('.invoice-items-container').empty();
837
838 // Add one empty item
839 this.addNewItem();
840
841 // Hide client info
842 $('#client_info').hide();
843
844 // Reset client dropdown
845 $('#client_id').val('');
846
847 // Reset payment settings if payment manager is available
848 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateUI === 'function') {
849 window.EasyInvoicePayment.updateUI();
850 }
851 },
852
853 // Collapse or expand all invoice items
854 collapseAllItems: function() {
855 var self = this;
856 var $button = $('#collapse-all-items');
857 var allCollapsed = true;
858
859 // Check if all items are already collapsed
860 $('.invoice-item').each(function() {
861 var $item = $(this);
862 var itemContent = $item.find('.item-content');
863 if (itemContent.is(':visible')) {
864 allCollapsed = false;
865 return false; // Break the loop if we find an expanded item
866 }
867 });
868
869 // Update button text based on current state
870 if (allCollapsed) {
871 // If all items are collapsed, expand them
872 $button.html('<i class="fas fa-chevron-down mr-1"></i> Collapse All');
873 $('.invoice-item').each(function() {
874 var $item = $(this);
875 var itemContent = $item.find('.item-content');
876 var summaryElement = $item.find('.item-collapsed-summary');
877 var sampleButton = $item.find('.fill-sample-data-btn');
878 var icon = $item.find('.item-collapse-toggle i');
879
880
881 // Only toggle if it's currently collapsed
882 if (!itemContent.is(':visible')) {
883 itemContent.slideDown(200);
884 summaryElement.slideUp(200);
885 sampleButton.show(); // Show sample button when expanded
886 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
887 // Remove collapsed item styling
888 $item.removeClass('collapsed-item');
889 }
890 });
891 } else {
892 // If any items are expanded, collapse them all
893 $button.html('<i class="fas fa-chevron-right mr-1"></i> Expand All');
894 $('.invoice-item').each(function() {
895 var $item = $(this);
896 var itemContent = $item.find('.item-content');
897 var summaryElement = $item.find('.item-collapsed-summary');
898 var sampleButton = $item.find('.fill-sample-data-btn');
899 var icon = $item.find('.item-collapse-toggle i');
900
901 // Only toggle if it's currently expanded
902 if (itemContent.is(':visible')) {
903 // Update the summary before collapsing
904 self.updateItemSummary($item);
905 itemContent.slideUp(200);
906 summaryElement.slideDown(200);
907 sampleButton.hide(); // Hide sample button when collapsed
908 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
909 // Add collapsed item styling
910 $item.addClass('collapsed-item');
911 }
912 });
913 }
914
915 // Update the preview to reflect changes
916 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
917 window.EasyInvoicePayment.updateTotals();
918 } else if (typeof updatePreview === 'function') {
919 updatePreview();
920 }
921 },
922
923 // Fill an item with sample data
924 fillItemWithSampleData: function($item) {
925
926 // Array of realistic sample data
927 var sampleItems = [
928 {
929 title: 'Web Development Services',
930 description: 'Custom website development including responsive design, SEO optimization, and content management system integration.',
931 quantity: 1,
932 price: 2500.00,
933 taxable: true
934 },
935 {
936 title: 'Logo Design Package',
937 description: 'Professional logo design with multiple concepts, revisions, and final files in various formats (AI, EPS, PNG, JPG).',
938 quantity: 1,
939 price: 450.00,
940 taxable: false
941 },
942 {
943 title: 'Monthly Website Maintenance',
944 description: 'Ongoing website maintenance including security updates, content updates, and technical support.',
945 quantity: 3,
946 price: 150.00,
947 taxable: true
948 },
949 {
950 title: 'SEO Optimization',
951 description: 'Search engine optimization services including keyword research, on-page optimization, and performance monitoring.',
952 quantity: 1,
953 price: 800.00,
954 taxable: true
955 },
956 {
957 title: 'Content Writing',
958 description: 'Professional content writing services including blog posts, website copy, and marketing materials.',
959 quantity: 5,
960 price: 75.00,
961 taxable: true
962 },
963 {
964 title: 'Social Media Management',
965 description: 'Monthly social media management including content creation, posting, and engagement monitoring.',
966 quantity: 1,
967 price: 300.00,
968 taxable: true
969 }
970 ];
971
972 // Pick a random sample item
973 var randomIndex = Math.floor(Math.random() * sampleItems.length);
974 var sampleData = sampleItems[randomIndex];
975
976 // Add dynamic sample values for any custom fields
977 if (window.EasyInvoiceFieldConfig) {
978 Object.keys(window.EasyInvoiceFieldConfig).forEach(function(fieldName) {
979 // Skip standard fields that are already in sampleData
980 if (!sampleData.hasOwnProperty(fieldName)) {
981 var fieldConfig = window.EasyInvoiceFieldConfig[fieldName];
982 var fieldType = fieldConfig.type;
983
984 // Generate appropriate sample value based on field type
985 switch (fieldType) {
986 case 'text':
987 sampleData[fieldName] = 'Sample ' + fieldName.replace(/_/g, ' ').replace(/\b\w/g, function(l) { return l.toUpperCase(); });
988 break;
989 case 'number':
990 sampleData[fieldName] = Math.floor(Math.random() * 100) + 1;
991 break;
992 case 'checkbox':
993 sampleData[fieldName] = Math.random() > 0.5 ? '1' : '0';
994 break;
995 case 'textarea':
996 sampleData[fieldName] = 'This is a sample value for ' + fieldName.replace(/_/g, ' ') + '.';
997 break;
998 default:
999 sampleData[fieldName] = 'Sample ' + fieldName;
1000 break;
1001 }
1002 }
1003 });
1004 }
1005
1006 // Set values using dynamic field helpers
1007 if (window.EasyInvoiceFieldHelpers) {
1008 Object.keys(sampleData).forEach(function(fieldName) {
1009 if (window.EasyInvoiceFieldConfig[fieldName]) {
1010 window.EasyInvoiceFieldHelpers.setFieldValue(fieldName, sampleData[fieldName], $item);
1011 }
1012 });
1013
1014 // Trigger input event for title field to update header
1015 $item.find('input[name*="[title]"]').trigger('input');
1016 } else {
1017 // Fallback to hardcoded field names
1018 $item.find('input[name*="[title]"]').val(sampleData.title).trigger('input');
1019 $item.find('textarea[name*="[description]"]').val(sampleData.description);
1020 $item.find('input[name*="[quantity]"]').val(sampleData.quantity);
1021 $item.find('input[name*="[price]"]').val(sampleData.price);
1022 $item.find('input[name*="[taxable]"]').prop('checked', sampleData.taxable);
1023 }
1024
1025 // Calculate total
1026 this.calculateItemTotal($item);
1027
1028 // Add a small delay to ensure total calculation is complete
1029 setTimeout(function() {
1030 // Re-calculate total to ensure it's correct
1031 // Store reference to the correct context
1032 var self = this;
1033 self.calculateItemTotal($item);
1034
1035 // Update totals
1036 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
1037 window.EasyInvoicePayment.updateTotals();
1038 }
1039 }.bind(this), 100);
1040
1041 },
1042
1043 // Load invoice data when in edit mode
1044 loadInvoiceData: function(invoiceData) {
1045
1046 // Set form fields using the correct field names from InvoiceFormManager
1047 $('#invoice-form').find('input[name="invoice_title"]').val(invoiceData.title || '');
1048 $('#invoice-form').find('input[name="invoice-number"]').val(invoiceData.number || '');
1049 $('#invoice-form').find('input[name="issue-date"]').val(invoiceData.issue_date || '');
1050 $('#invoice-form').find('input[name="due-date"]').val(invoiceData.due_date || '');
1051 $('#invoice-form').find('select[name="status"]').val(invoiceData.status || 'draft');
1052 $('#invoice-form').find('textarea[name="notes"]').val(invoiceData.notes || '');
1053
1054 // Set client info if available
1055 if (easyInvoice.clientData) {
1056 $('#client_name_display').text(easyInvoice.clientData.name || '');
1057 $('#client_email_display').text(easyInvoice.clientData.email || '');
1058 $('#client_address_display').html((easyInvoice.clientData.address || '').replace(/\n/g, '<br>'));
1059 $('#client_info').show();
1060 }
1061
1062 // Set items
1063 if (easyInvoice.invoiceItems && Array.isArray(easyInvoice.invoiceItems)) {
1064 this.settings.items = easyInvoice.invoiceItems;
1065 }
1066 },
1067
1068 // Show notification
1069 showNotification: function(message, type) {
1070 // Remove any existing notifications
1071 $('.easy-invoice-notification').remove();
1072
1073 // Create notification element
1074 var notification = $('<div class="easy-invoice-notification"></div>');
1075
1076 // Set notification content and styling
1077 var icon = type === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle';
1078 var bgColor = type === 'success' ? 'bg-green-50' : 'bg-red-50';
1079 var borderColor = type === 'success' ? 'border-green-200' : 'border-red-200';
1080 var textColor = type === 'success' ? 'text-green-800' : 'text-red-800';
1081 var iconColor = type === 'success' ? 'text-green-400' : 'text-red-400';
1082
1083 notification.html(`
1084 <div class="fixed top-4 right-4 z-50 max-w-sm w-full ${bgColor} border ${borderColor} rounded-lg shadow-lg p-4">
1085 <div class="flex items-start">
1086 <div class="flex-shrink-0">
1087 <i class="${icon} ${iconColor} text-lg"></i>
1088 </div>
1089 <div class="ml-3 w-0 flex-1">
1090 <p class="text-sm font-medium ${textColor}">${message}</p>
1091 </div>
1092 <div class="ml-4 flex-shrink-0 flex">
1093 <button class="notification-close bg-transparent rounded-md inline-flex text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
1094 <span class="sr-only">Close</span>
1095 <i class="fas fa-times"></i>
1096 </button>
1097 </div>
1098 </div>
1099 </div>
1100 `);
1101
1102 // Add to page
1103 $('body').append(notification);
1104
1105 // Auto-hide after 5 seconds
1106 setTimeout(function() {
1107 notification.fadeOut(300, function() {
1108 $(this).remove();
1109 });
1110 }, 5000);
1111
1112 // Handle close button
1113 notification.find('.notification-close').on('click', function() {
1114 notification.fadeOut(300, function() {
1115 $(this).remove();
1116 });
1117 });
1118 },
1119
1120 // Update UI for edit mode
1121 updateUIForEditMode: function() {
1122 // Update page title to show edit mode
1123 if (this.settings.editMode && this.settings.invoiceId > 0) {
1124 document.title = document.title.replace('New Invoice', 'Edit Invoice');
1125
1126 // Update any buttons or UI elements that should change in edit mode
1127 $('.save-invoice-btn').text('Update Invoice');
1128 $('.send-invoice-btn').text('Update & Send');
1129 }
1130 },
1131
1132 // Set up event handlers for existing items loaded from PHP
1133 setupExistingItems: function() {
1134
1135 // Check if we have existing items in the container (added by PHP)
1136 var existingItems = $('.invoice-items-container .invoice-item');
1137
1138 if (existingItems.length > 0) {
1139 var self = this;
1140
1141 // Set the item counter to the number of existing items
1142 this.settings.itemCounter = existingItems.length;
1143
1144 // Update field indices for all existing items to ensure they are sequential
1145 existingItems.each(function(index) {
1146 var $item = $(this);
1147
1148 // Update field indices to ensure they are sequential (0, 1, 2, etc.)
1149 self.updateItemFieldIndices($item, index);
1150
1151 // Set up event handlers for this item
1152 self.setupItemEvents($item);
1153 });
1154
1155 // Update item numbers and titles
1156 this.updateItemNumbers();
1157
1158 // Update totals after setting up existing items
1159 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
1160 window.EasyInvoicePayment.updateTotals();
1161 }
1162
1163 // Calculate totals for all existing items
1164 existingItems.each(function() {
1165 self.calculateItemTotal($(this));
1166 });
1167 } else {
1168 // Set item counter to 0 if no existing items
1169 this.settings.itemCounter = 0;
1170 }
1171
1172 },
1173
1174 // Initialize field helpers for dynamic field handling
1175 initFieldHelpers: function() {
1176
1177 // Wait a bit for the field config to be available
1178 var self = this;
1179 var attempts = 0;
1180 var maxAttempts = 10;
1181
1182 function tryInitFieldHelpers() {
1183 attempts++;
1184
1185 if (window.easyInvoice && window.easyInvoice.fieldConfig && window.easyInvoice.fieldConfig.itemFields) {
1186 window.EasyInvoiceFieldConfig = window.easyInvoice.fieldConfig.itemFields;
1187
1188 window.EasyInvoiceFieldHelpers = {
1189 getFieldValue: function(fieldName, $item) {
1190 var config = window.EasyInvoiceFieldConfig[fieldName];
1191 if (!config) {
1192 return '';
1193 }
1194
1195 var fieldType = config.type;
1196
1197 switch (fieldType) {
1198 case 'text':
1199 case 'number':
1200 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1201 return $field.val() || '';
1202 case 'textarea':
1203 var $field = $item.find('textarea[name*="[' + fieldName + ']"]');
1204 return $field.val() || '';
1205 case 'checkbox':
1206 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1207 return $field.is(':checked') ? '1' : '0';
1208 default:
1209 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1210 return $field.val() || '';
1211 }
1212 },
1213
1214 setFieldValue: function(fieldName, value, $item) {
1215 var config = window.EasyInvoiceFieldConfig[fieldName];
1216 if (!config) {
1217 return;
1218 }
1219
1220 var fieldType = config.type;
1221
1222 switch (fieldType) {
1223 case 'text':
1224 case 'number':
1225 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1226 $field.val(value);
1227 break;
1228 case 'textarea':
1229 var $field = $item.find('textarea[name*="[' + fieldName + ']"]');
1230 $field.val(value);
1231 break;
1232 case 'checkbox':
1233 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1234 if (value === '1' || value === true || value === 'true') {
1235 $field.prop('checked', true);
1236 } else {
1237 $field.prop('checked', false);
1238 }
1239 break;
1240 default:
1241 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1242 $field.val(value);
1243 break;
1244 }
1245 },
1246
1247 calculateFieldValue: function(fieldName, $item) {
1248 if (fieldName === 'total') {
1249 var quantity = parseFloat(this.getFieldValue('quantity', $item)) || 0;
1250 var price = parseFloat(this.getFieldValue('price', $item)) || 0;
1251 var adjustPercentage = parseFloat(this.getFieldValue('adjust_percentage', $item)) || 0;
1252 var baseTotal = quantity * price;
1253 var total = baseTotal * (1 + adjustPercentage / 100);
1254 return total;
1255 }
1256 return null;
1257 }
1258 };
1259
1260 return;
1261 }
1262
1263 if (attempts < maxAttempts) {
1264 setTimeout(tryInitFieldHelpers, 100);
1265 } else {
1266 // Failed to initialize field helpers after multiple attempts
1267 }
1268 }
1269
1270 // Start the initialization process
1271 tryInitFieldHelpers();
1272 },
1273
1274 // Load client data from PHP (for initial load)
1275 loadClientDataFromPHP: function(clientData) {
1276
1277 // Set the client ID in the form
1278 $('#client_id').val(clientData.id);
1279
1280 // Update client display
1281 $('#selected-client-name').text(clientData.name || '');
1282 $('#selected-client-email').text(clientData.email || '');
1283 $('#display-client-name').text(clientData.name || '-');
1284 $('#display-client-company').text(clientData.company || '-');
1285 $('#display-client-email').text(clientData.email || '-');
1286 $('#display-client-phone').text(clientData.phone || '-');
1287 $('#display-client-website').text(clientData.website || '-');
1288 $('#display-client-address').text(clientData.address || '-');
1289
1290 // Show client info sections
1291 $('#selected-client-display').show().removeClass('hidden');
1292 $('#client-info-display').show().removeClass('hidden');
1293 $('#no-client-message').hide().addClass('hidden');
1294
1295 // Update the edit client button URL
1296 $('#edit-selected-client').attr('href', easyInvoice.adminUrl + 'admin.php?page=easy-invoice-client-edit&client_id=' + clientData.id);
1297 },
1298
1299 // Load invoice data (for edit mode)
1300 loadInvoiceData: function(invoiceData) {
1301
1302 // Set form fields with invoice data
1303 if (invoiceData.title) {
1304 $('input[name="invoice_title"]').val(invoiceData.title);
1305 }
1306 if (invoiceData.issue_date) {
1307 $('input[name="issue-date"]').val(invoiceData.issue_date);
1308 }
1309 if (invoiceData.due_date) {
1310 $('input[name="due-date"]').val(invoiceData.due_date);
1311 }
1312 if (invoiceData.status) {
1313 $('select[name="status"]').val(invoiceData.status);
1314 }
1315 if (invoiceData.notes) {
1316 $('textarea[name="notes"]').val(invoiceData.notes);
1317 }
1318 if (invoiceData.terms) {
1319 $('textarea[name="terms"]').val(invoiceData.terms);
1320 }
1321 if (invoiceData.internal_notes) {
1322 $('textarea[name="internal_notes"]').val(invoiceData.internal_notes);
1323 }
1324 },
1325
1326 // Set up initial tab state
1327 setupInitialTabState: function() {
1328 // Ensure the first tab is active by default
1329 var $firstTab = $('.tab-button').first();
1330 var $firstContent = $('.tab-content').first();
1331
1332 if ($firstTab.length && $firstContent.length) {
1333 $firstTab.addClass('border-indigo-500 text-indigo-600')
1334 .removeClass('border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300');
1335 $firstContent.addClass('active').removeClass('hidden');
1336 }
1337 },
1338
1339 // Initialize existing items
1340 initializeExistingItems: function() {
1341 // Check if we have the easyInvoice object
1342 if (typeof easyInvoice !== 'undefined') {
1343 this.settings.editMode = easyInvoice.editMode || false;
1344 this.settings.invoiceId = parseInt(easyInvoice.invoice_id) || 0;
1345
1346 // Load invoice data if in edit mode
1347 if (this.settings.editMode && easyInvoice.invoiceData) {
1348 this.loadInvoiceData(easyInvoice.invoiceData);
1349 }
1350
1351 // Load items if available
1352 if (easyInvoice.invoiceItems && Array.isArray(easyInvoice.invoiceItems)) {
1353 this.settings.items = easyInvoice.invoiceItems;
1354 }
1355
1356 // Load initial client data if available from PHP
1357 if (easyInvoice.clientData && easyInvoice.clientData.id) {
1358 this.loadClientDataFromPHP(easyInvoice.clientData);
1359 }
1360 } else {
1361 // easyInvoice object is not defined
1362 }
1363
1364 // Ensure initial active tab has correct styling
1365 this.setupInitialTabState();
1366
1367 // Set up event handlers for existing items loaded from PHP
1368 this.setupExistingItems();
1369
1370 // Set up client selection functionality
1371 this.setupClientSelection();
1372 }
1373 };
1374
1375 // Initialize invoice builder when document is ready
1376 jQuery(document).ready(function($) {
1377 // Initialize the invoice builder
1378 if (typeof EasyInvoiceBuilder !== 'undefined') {
1379 EasyInvoiceBuilder.init();
1380 }
1381
1382 // Fallback: Try to set up event handlers again after a short delay
1383 // in case the DOM elements weren't ready yet
1384 setTimeout(function() {
1385 if ($('#collapse-all-items').length === 0) {
1386 // Collapse button still not found after delay
1387 } else {
1388 // Re-attach event handler if needed (using delegation)
1389 $(document).off('click', '#collapse-all-items').on('click', '#collapse-all-items', function(e) {
1390 e.preventDefault();
1391 e.stopPropagation();
1392 EasyInvoiceBuilder.collapseAllItems();
1393 });
1394 }
1395 }, 1000);
1396 });
1397
1398 })(jQuery);