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

1,410 lines 62.9 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 = 0;
370 // Only apply adjust percentage if the adjust field is enabled
371 if (window.easyInvoice && window.easyInvoice.showAdjustField) {
372 adjustPercentage = parseFloat($item.find('input[name*="[adjust_percentage]"]').val()) || 0;
373 }
374 var baseTotal = quantity * price;
375 var total = baseTotal * (1 + adjustPercentage / 100);
376
377 // Update the total field
378 var $totalField = $item.find('input[name*="[total]"]');
379 $totalField.val(total.toFixed(2));
380
381 // Update the collapsed summary
382 $item.find('.quantity-summary').text(quantity);
383 $item.find('.price-summary').text(price.toFixed(2));
384 $item.find('.total-summary').text(total.toFixed(2));
385 }
386 },
387
388 // Update the collapsed summary of an item
389 updateItemSummary: function($item) {
390 // Use dynamic field helpers for summary updates
391 if (window.EasyInvoiceFieldHelpers) {
392 var quantity = window.EasyInvoiceFieldHelpers.getFieldValue('quantity', $item);
393 var price = window.EasyInvoiceFieldHelpers.getFieldValue('price', $item);
394 var total = window.EasyInvoiceFieldHelpers.getFieldValue('total', $item);
395
396 // Update summary fields directly since updateSummaryFields doesn't exist
397 $item.find('.quantity-summary').text(quantity || '0');
398 $item.find('.price-summary').text((parseFloat(price) || 0).toFixed(2));
399 $item.find('.total-summary').text((parseFloat(total) || 0).toFixed(2));
400
401 } else {
402 // Fallback to hardcoded summary
403 var quantity = parseFloat($item.find('input[name*="[quantity]"]').val()) || 0;
404 var price = parseFloat($item.find('input[name*="[price]"]').val()) || 0;
405 var adjustPercentage = 0;
406 // Only apply adjust percentage if the adjust field is enabled
407 if (window.easyInvoice && window.easyInvoice.showAdjustField) {
408 adjustPercentage = parseFloat($item.find('input[name*="[adjust_percentage]"]').val()) || 0;
409 }
410 var baseTotal = quantity * price;
411 var total = baseTotal * (1 + adjustPercentage / 100);
412
413 $item.find('.quantity-summary').text(quantity);
414 $item.find('.price-summary').text(price.toFixed(2));
415 $item.find('.total-summary').text(total.toFixed(2));
416
417 }
418 },
419
420 // Remove an item from the invoice
421 removeItem: function($item) {
422 var self = this;
423 $item.addClass('opacity-0');
424 setTimeout(function() {
425 $item.remove();
426
427 // Ensure at least one item remains
428 if ($('.invoice-item').length === 0) {
429 self.addNewItem();
430 } else {
431 self.updateItemNumbers();
432 }
433
434 // Update totals
435 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
436 window.EasyInvoicePayment.updateTotals();
437 }
438 }.bind(this), 200);
439 },
440
441 updateItemNumbers: function() {
442 var self = this;
443 $('.invoice-items-container .invoice-item').each(function(index) {
444 var itemNumber = index + 1;
445 var title = $(this).find('input[name*="[title]"]').val();
446 var shortTitle = title ? (title.length > 30 ? title.substring(0, 30) + '...' : title) : 'New Item';
447 $(this).find('h3').text(shortTitle);
448
449 // Only update field indices if they don't already match the current index
450 var $item = $(this);
451 var firstField = $item.find('input[name*="items["]').first();
452 if (firstField.length > 0) {
453 var fieldName = firstField.attr('name');
454 var currentIndex = fieldName.match(/items\[(\d+)\]/);
455 if (currentIndex && parseInt(currentIndex[1]) !== index) {
456 // Field index doesn't match, update it
457 self.updateItemFieldIndices($item, index);
458 }
459 }
460 });
461 self.settings.itemCounter = $('.invoice-items-container .invoice-item').length;
462 },
463
464 // Format currency value
465 formatCurrency: function(value) {
466 var symbol = '$';
467
468 // Use the currency symbol from payment manager if available
469 if (window.EasyInvoicePayment && window.EasyInvoicePayment.settings.currencySymbol) {
470 symbol = window.EasyInvoicePayment.settings.currencySymbol;
471 }
472
473 return symbol + parseFloat(value).toFixed(2);
474 },
475
476 // Get all current invoice items
477 getItems: function() {
478 var items = [];
479
480 $('.invoice-item').each(function(index) {
481 var $item = $(this);
482 var item = {
483 id: $item.attr('id')
484 };
485
486 // Use dynamic field helpers to get all field values
487 if (window.EasyInvoiceFieldHelpers && window.EasyInvoiceFieldConfig) {
488 Object.keys(window.EasyInvoiceFieldConfig).forEach(function(fieldName) {
489 var value = window.EasyInvoiceFieldHelpers.getFieldValue(fieldName, $item);
490 item[fieldName] = value;
491 });
492 } else {
493 // Fallback to hardcoded field names
494 item.name = $item.find('input[name*="[title]"]').val() || '';
495 item.description = $item.find('textarea[name*="[description]"]').val() || '';
496 item.quantity = parseFloat($item.find('input[name*="[quantity]"]').val()) || 0;
497 item.price = parseFloat($item.find('input[name*="[price]"]').val()) || 0;
498 item.taxable = $item.find('input[name*="[taxable]"]').is(':checked');
499 }
500
501 items.push(item);
502 });
503
504 return items;
505 },
506
507 // Set up client selection functionality
508 setupClientSelection: function() {
509 var self = this;
510 var isInitialLoad = true; // Flag to prevent AJAX calls on initial load
511
512 // Client dropdown change
513 $('#client_id').on('change', function() {
514 var clientId = $(this).val();
515
516 // Skip AJAX call if this is the initial load
517 if (isInitialLoad) {
518 isInitialLoad = false;
519 return;
520 }
521
522 if (clientId === 'new') {
523 // Show new client modal
524 $('#add_client_modal').show();
525 } else if (clientId !== '') {
526 // Load client data (ajax call or from already available data)
527 self.loadClientData(clientId);
528 }
529 });
530
531 // Close modal button
532 $('.close-modal').on('click', function() {
533 $('#add_client_modal').hide();
534
535 // Reset client dropdown if no client was selected
536 if ($('#client_id').val() === 'new') {
537 $('#client_id').val('');
538 }
539 });
540
541 // Submit new client form
542 $('#add_client_form').on('submit', function(e) {
543 e.preventDefault();
544 self.addNewClient();
545 });
546
547 // Reset the flag after a short delay to allow for user interactions
548 setTimeout(function() {
549 isInitialLoad = false;
550 }, 500);
551 },
552
553 // Load client data when a client is selected
554 loadClientData: function(clientId) {
555 // First check if we have client data already loaded from PHP
556 if (typeof easyInvoice !== 'undefined' && easyInvoice.clientData && easyInvoice.clientData.id == clientId) {
557 var client = easyInvoice.clientData;
558
559 // Update client info fields
560 $('#client_name_display').text(client.name || '');
561 $('#client_email_display').text(client.email || '');
562 $('#client_phone_display').text(client.phone || '');
563 $('#client_address_display').html((client.address || '').replace(/\n/g, '<br>'));
564
565 // Show client info section
566 $('#client_info').show();
567 return;
568 }
569
570 // For dynamic client selection (not initial load), make an AJAX call to get client data
571 $.ajax({
572 url: easyInvoice.ajaxUrl,
573 type: 'POST',
574 data: {
575 action: 'easy_invoice_get_client',
576 nonce: easyInvoice.nonce,
577 client_id: clientId
578 },
579 success: function(response) {
580 if (response.success) {
581 var client = response.data;
582
583 // Update client info fields
584 $('#client_name_display').text(client.name);
585 $('#client_email_display').text(client.email);
586 $('#client_phone_display').text(client.phone || '');
587 $('#client_address_display').html(client.address.replace(/\n/g, '<br>') || '');
588
589 // Show client info section
590 $('#client_info').show();
591 } else {
592 // Error loading client data
593 }
594 },
595 error: function(xhr, status, error) {
596 // AJAX error loading client data
597 }
598 });
599 },
600
601 // Add a new client via AJAX
602 addNewClient: function() {
603 var self = this;
604 var clientData = {
605 name: $('#new_client_name').val(),
606 email: $('#new_client_email').val(),
607 phone: $('#new_client_phone').val(),
608 address: $('#new_client_address').val(),
609 notes: $('#new_client_notes').val()
610 };
611
612 // Validate required fields
613 if (!clientData.name || !clientData.email) {
614 if (typeof EasyInvoiceToast !== 'undefined') {
615 EasyInvoiceToast.show('error', 'Client name and email are required.');
616 }
617 return;
618 }
619
620 // Send AJAX request to add client
621 $.ajax({
622 url: easyInvoice.ajaxUrl,
623 type: 'POST',
624 data: {
625 action: 'easy_invoice_add_client',
626 nonce: easyInvoice.nonce,
627 client_data: clientData
628 },
629 success: function(response) {
630 if (response.success) {
631 var newClient = response.data.client;
632 var newClientId = response.data.client_id;
633
634 // Add new client to dropdown
635 $('#client_id').append($('<option>', {
636 value: newClientId,
637 text: newClient.name
638 }));
639
640 // Select the new client
641 $('#client_id').val(newClientId);
642
643 // Load the client data
644 self.loadClientData(newClientId);
645
646 // Hide modal
647 $('#add_client_modal').hide();
648
649 // Clear form
650 $('#add_client_form')[0].reset();
651
652 // Show success message
653 if (typeof EasyInvoiceToast !== 'undefined') {
654 EasyInvoiceToast.show('success', 'Client added successfully!');
655 }
656 } else {
657 if (typeof EasyInvoiceToast !== 'undefined') {
658 EasyInvoiceToast.show('error', 'Error adding client: ' + response.data);
659 }
660 }
661 },
662 error: function(xhr, status, error) {
663 // AJAX error adding client
664 if (typeof EasyInvoiceToast !== 'undefined') {
665 EasyInvoiceToast.show('error', 'Error adding client. Please try again.');
666 }
667 }
668 });
669 },
670
671 // Save the invoice
672 saveInvoice: function() {
673 var self = this;
674
675 // Collect all form data at once
676 var formData = {};
677
678 // Get all form fields using serializeArray
679 $('#invoice-form').serializeArray().forEach(function(item) {
680 formData[item.name] = item.value;
681 });
682
683 // Add items data
684 formData.items = this.getItems();
685
686 // Only exclude invoice number for updates, not for new invoices
687 if (this.settings.editMode && this.settings.invoiceId > 0) {
688 delete formData['invoice-number'];
689 delete formData.invoice_number;
690 }
691
692 // Add invoice ID if in edit mode
693 if (this.settings.editMode && this.settings.invoiceId > 0) {
694 formData.invoice_id = this.settings.invoiceId;
695 }
696
697 // Add client ID if selected
698 var clientId = $('#client_id').val();
699 if (clientId && clientId !== '') {
700 formData.client_id = clientId;
701 }
702
703 // Show loading state
704 var $saveBtn = $('.save-invoice-btn');
705 var originalText = $saveBtn.text();
706 $saveBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-2"></i>Saving...');
707
708 // Send AJAX request to save invoice
709 $.ajax({
710 url: easyInvoice.ajaxUrl,
711 type: 'POST',
712 data: {
713 action: 'easy_invoice_save_invoice',
714 nonce: easyInvoice.nonce,
715 invoice_data: formData
716 },
717 success: function(response) {
718 // Restore button state
719 $saveBtn.prop('disabled', false).text(originalText);
720
721 if (response.success) {
722 // Show success message without page reload
723 self.showNotification('Invoice saved successfully!', 'success');
724
725 // Update invoice ID if it changed (for new invoices)
726 if (response.data.invoice_id) {
727 self.settings.invoiceId = response.data.invoice_id;
728 self.settings.editMode = true;
729
730 // Update the URL to reflect the invoice ID
731 if (window.history && window.history.pushState) {
732 var newUrl = window.location.href.split('?')[0] + '?invoice_id=' + response.data.invoice_id;
733 window.history.pushState({}, '', newUrl);
734 }
735 }
736
737 // Update any UI elements that depend on edit mode
738 self.updateUIForEditMode();
739
740 } else {
741 self.showNotification('Error saving invoice: ' + response.data, 'error');
742 }
743 },
744 error: function(xhr, status, error) {
745 // Restore button state
746 $saveBtn.prop('disabled', false).text(originalText);
747
748 // AJAX error saving invoice
749 self.showNotification('Error saving invoice. Please try again.', 'error');
750 }
751 });
752 },
753
754 // Send the invoice to the client
755 sendInvoice: function() {
756 // First save the invoice, then send it
757 var self = this;
758
759 // Collect all form data at once
760 var formData = {};
761
762 // Get all form fields using serializeArray
763 $('#invoice-form').serializeArray().forEach(function(item) {
764 formData[item.name] = item.value;
765 });
766
767 // Add items data
768 formData.items = this.getItems();
769
770 // Only exclude invoice number for updates, not for new invoices
771 if (this.settings.editMode && this.settings.invoiceId > 0) {
772 delete formData['invoice-number'];
773 delete formData.invoice_number;
774 }
775
776 // Add invoice ID if in edit mode
777 if (this.settings.editMode && this.settings.invoiceId > 0) {
778 formData.invoice_id = this.settings.invoiceId;
779 }
780
781 // Add client ID if selected
782 var clientId = $('#client_id').val();
783 if (clientId && clientId !== '') {
784 formData.client_id = clientId;
785 }
786
787 // Show loading state
788 var $sendBtn = $('.send-invoice-btn');
789 var originalText = $sendBtn.text();
790 $sendBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-2"></i>Sending...');
791
792 // Send AJAX request to save and send invoice
793 $.ajax({
794 url: easyInvoice.ajaxUrl,
795 type: 'POST',
796 data: {
797 action: 'easy_invoice_save_and_send_invoice',
798 nonce: easyInvoice.nonce,
799 invoice_data: formData
800 },
801 success: function(response) {
802 // Restore button state
803 $sendBtn.prop('disabled', false).text(originalText);
804
805 if (response.success) {
806 // Show success message without page reload
807 self.showNotification('Invoice sent successfully!', 'success');
808
809 // Update invoice ID if it changed (for new invoices)
810 if (response.data.invoice_id) {
811 self.settings.invoiceId = response.data.invoice_id;
812 self.settings.editMode = true;
813
814 // Update the URL to reflect the invoice ID
815 if (window.history && window.history.pushState) {
816 var newUrl = window.location.href.split('?')[0] + '?invoice_id=' + response.data.invoice_id;
817 window.history.pushState({}, '', newUrl);
818 }
819 }
820
821 // Update any UI elements that depend on edit mode
822 self.updateUIForEditMode();
823
824 } else {
825 self.showNotification('Error sending invoice: ' + response.data, 'error');
826 }
827 },
828 error: function(xhr, status, error) {
829 // Restore button state
830 $sendBtn.prop('disabled', false).text(originalText);
831
832 // AJAX error sending invoice
833 self.showNotification('Error sending invoice. Please try again.', 'error');
834 }
835 });
836 },
837
838 // Reset the form
839 resetForm: function() {
840 // Reset form fields
841 $('#invoice-form')[0].reset();
842
843 // Clear items
844 $('.invoice-items-container').empty();
845
846 // Add one empty item
847 this.addNewItem();
848
849 // Hide client info
850 $('#client_info').hide();
851
852 // Reset client dropdown
853 $('#client_id').val('');
854
855 // Reset payment settings if payment manager is available
856 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateUI === 'function') {
857 window.EasyInvoicePayment.updateUI();
858 }
859 },
860
861 // Collapse or expand all invoice items
862 collapseAllItems: function() {
863 var self = this;
864 var $button = $('#collapse-all-items');
865 var allCollapsed = true;
866
867 // Check if all items are already collapsed
868 $('.invoice-item').each(function() {
869 var $item = $(this);
870 var itemContent = $item.find('.item-content');
871 if (itemContent.is(':visible')) {
872 allCollapsed = false;
873 return false; // Break the loop if we find an expanded item
874 }
875 });
876
877 // Update button text based on current state
878 if (allCollapsed) {
879 // If all items are collapsed, expand them
880 $button.html('<i class="fas fa-chevron-down mr-1"></i> Collapse All');
881 $('.invoice-item').each(function() {
882 var $item = $(this);
883 var itemContent = $item.find('.item-content');
884 var summaryElement = $item.find('.item-collapsed-summary');
885 var sampleButton = $item.find('.fill-sample-data-btn');
886 var icon = $item.find('.item-collapse-toggle i');
887
888
889 // Only toggle if it's currently collapsed
890 if (!itemContent.is(':visible')) {
891 itemContent.slideDown(200);
892 summaryElement.slideUp(200);
893 sampleButton.show(); // Show sample button when expanded
894 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
895 // Remove collapsed item styling
896 $item.removeClass('collapsed-item');
897 }
898 });
899 } else {
900 // If any items are expanded, collapse them all
901 $button.html('<i class="fas fa-chevron-right mr-1"></i> Expand All');
902 $('.invoice-item').each(function() {
903 var $item = $(this);
904 var itemContent = $item.find('.item-content');
905 var summaryElement = $item.find('.item-collapsed-summary');
906 var sampleButton = $item.find('.fill-sample-data-btn');
907 var icon = $item.find('.item-collapse-toggle i');
908
909 // Only toggle if it's currently expanded
910 if (itemContent.is(':visible')) {
911 // Update the summary before collapsing
912 self.updateItemSummary($item);
913 itemContent.slideUp(200);
914 summaryElement.slideDown(200);
915 sampleButton.hide(); // Hide sample button when collapsed
916 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
917 // Add collapsed item styling
918 $item.addClass('collapsed-item');
919 }
920 });
921 }
922
923 // Update the preview to reflect changes
924 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
925 window.EasyInvoicePayment.updateTotals();
926 } else if (typeof updatePreview === 'function') {
927 updatePreview();
928 }
929 },
930
931 // Fill an item with sample data
932 fillItemWithSampleData: function($item) {
933
934 // Array of realistic sample data
935 var sampleItems = [
936 {
937 title: 'Web Development Services',
938 description: 'Custom website development including responsive design, SEO optimization, and content management system integration.',
939 quantity: 1,
940 price: 2500.00,
941 taxable: true
942 },
943 {
944 title: 'Logo Design Package',
945 description: 'Professional logo design with multiple concepts, revisions, and final files in various formats (AI, EPS, PNG, JPG).',
946 quantity: 1,
947 price: 450.00,
948 taxable: false
949 },
950 {
951 title: 'Monthly Website Maintenance',
952 description: 'Ongoing website maintenance including security updates, content updates, and technical support.',
953 quantity: 3,
954 price: 150.00,
955 taxable: true
956 },
957 {
958 title: 'SEO Optimization',
959 description: 'Search engine optimization services including keyword research, on-page optimization, and performance monitoring.',
960 quantity: 1,
961 price: 800.00,
962 taxable: true
963 },
964 {
965 title: 'Content Writing',
966 description: 'Professional content writing services including blog posts, website copy, and marketing materials.',
967 quantity: 5,
968 price: 75.00,
969 taxable: true
970 },
971 {
972 title: 'Social Media Management',
973 description: 'Monthly social media management including content creation, posting, and engagement monitoring.',
974 quantity: 1,
975 price: 300.00,
976 taxable: true
977 }
978 ];
979
980 // Pick a random sample item
981 var randomIndex = Math.floor(Math.random() * sampleItems.length);
982 var sampleData = sampleItems[randomIndex];
983
984 // Add dynamic sample values for any custom fields
985 if (window.EasyInvoiceFieldConfig) {
986 Object.keys(window.EasyInvoiceFieldConfig).forEach(function(fieldName) {
987 // Skip standard fields that are already in sampleData
988 if (!sampleData.hasOwnProperty(fieldName)) {
989 var fieldConfig = window.EasyInvoiceFieldConfig[fieldName];
990 var fieldType = fieldConfig.type;
991
992 // Generate appropriate sample value based on field type
993 switch (fieldType) {
994 case 'text':
995 sampleData[fieldName] = 'Sample ' + fieldName.replace(/_/g, ' ').replace(/\b\w/g, function(l) { return l.toUpperCase(); });
996 break;
997 case 'number':
998 sampleData[fieldName] = Math.floor(Math.random() * 100) + 1;
999 break;
1000 case 'checkbox':
1001 sampleData[fieldName] = Math.random() > 0.5 ? '1' : '0';
1002 break;
1003 case 'textarea':
1004 sampleData[fieldName] = 'This is a sample value for ' + fieldName.replace(/_/g, ' ') + '.';
1005 break;
1006 default:
1007 sampleData[fieldName] = 'Sample ' + fieldName;
1008 break;
1009 }
1010 }
1011 });
1012 }
1013
1014 // Set values using dynamic field helpers
1015 if (window.EasyInvoiceFieldHelpers) {
1016 Object.keys(sampleData).forEach(function(fieldName) {
1017 if (window.EasyInvoiceFieldConfig[fieldName]) {
1018 window.EasyInvoiceFieldHelpers.setFieldValue(fieldName, sampleData[fieldName], $item);
1019 }
1020 });
1021
1022 // Trigger input event for title field to update header
1023 $item.find('input[name*="[title]"]').trigger('input');
1024 } else {
1025 // Fallback to hardcoded field names
1026 $item.find('input[name*="[title]"]').val(sampleData.title).trigger('input');
1027 $item.find('textarea[name*="[description]"]').val(sampleData.description);
1028 $item.find('input[name*="[quantity]"]').val(sampleData.quantity);
1029 $item.find('input[name*="[price]"]').val(sampleData.price);
1030 $item.find('input[name*="[taxable]"]').prop('checked', sampleData.taxable);
1031 }
1032
1033 // Calculate total
1034 this.calculateItemTotal($item);
1035
1036 // Add a small delay to ensure total calculation is complete
1037 setTimeout(function() {
1038 // Re-calculate total to ensure it's correct
1039 // Store reference to the correct context
1040 var self = this;
1041 self.calculateItemTotal($item);
1042
1043 // Update totals
1044 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
1045 window.EasyInvoicePayment.updateTotals();
1046 }
1047 }.bind(this), 100);
1048
1049 },
1050
1051 // Load invoice data when in edit mode
1052 loadInvoiceData: function(invoiceData) {
1053
1054 // Set form fields using the correct field names from InvoiceFormManager
1055 $('#invoice-form').find('input[name="invoice_title"]').val(invoiceData.title || '');
1056 $('#invoice-form').find('input[name="invoice-number"]').val(invoiceData.number || '');
1057 $('#invoice-form').find('input[name="issue-date"]').val(invoiceData.issue_date || '');
1058 $('#invoice-form').find('input[name="due-date"]').val(invoiceData.due_date || '');
1059 $('#invoice-form').find('select[name="status"]').val(invoiceData.status || 'draft');
1060 $('#invoice-form').find('textarea[name="notes"]').val(invoiceData.notes || '');
1061
1062 // Set client info if available
1063 if (easyInvoice.clientData) {
1064 $('#client_name_display').text(easyInvoice.clientData.name || '');
1065 $('#client_email_display').text(easyInvoice.clientData.email || '');
1066 $('#client_address_display').html((easyInvoice.clientData.address || '').replace(/\n/g, '<br>'));
1067 $('#client_info').show();
1068 }
1069
1070 // Set items
1071 if (easyInvoice.invoiceItems && Array.isArray(easyInvoice.invoiceItems)) {
1072 this.settings.items = easyInvoice.invoiceItems;
1073 }
1074 },
1075
1076 // Show notification
1077 showNotification: function(message, type) {
1078 // Remove any existing notifications
1079 $('.easy-invoice-notification').remove();
1080
1081 // Create notification element
1082 var notification = $('<div class="easy-invoice-notification"></div>');
1083
1084 // Set notification content and styling
1085 var icon = type === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle';
1086 var bgColor = type === 'success' ? 'bg-green-50' : 'bg-red-50';
1087 var borderColor = type === 'success' ? 'border-green-200' : 'border-red-200';
1088 var textColor = type === 'success' ? 'text-green-800' : 'text-red-800';
1089 var iconColor = type === 'success' ? 'text-green-400' : 'text-red-400';
1090
1091 notification.html(`
1092 <div class="fixed top-4 right-4 z-50 max-w-sm w-full ${bgColor} border ${borderColor} rounded-lg shadow-lg p-4">
1093 <div class="flex items-start">
1094 <div class="flex-shrink-0">
1095 <i class="${icon} ${iconColor} text-lg"></i>
1096 </div>
1097 <div class="ml-3 w-0 flex-1">
1098 <p class="text-sm font-medium ${textColor}">${message}</p>
1099 </div>
1100 <div class="ml-4 flex-shrink-0 flex">
1101 <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">
1102 <span class="sr-only">Close</span>
1103 <i class="fas fa-times"></i>
1104 </button>
1105 </div>
1106 </div>
1107 </div>
1108 `);
1109
1110 // Add to page
1111 $('body').append(notification);
1112
1113 // Auto-hide after 5 seconds
1114 setTimeout(function() {
1115 notification.fadeOut(300, function() {
1116 $(this).remove();
1117 });
1118 }, 5000);
1119
1120 // Handle close button
1121 notification.find('.notification-close').on('click', function() {
1122 notification.fadeOut(300, function() {
1123 $(this).remove();
1124 });
1125 });
1126 },
1127
1128 // Update UI for edit mode
1129 updateUIForEditMode: function() {
1130 // Update page title to show edit mode
1131 if (this.settings.editMode && this.settings.invoiceId > 0) {
1132 document.title = document.title.replace('New Invoice', 'Edit Invoice');
1133
1134 // Update any buttons or UI elements that should change in edit mode
1135 $('.save-invoice-btn').text('Update Invoice');
1136 $('.send-invoice-btn').text('Update & Send');
1137 }
1138 },
1139
1140 // Set up event handlers for existing items loaded from PHP
1141 setupExistingItems: function() {
1142
1143 // Check if we have existing items in the container (added by PHP)
1144 var existingItems = $('.invoice-items-container .invoice-item');
1145
1146 if (existingItems.length > 0) {
1147 var self = this;
1148
1149 // Set the item counter to the number of existing items
1150 this.settings.itemCounter = existingItems.length;
1151
1152 // Update field indices for all existing items to ensure they are sequential
1153 existingItems.each(function(index) {
1154 var $item = $(this);
1155
1156 // Update field indices to ensure they are sequential (0, 1, 2, etc.)
1157 self.updateItemFieldIndices($item, index);
1158
1159 // Set up event handlers for this item
1160 self.setupItemEvents($item);
1161 });
1162
1163 // Update item numbers and titles
1164 this.updateItemNumbers();
1165
1166 // Update totals after setting up existing items
1167 if (window.EasyInvoicePayment && typeof window.EasyInvoicePayment.updateTotals === 'function') {
1168 window.EasyInvoicePayment.updateTotals();
1169 }
1170
1171 // Calculate totals for all existing items
1172 existingItems.each(function() {
1173 self.calculateItemTotal($(this));
1174 });
1175 } else {
1176 // Set item counter to 0 if no existing items
1177 this.settings.itemCounter = 0;
1178 }
1179
1180 },
1181
1182 // Initialize field helpers for dynamic field handling
1183 initFieldHelpers: function() {
1184
1185 // Wait a bit for the field config to be available
1186 var self = this;
1187 var attempts = 0;
1188 var maxAttempts = 10;
1189
1190 function tryInitFieldHelpers() {
1191 attempts++;
1192
1193 if (window.easyInvoice && window.easyInvoice.fieldConfig && window.easyInvoice.fieldConfig.itemFields) {
1194 window.EasyInvoiceFieldConfig = window.easyInvoice.fieldConfig.itemFields;
1195
1196 window.EasyInvoiceFieldHelpers = {
1197 getFieldValue: function(fieldName, $item) {
1198 var config = window.EasyInvoiceFieldConfig[fieldName];
1199 if (!config) {
1200 return '';
1201 }
1202
1203 var fieldType = config.type;
1204
1205 switch (fieldType) {
1206 case 'text':
1207 case 'number':
1208 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1209 return $field.val() || '';
1210 case 'textarea':
1211 var $field = $item.find('textarea[name*="[' + fieldName + ']"]');
1212 return $field.val() || '';
1213 case 'checkbox':
1214 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1215 return $field.is(':checked') ? '1' : '0';
1216 default:
1217 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1218 return $field.val() || '';
1219 }
1220 },
1221
1222 setFieldValue: function(fieldName, value, $item) {
1223 var config = window.EasyInvoiceFieldConfig[fieldName];
1224 if (!config) {
1225 return;
1226 }
1227
1228 var fieldType = config.type;
1229
1230 switch (fieldType) {
1231 case 'text':
1232 case 'number':
1233 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1234 $field.val(value);
1235 break;
1236 case 'textarea':
1237 var $field = $item.find('textarea[name*="[' + fieldName + ']"]');
1238 $field.val(value);
1239 break;
1240 case 'checkbox':
1241 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1242 if (value === '1' || value === true || value === 'true') {
1243 $field.prop('checked', true);
1244 } else {
1245 $field.prop('checked', false);
1246 }
1247 break;
1248 default:
1249 var $field = $item.find('input[name*="[' + fieldName + ']"]');
1250 $field.val(value);
1251 break;
1252 }
1253 },
1254
1255 calculateFieldValue: function(fieldName, $item) {
1256 if (fieldName === 'total') {
1257 var quantity = parseFloat(this.getFieldValue('quantity', $item)) || 0;
1258 var price = parseFloat(this.getFieldValue('price', $item)) || 0;
1259 var adjustPercentage = 0;
1260 // Only apply adjust percentage if the adjust field is enabled
1261 if (window.easyInvoice && window.easyInvoice.showAdjustField) {
1262 adjustPercentage = parseFloat(this.getFieldValue('adjust_percentage', $item)) || 0;
1263 }
1264 var baseTotal = quantity * price;
1265 var total = baseTotal * (1 + adjustPercentage / 100);
1266 return total;
1267 }
1268 return null;
1269 }
1270 };
1271
1272 return;
1273 }
1274
1275 if (attempts < maxAttempts) {
1276 setTimeout(tryInitFieldHelpers, 100);
1277 } else {
1278 // Failed to initialize field helpers after multiple attempts
1279 }
1280 }
1281
1282 // Start the initialization process
1283 tryInitFieldHelpers();
1284 },
1285
1286 // Load client data from PHP (for initial load)
1287 loadClientDataFromPHP: function(clientData) {
1288
1289 // Set the client ID in the form
1290 $('#client_id').val(clientData.id);
1291
1292 // Update client display
1293 $('#selected-client-name').text(clientData.name || '');
1294 $('#selected-client-email').text(clientData.email || '');
1295 $('#display-client-name').text(clientData.name || '-');
1296 $('#display-client-company').text(clientData.company || '-');
1297 $('#display-client-email').text(clientData.email || '-');
1298 $('#display-client-phone').text(clientData.phone || '-');
1299 $('#display-client-website').text(clientData.website || '-');
1300 $('#display-client-address').text(clientData.address || '-');
1301
1302 // Show client info sections
1303 $('#selected-client-display').show().removeClass('hidden');
1304 $('#client-info-display').show().removeClass('hidden');
1305 $('#no-client-message').hide().addClass('hidden');
1306
1307 // Update the edit client button URL
1308 $('#edit-selected-client').attr('href', easyInvoice.adminUrl + 'admin.php?page=easy-invoice-client-edit&client_id=' + clientData.id);
1309 },
1310
1311 // Load invoice data (for edit mode)
1312 loadInvoiceData: function(invoiceData) {
1313
1314 // Set form fields with invoice data
1315 if (invoiceData.title) {
1316 $('input[name="invoice_title"]').val(invoiceData.title);
1317 }
1318 if (invoiceData.issue_date) {
1319 $('input[name="issue-date"]').val(invoiceData.issue_date);
1320 }
1321 if (invoiceData.due_date) {
1322 $('input[name="due-date"]').val(invoiceData.due_date);
1323 }
1324 if (invoiceData.status) {
1325 $('select[name="status"]').val(invoiceData.status);
1326 }
1327 if (invoiceData.notes) {
1328 $('textarea[name="notes"]').val(invoiceData.notes);
1329 }
1330 if (invoiceData.terms) {
1331 $('textarea[name="terms"]').val(invoiceData.terms);
1332 }
1333 if (invoiceData.internal_notes) {
1334 $('textarea[name="internal_notes"]').val(invoiceData.internal_notes);
1335 }
1336 },
1337
1338 // Set up initial tab state
1339 setupInitialTabState: function() {
1340 // Ensure the first tab is active by default
1341 var $firstTab = $('.tab-button').first();
1342 var $firstContent = $('.tab-content').first();
1343
1344 if ($firstTab.length && $firstContent.length) {
1345 $firstTab.addClass('border-indigo-500 text-indigo-600')
1346 .removeClass('border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300');
1347 $firstContent.addClass('active').removeClass('hidden');
1348 }
1349 },
1350
1351 // Initialize existing items
1352 initializeExistingItems: function() {
1353 // Check if we have the easyInvoice object
1354 if (typeof easyInvoice !== 'undefined') {
1355 this.settings.editMode = easyInvoice.editMode || false;
1356 this.settings.invoiceId = parseInt(easyInvoice.invoice_id) || 0;
1357
1358 // Load invoice data if in edit mode
1359 if (this.settings.editMode && easyInvoice.invoiceData) {
1360 this.loadInvoiceData(easyInvoice.invoiceData);
1361 }
1362
1363 // Load items if available
1364 if (easyInvoice.invoiceItems && Array.isArray(easyInvoice.invoiceItems)) {
1365 this.settings.items = easyInvoice.invoiceItems;
1366 }
1367
1368 // Load initial client data if available from PHP
1369 if (easyInvoice.clientData && easyInvoice.clientData.id) {
1370 this.loadClientDataFromPHP(easyInvoice.clientData);
1371 }
1372 } else {
1373 // easyInvoice object is not defined
1374 }
1375
1376 // Ensure initial active tab has correct styling
1377 this.setupInitialTabState();
1378
1379 // Set up event handlers for existing items loaded from PHP
1380 this.setupExistingItems();
1381
1382 // Set up client selection functionality
1383 this.setupClientSelection();
1384 }
1385 };
1386
1387 // Initialize invoice builder when document is ready
1388 jQuery(document).ready(function($) {
1389 // Initialize the invoice builder
1390 if (typeof EasyInvoiceBuilder !== 'undefined') {
1391 EasyInvoiceBuilder.init();
1392 }
1393
1394 // Fallback: Try to set up event handlers again after a short delay
1395 // in case the DOM elements weren't ready yet
1396 setTimeout(function() {
1397 if ($('#collapse-all-items').length === 0) {
1398 // Collapse button still not found after delay
1399 } else {
1400 // Re-attach event handler if needed (using delegation)
1401 $(document).off('click', '#collapse-all-items').on('click', '#collapse-all-items', function(e) {
1402 e.preventDefault();
1403 e.stopPropagation();
1404 EasyInvoiceBuilder.collapseAllItems();
1405 });
1406 }
1407 }, 1000);
1408 });
1409
1410 })(jQuery);