| 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); |