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

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

425 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($) {
2 // Check if we're on the builder page to avoid conflicts with invoice-builder.js
3 const isBuilderPage = window.location.href.indexOf('easy-invoice-new') !== -1 ||
4 window.location.href.indexOf('page=easy-invoice-new') !== -1 ||
5 window.location.href.indexOf('easy-invoice-edit') !== -1 ||
6 window.location.href.indexOf('page=easy-invoice-edit') !== -1;
7
8
9
10 // If we're on the builder page, don't initialize the old invoice builder
11 // as it will conflict with the new invoice-builder.js
12 if (isBuilderPage) {
13 return;
14 }
15
16 // Initialize invoice builder
17 function initInvoiceBuilder() {
18 // Wait for the template to be available
19 const template = document.getElementById('invoice-item-template');
20 if (!template) {
21 // Invoice item template not found
22 return;
23 }
24
25 // Initialize tabs
26 initTabs();
27
28 // Add event listener for "Add Item" button - now handled in invoice-builder.js
29
30 // Add event listener for removing items
31 $(document).on('click', '.remove-item', function() {
32 const item = $(this).closest('.invoice-item');
33 item.addClass('opacity-0');
34 setTimeout(() => {
35 item.remove();
36 updatePreview();
37 }, 200);
38 });
39
40 // Add event listener for collapsing/expanding items
41 $(document).on('click', '.item-collapse-toggle', function() {
42 const item = $(this).closest('.invoice-item');
43 const itemContent = item.find('.item-content');
44 const summaryElement = item.find('.item-collapsed-summary');
45 const icon = $(this).find('i');
46
47 // Update the summary before collapsing
48 if (itemContent.is(':visible')) {
49 updateItemSummary(item);
50 // Collapse
51 itemContent.slideUp(200);
52 summaryElement.slideDown(200);
53 icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
54 } else {
55 // Expand
56 itemContent.slideDown(200);
57 summaryElement.slideUp(200);
58 icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
59 }
60 });
61
62 // Add event listeners for all form inputs
63 $('#invoice-form').on('input', 'input, textarea', function() {
64 updatePreview();
65 });
66
67 // Add event listeners for taxable item clicking
68 $(document).on('click', '.taxable-item-container', function(e) {
69 // Don't toggle if clicking directly on the checkbox (let the default behavior handle it)
70 if (e.target.type === 'checkbox') {
71 return;
72 }
73
74 // Prevent default behavior if this is a label click to avoid double toggling
75 if (e.target.tagName.toLowerCase() === 'label') {
76 e.preventDefault();
77 }
78
79 const checkbox = $(this).find('input[type="checkbox"]');
80 checkbox.prop('checked', !checkbox.prop('checked'));
81 updatePreview();
82 });
83
84 // Add event listener for taxable checkbox changes
85 $(document).on('change', 'input[name="item-taxable[]"]', function() {
86 updatePreview();
87 });
88
89 // Add auto-calculate for quantity and price
90 $(document).on('input', 'input[name="item-quantity[]"], input[name="item-price[]"]', function() {
91 const item = $(this).closest('.invoice-item');
92 calculateItemTotal(item);
93 });
94
95 // Add keyboard shortcuts
96 $(document).on('keydown', function(e) {
97 // Ctrl/Cmd + Enter to save
98 if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
99 e.preventDefault();
100 $('button:contains("Save Draft")').click();
101 }
102
103 // Ctrl/Cmd + Shift + Enter to send
104 if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'Enter') {
105 e.preventDefault();
106 $('button:contains("Send Invoice")').click();
107 }
108 });
109
110 // Initialize with one item
111 }
112
113 // Initialize tab functionality
114 function initTabs() {
115 // Check if we're on an invoice page - if so, let invoice-builder.js handle tabs
116 if (window.location.href.includes('easy-invoice-builder') ||
117 window.location.href.includes('easy-invoice-new') ||
118 $('.invoice-item').length > 0) {
119 return;
120 }
121
122 // Add click event listeners to tab buttons
123 $('.tab-button').on('click', function() {
124 const tabId = $(this).data('tab');
125
126 // Remove active class from all tabs and buttons
127 $('.tab-button').removeClass('active');
128 $('.tab-button').removeClass('text-indigo-600').addClass('text-gray-500');
129 $('.tab-button').removeClass('border-indigo-500').addClass('border-transparent');
130 $('.tab-content').removeClass('active').addClass('hidden');
131
132 // Add active class to clicked tab and its content
133 $(this).addClass('active');
134 $(this).removeClass('text-gray-500').addClass('text-indigo-600');
135 $(this).removeClass('border-transparent').addClass('border-indigo-500');
136 $('#' + tabId).removeClass('hidden').addClass('active');
137
138 // If the items tab is active, make sure the items are properly sized
139 if (tabId === 'items-tab') {
140 updateItemSummaries();
141 }
142 });
143
144 // Handle tab navigation via keyboard
145 $('.tab-button').on('keydown', function(e) {
146 // Arrow right or arrow down
147 if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
148 e.preventDefault();
149 const nextTab = $(this).next('.tab-button');
150 if (nextTab.length) {
151 nextTab.click();
152 nextTab.focus();
153 }
154 }
155
156 // Arrow left or arrow up
157 if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
158 e.preventDefault();
159 const prevTab = $(this).prev('.tab-button');
160 if (prevTab.length) {
161 prevTab.click();
162 prevTab.focus();
163 }
164 }
165 });
166 }
167
168 // Update all item summaries
169 function updateItemSummaries() {
170 $('.invoice-item').each(function() {
171 updateItemSummary($(this));
172 });
173 }
174
175 // Add a new invoice item
176
177
178 // Calculate total for a single item
179 function calculateItemTotal(item) {
180 const quantity = parseFloat(item.find('input[name="item-quantity[]"]').val()) || 0;
181 const price = parseFloat(item.find('input[name="item-price[]"]').val()) || 0;
182 const isTaxable = item.find('input[name="item-taxable[]"]').is(':checked');
183 const pricesIncludeTax = $('#prices-include-tax').val() === 'yes';
184 const taxRate = parseFloat($('#tax-rate').val()) || 0;
185
186 let total = quantity * price;
187
188 // We don't need to adjust the displayed total in the item form
189 // The tax/non-tax calculation is handled in the updatePreview function
190
191 item.find('input[name="item-total[]"]').val(total.toFixed(2));
192
193 // Update the summary fields
194 updateItemSummary(item);
195
196 updatePreview();
197 }
198
199 // Update the collapsed summary of an item
200 function updateItemSummary(item) {
201 const quantity = parseFloat(item.find('input[name="item-quantity[]"]').val()) || 0;
202 const price = parseFloat(item.find('input[name="item-price[]"]').val()) || 0;
203 const total = quantity * price;
204
205 item.find('.quantity-summary').text(quantity);
206 item.find('.price-summary').text(price.toFixed(2));
207 item.find('.total-summary').text(total.toFixed(2));
208 }
209
210 // Update the preview panel
211 function updatePreview() {
212 // Add loading state
213 $('#preview-items').addClass('loading');
214
215 // Update customer info
216 $('#preview-customer-name').text($('#customer-name').val() || 'Customer Name');
217 $('#preview-customer-email').text($('#customer-email').val() || 'customer@example.com');
218
219 // Update invoice details
220 $('#preview-invoice-number').text($('#invoice-number').val());
221 $('#preview-invoice-date').text(formatDate($('#invoice-date').val()));
222 $('#preview-due-date').text(formatDate($('#due-date').val()));
223 $('#preview-invoice-title').text($('#invoice-title').val() || 'Invoice Title');
224 $('#preview-invoice-description').text($('#invoice-description').val() || 'Invoice description will appear here.');
225
226 // Get tax settings
227 const taxRate = parseFloat($('#tax-rate').val()) || 0;
228 const pricesIncludeTax = $('#prices-include-tax').val() === 'yes';
229
230 // Update items
231 let subtotal = 0;
232 let taxableSubtotal = 0;
233 const previewItemsContainer = $('#preview-items');
234 previewItemsContainer.empty();
235
236 $('.invoice-item').each(function() {
237 const title = $(this).find('input[name="item-title[]"]').val() || 'Item Title';
238 const description = $(this).find('textarea[name="item-description[]"]').val() || '';
239 const quantity = parseFloat($(this).find('input[name="item-quantity[]"]').val()) || 0;
240 let price = parseFloat($(this).find('input[name="item-price[]"]').val()) || 0;
241 const isTaxable = $(this).find('input[name="item-taxable[]"]').is(':checked');
242
243 // If prices include tax and this item is taxable, we need to extract the tax
244 // to get the real pre-tax price for calculations
245 let displayPrice = price;
246 if (pricesIncludeTax && isTaxable) {
247 price = price / (1 + (taxRate / 100));
248 }
249
250 const total = quantity * price;
251
252 subtotal += total;
253 if (isTaxable) {
254 taxableSubtotal += total;
255 }
256
257 const itemRow = `
258 <tr class="border-b border-gray-200">
259 <td class="px-3 py-4">
260 <div class="font-medium text-gray-900 flex items-center">
261 ${title}
262 ${isTaxable ? '<i class="fas fa-percentage ml-2 text-xs text-green-500" title="Taxable item"></i>' : ''}
263 </div>
264 ${description ? `<div class="text-sm text-gray-500">${description}</div>` : ''}
265 </td>
266 <td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 text-right">${quantity}</td>
267 <td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 text-right">$${pricesIncludeTax ? displayPrice.toFixed(2) : price.toFixed(2)}</td>
268 <td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 text-right">$${(quantity * (pricesIncludeTax ? displayPrice : price)).toFixed(2)}</td>
269 </tr>
270 `;
271 previewItemsContainer.append(itemRow);
272 });
273
274 // Get discount and tax values
275 const discountValue = parseFloat($('#discount').val()) || 0;
276 const discountType = $('#discount-type').val();
277 const calculationMethod = $('#calculation-method').val();
278
279 // Calculate discount amount based on type
280 let discountAmount = 0;
281 if (discountType === 'percentage') {
282 discountAmount = subtotal * (discountValue / 100);
283 } else { // fixed amount
284 discountAmount = Math.min(discountValue, subtotal); // Can't discount more than subtotal
285 }
286
287 // Calculate tax and total based on calculation method
288 let taxableAmount = 0;
289 let tax = 0;
290 let total = 0;
291
292 if (calculationMethod === 'before_tax') {
293 // Apply discount before calculating tax
294 // Distribute discount proportionally between taxable and non-taxable items
295 let taxableDiscount = 0;
296 if (subtotal > 0) {
297 taxableDiscount = discountAmount * (taxableSubtotal / subtotal);
298 }
299 taxableAmount = taxableSubtotal - taxableDiscount;
300 tax = taxableAmount * (taxRate / 100);
301 total = subtotal - discountAmount + tax;
302 } else { // after_tax
303 // Calculate tax first, then apply discount
304 tax = taxableSubtotal * (taxRate / 100);
305 total = subtotal + tax - discountAmount;
306 }
307
308 // For display purposes, if prices already include tax, we need to adjust the subtotal and tax display
309 let displaySubtotal = subtotal;
310 let displayTax = tax;
311
312 if (pricesIncludeTax) {
313 // If prices include tax, the subtotal shown should be the sum of entered prices
314 // which already include tax for taxable items
315 displaySubtotal = 0;
316 $('.invoice-item').each(function() {
317 const quantity = parseFloat($(this).find('input[name="item-quantity[]"]').val()) || 0;
318 const price = parseFloat($(this).find('input[name="item-price[]"]').val()) || 0;
319 displaySubtotal += quantity * price;
320 });
321
322 // The tax shown is the portion of the entered prices that represents tax
323 displayTax = taxableSubtotal * (taxRate / 100);
324
325 // Adjust the total if we're using the "before tax" calculation method
326 if (calculationMethod === 'before_tax') {
327 // The total remains the same, but the displayed components change
328 total = displaySubtotal - discountAmount;
329 }
330 }
331
332 // Ensure total is not negative
333 total = Math.max(0, total);
334
335 // Update preview elements
336 $('#preview-subtotal').text('$' + displaySubtotal.toFixed(2));
337 $('#preview-discount').text('$' + discountAmount.toFixed(2));
338 $('#preview-tax-rate').text(taxRate);
339 $('#preview-tax').text('$' + displayTax.toFixed(2));
340 $('#preview-total').text('$' + total.toFixed(2));
341
342 // Update notes and terms
343 $('#preview-notes').text($('#notes').val() || 'Thank you for your business!');
344 $('#preview-terms').text($('#terms').val() || 'Payment is due within 30 days. Please make checks payable to Company Name.');
345
346 // Remove loading state
347 setTimeout(() => {
348 $('#preview-items').removeClass('loading');
349 }, 300);
350 }
351
352 // Format date as MMMM D, YYYY
353 function formatDate(dateString) {
354 if (!dateString) return '';
355 const date = new Date(dateString);
356 return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
357 }
358
359 // Initialize all components
360 function init() {
361 initInvoiceBuilder();
362 }
363
364 // Start initialization
365 init();
366 });
367
368 function initSettingsForm() {
369 const $ = jQuery;
370
371 // Handle company logo upload
372 $('.settings-page').on('click', '.upload-logo', function(e) {
373 e.preventDefault();
374
375 const image = wp.media({
376 title: 'Upload Company Logo',
377 multiple: false
378 }).open()
379 .on('select', function() {
380 const uploadedImage = image.state().get('selection').first();
381 const imageUrl = uploadedImage.toJSON().url;
382
383 // Update logo preview
384 $('.company-logo-preview').attr('src', imageUrl);
385
386 // Store image URL in hidden field
387 $('#company-logo-url').val(imageUrl);
388 });
389 });
390
391 // Handle form submission
392 $('.settings-form').on('submit', function(e) {
393 e.preventDefault();
394
395 const formData = $(this).serialize();
396
397 $.ajax({
398 url: easyInvoice.ajaxUrl,
399 type: 'POST',
400 data: {
401 action: 'save_easy_invoice_settings',
402 nonce: easyInvoice.nonce,
403 ...formData
404 },
405 success: function(response) {
406 if (response.success) {
407 // Show success message
408 if (typeof EasyInvoiceToast !== 'undefined') {
409 EasyInvoiceToast.show('success', 'Settings saved successfully!');
410 }
411 } else {
412 // Show error message
413 if (typeof EasyInvoiceToast !== 'undefined') {
414 EasyInvoiceToast.show('error', 'Error saving settings. Please try again.');
415 }
416 }
417 },
418 error: function() {
419 if (typeof EasyInvoiceToast !== 'undefined') {
420 EasyInvoiceToast.show('error', 'Error saving settings. Please try again.');
421 }
422 }
423 });
424 });
425 }