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-form.js

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

490 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($) {
2 // Determine if this is a quote form
3 const isQuoteForm = window.isQuoteForm || false;
4 const formType = isQuoteForm ? 'quote' : 'invoice';
5 const templateFieldName = isQuoteForm ? 'quote_template' : 'invoice_template';
6 const previewClass = isQuoteForm ? 'quote-preview' : 'invoice-preview';
7
8 // Function to get URL parameter
9 function getUrlParameter(name) {
10 name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
11 var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
12 var results = regex.exec(location.search);
13 return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
14 }
15
16 // Function to switch to a specific tab
17 function switchToTab(tabId) {
18 if (tabId && $('#' + tabId).length) {
19 // Remove active state from all tabs
20 $('.tab-button').removeClass('border-indigo-500 text-indigo-600').addClass('border-transparent text-gray-500');
21 $('.tab-content').addClass('hidden');
22
23 // Activate the specified tab
24 $('.tab-button[data-tab="' + tabId + '"]').addClass('border-indigo-500 text-indigo-600');
25 $('#' + tabId).removeClass('hidden');
26
27 // Update URL without page reload
28 var url = new URL(window.location);
29 url.searchParams.set('tab', tabId);
30 window.history.replaceState({}, '', url);
31 }
32 }
33
34 // Make switchToTab globally available
35 window.switchToTab = switchToTab;
36
37 // Check for tab parameter on page load
38 var initialTab = getUrlParameter('tab');
39 if (initialTab) {
40 switchToTab(initialTab);
41 }
42
43 // Tab switching
44 $('.tab-button').on('click', function() {
45 var tabId = $(this).data('tab');
46 switchToTab(tabId);
47 });
48
49 // Remove the updateTemplatePreview function and all quote-specific template preview logic
50 // (No need to handle quote template preview here anymore)
51
52 // Handle template selection after page reload
53 const selectedTemplate = sessionStorage.getItem('selectedTemplate');
54 if (selectedTemplate) {
55
56
57 // Update the hidden input field
58 $('input[name="' + templateFieldName + '"][type="hidden"]').val(selectedTemplate);
59
60 // Update the radio button if it exists
61 let radioInput = $('input[name="' + templateFieldName + '"][type="radio"][value="' + selectedTemplate + '"]');
62 if (radioInput.length > 0) {
63 $('input[name="' + templateFieldName + '"][type="radio"]').prop('checked', false);
64 radioInput.prop('checked', true);
65 }
66
67 // Update the visual state and trigger click event
68 $('.template-card').removeClass('selected');
69 const selectedCard = $('.template-card[data-template-id="' + selectedTemplate + '"]');
70 if (selectedCard.length > 0) {
71 selectedCard.addClass('selected');
72 // Trigger click event to ensure all handlers are executed
73 selectedCard.trigger('click');
74 }
75
76 // Clear the sessionStorage
77 sessionStorage.removeItem('selectedTemplate');
78 } else {
79 // Handle initial template state for existing invoices/quotes
80 const currentTemplate = $('input[name="' + templateFieldName + '"][type="hidden"]').val();
81
82 if (currentTemplate) {
83
84
85 // Update the radio button if it exists
86 let radioInput = $('input[name="' + templateFieldName + '"][type="radio"][value="' + currentTemplate + '"]');
87 if (radioInput.length > 0) {
88 $('input[name="' + templateFieldName + '"][type="radio"]').prop('checked', false);
89 radioInput.prop('checked', true);
90 }
91
92 // Update the visual state
93 $('.template-card').removeClass('selected');
94 const selectedCard = $('.template-card[data-template-id="' + currentTemplate + '"]');
95 if (selectedCard.length > 0) {
96 selectedCard.addClass('selected');
97 }
98
99 // Update the preview
100 //updateTemplatePreview(currentTemplate);
101 }
102 }
103
104
105
106 // Initialize item counter
107 let itemCounter = $('.invoice-item, .quote-item').length;
108
109 // Auto-calculate totals when quantity or price changes
110 $(document).on('input', 'input[name="item-quantity[]"], input[name="item-price[]"]', function() {
111 // This will be handled by invoice-builder.js
112 if (window.EasyInvoiceBuilder && typeof window.EasyInvoiceBuilder.calculateItemTotal === 'function') {
113 window.EasyInvoiceBuilder.calculateItemTotal($(this).closest('.invoice-item, .quote-item'));
114 }
115 });
116
117 // Both buttons should open the template gallery - using event delegation for better reliability
118 $(document).on('click', '#browse-all-templates, #browse-templates-btn', function(e) {
119 e.preventDefault();
120 e.stopPropagation();
121
122 // Check if this is the free version
123 const isFreeVersion = !window.easyInvoice || !window.easyInvoice.isPro;
124
125 if (isFreeVersion) {
126 // Show premium upgrade modal for free users
127 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
128 const title = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.premiumTemplates : 'Premium Templates';
129 const description = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.premiumTemplatesDescription : 'Access all premium templates including Modern, Professional, Classic, and more to give your invoices a unique and professional look.';
130 EasyInvoiceConfirmation.showFeatureUpgrade(title, description);
131 } else {
132 const message = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.premiumFeatureMessage : 'This is a premium feature. Please upgrade to Pro to access all templates.';
133 if (typeof EasyInvoiceToast !== 'undefined') {
134 EasyInvoiceToast.show('info', message);
135 }
136 }
137 } else {
138 // Pro users can browse all templates
139 $('#template-gallery-modal').fadeIn();
140
141 // Highlight the currently selected template in the gallery
142 const currentTemplate = $('input[name="' + templateFieldName + '"]:checked').val();
143 if (currentTemplate) {
144 $('.gallery-template-card').removeClass('selected');
145 $('.gallery-template-card[data-template-id="' + currentTemplate + '"]').addClass('selected');
146 }
147 }
148 });
149
150
151
152 // Template card click handler in main grid
153 $('.template-card').on('click', function(e) {
154 // If clicking on premium upgrade button, handle separately
155 if ($(e.target).hasClass('premium-upgrade-btn') || $(e.target).closest('.premium-upgrade-btn').length) {
156 return;
157 }
158
159 const templateId = $(this).data('template-id');
160 const radioInput = $(this).find('input[type="radio"]');
161
162 // Check if this is a premium template in free version
163 const isPremium = $(this).attr('data-is-premium') === 'true';
164 const isFreeVersion = !window.easyInvoice || !window.easyInvoice.isPro;
165
166 if (isPremium && isFreeVersion) {
167 // Show premium upgrade modal instead of selecting the template
168 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
169 const title = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.premiumTemplates : 'Premium Templates';
170 const description = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.premiumTemplatesDescription : 'Access all premium templates including Modern, Professional, Classic, and more to give your invoices a unique and professional look.';
171 EasyInvoiceConfirmation.showFeatureUpgrade(title, description);
172 } else {
173 const message = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.premiumFeatureMessage : 'This is a premium feature. Please upgrade to Pro to access all templates.';
174 if (typeof EasyInvoiceToast !== 'undefined') {
175 EasyInvoiceToast.show('info', message);
176 }
177 }
178 return;
179 }
180
181 // Check the radio input within this card
182 radioInput.prop('checked', true);
183
184 // Update the hidden input field value
185 $('input[name="' + templateFieldName + '"][type="hidden"]').val(templateId);
186
187 // Update the visual state
188 $('.template-card').removeClass('selected');
189 $(this).addClass('selected');
190
191 // No need to call updateTemplatePreview for quotes
192 });
193
194
195
196 // Apply button click handler
197 $('#apply-selected-template').on('click', function() {
198 // Get the selected template from the gallery
199 const selectedGalleryCard = $('.gallery-template-card.selected');
200
201 if (selectedGalleryCard.length === 0) {
202 // If no template is selected, show an error message and return
203 const message = window.easyInvoice && window.easyInvoice.translations ? window.easyInvoice.translations.selectTemplateFirst : 'Please select a template first';
204 if (typeof EasyInvoiceToast !== 'undefined') {
205 EasyInvoiceToast.show('error', message);
206 }
207 return;
208 }
209
210 const templateId = selectedGalleryCard.data('template-id');
211 const templateName = selectedGalleryCard.find('.font-medium').text();
212 const templateDescription = selectedGalleryCard.find('.text-sm.text-gray-600').text();
213 const templateIcon = selectedGalleryCard.find('i').attr('class');
214 const isPremium = selectedGalleryCard.data('is-premium') === 'true';
215
216 // Store the selected template in sessionStorage for after page reload
217 sessionStorage.setItem('selectedTemplate', templateId);
218
219 // Update the hidden input field value
220 $('input[name="' + templateFieldName + '"][type="hidden"]').val(templateId);
221
222 // Update the main template grid
223 updateMainTemplateGrid(templateId, templateName, templateDescription, templateIcon, isPremium);
224
225 // Close the modal
226 $('#template-gallery-modal').fadeOut();
227 });
228
229 // Close gallery modal
230 $('.close-gallery-modal, .gallery-close-btn').on('click', function() {
231 $('#template-gallery-modal').fadeOut();
232 });
233
234 // Close gallery when clicking outside of it
235 $(window).on('click', function(e) {
236 if ($(e.target).is('#template-gallery-modal')) {
237 $('#template-gallery-modal').fadeOut();
238 }
239 });
240
241 // Gallery template card click handler - just selects the template visually
242 $('.gallery-template-card').on('click', function(e) {
243 // If clicking on the favorite icon, handle separately
244 if ($(e.target).hasClass('template-favorite') || $(e.target).closest('.template-favorite').length) {
245 return;
246 }
247
248 // Update the visual state in the gallery
249 $('.gallery-template-card').removeClass('selected');
250 $(this).addClass('selected');
251
252
253 });
254
255
256
257
258
259 // Client selection functionality - only for invoice forms, not quote forms
260 if (!isQuoteForm) {
261 $('#select-client').on('change', function() {
262 const selectedClientId = $(this).val();
263 const $clientInfoDisplay = $('#client-info-display');
264 const $noClientMessage = $('#no-client-message');
265 const $clientIdField = $('#client-id');
266
267 if (selectedClientId) {
268 // Set the hidden client ID field
269 $clientIdField.val(selectedClientId);
270
271 // Get client information via AJAX
272 loadClientInformation(selectedClientId);
273
274 // Show client info display, hide no client message
275 $clientInfoDisplay.removeClass('hidden');
276 $noClientMessage.addClass('hidden');
277 } else {
278 // Clear the hidden client ID field
279 $clientIdField.val('');
280
281 // Hide client info display, show no client message
282 $clientInfoDisplay.addClass('hidden');
283 $noClientMessage.removeClass('hidden');
284
285 // Clear all display fields
286 clearClientDisplay();
287 }
288 });
289 }
290
291 // Function to load client information
292 function loadClientInformation(clientId) {
293 // First check if we have client data already loaded from PHP
294 if (typeof easyInvoice !== 'undefined' && easyInvoice.clientData && easyInvoice.clientData.id == clientId) {
295 const clientData = easyInvoice.clientData;
296 populateClientDisplay(clientData);
297 return;
298 }
299
300 // If we don't have the client data pre-loaded, we should load it via PHP
301 // This should not happen in normal circumstances since client data is loaded on page load
302 // Client data not found in pre-loaded data
303 clearClientDisplay();
304 }
305
306 // Function to populate client display
307 function populateClientDisplay(clientData) {
308 $('#display-client-name').text(clientData.name || '-');
309 $('#display-client-email').text(clientData.email || '-');
310 $('#display-client-phone').text(clientData.phone || '-');
311 $('#display-client-company').text(clientData.company || '-');
312 $('#display-client-address').text(clientData.address || '-');
313 $('#display-client-website').text(clientData.website || '-');
314 }
315
316 // Function to clear client display
317 function clearClientDisplay() {
318 $('#display-client-name').text('-');
319 $('#display-client-email').text('-');
320 $('#display-client-phone').text('-');
321 $('#display-client-company').text('-');
322 $('#display-client-address').text('-');
323 $('#display-client-website').text('-');
324 }
325
326 // Function to update main template grid when template is selected from popup
327 function updateMainTemplateGrid(templateId, templateName, templateDescription, templateIcon, isPremium) {
328 const $mainGrid = $('#main-templates-grid');
329 const $existingCards = $mainGrid.find('.template-card');
330
331 // Check if the selected template already exists in the main grid
332 const $existingCard = $mainGrid.find('.template-card[data-template-id="' + templateId + '"]');
333
334 if ($existingCard.length > 0) {
335 // Template already exists, just select it and don't add duplicates
336 $existingCard.addClass('selected');
337 $existingCard.find('input[type="radio"]').prop('checked', true);
338 $mainGrid.find('.template-card').not($existingCard).removeClass('selected');
339 return;
340 }
341
342 // If we have 3 templates and the new one doesn't exist, replace the currently selected one
343 if ($existingCards.length >= 3) {
344 const $selectedCard = $mainGrid.find('.template-card.selected');
345 if ($selectedCard.length > 0) {
346 $selectedCard.remove();
347 } else {
348 // If no template is selected, remove the first one
349 $existingCards.first().remove();
350 }
351 }
352
353 // Create new template card HTML
354 const isFreeVersion = !window.easyInvoice || !window.easyInvoice.isPro;
355 const premiumClasses = (isPremium && isFreeVersion) ? 'premium-template-blurred' : '';
356 const disabledAttr = (isPremium && isFreeVersion) ? 'disabled' : '';
357
358 const newCardHtml = `
359 <div class="template-card selected ${premiumClasses}" data-template-id="${templateId}" data-is-premium="${isPremium}">
360 <input type="radio" id="template-${templateId}" name="${templateFieldName}" value="${templateId}" class="template-radio" checked ${disabledAttr}>
361 <div class="p-4 border rounded-lg">
362 <div class="flex items-center mb-2">
363 <i class="${templateIcon} text-indigo-600 mr-2"></i>
364 <span class="font-medium">${templateName}</span>
365 </div>
366 <p class="text-sm text-gray-600">${templateDescription}</p>
367 <div class="mt-2 flex-grow">
368 <div class="template-preview bg-gray-100 rounded h-16 flex items-center justify-center ${templateId}">
369 </div>
370 </div>
371 </div>
372 </div>
373 `;
374
375 // Add the new template card to the main grid
376 $mainGrid.append(newCardHtml);
377
378 // Update the visual state - ensure only the new template is selected
379 $mainGrid.find('.template-card').removeClass('selected');
380 $mainGrid.find('.template-card[data-template-id="' + templateId + '"]').addClass('selected');
381 }
382
383 // Save to Item Library functionality
384 $(document).on('click', '.save-to-library-btn', function(e) {
385 e.preventDefault();
386
387 var $btn = $(this);
388 var itemIndex = $btn.data('item-index');
389 // Find the closest item container (either invoice-item or quote-item)
390 var $itemContainer = $btn.closest('.invoice-item, .quote-item');
391
392 if (!$itemContainer.length) {
393 return;
394 }
395
396 // Get item data from the form
397 var itemData = {
398 title: '',
399 description: '',
400 price: 0,
401 quantity: 0,
402 taxable: 0
403 };
404
405 // Extract data from the item fields
406 $itemContainer.find('input, textarea, select').each(function() {
407 var $field = $(this);
408 var name = $field.attr('name');
409 var value = $field.val();
410 var type = $field.attr('type');
411
412 if (name) {
413 // Handle array names like items[0][title] or quote_items[0][title]
414 var matches = name.match(/(items|quote_items)\[\d+\]\[(.+)\]/);
415 if (matches && matches[2]) {
416 if (type === 'checkbox' || type === 'radio') {
417 itemData[matches[2]] = $field.is(':checked') ? 1 : 0;
418 } else {
419 itemData[matches[2]] = value;
420 }
421 }
422 }
423 });
424
425 // Convert numeric fields
426 itemData.price = parseFloat(itemData.price) || 0;
427 itemData.quantity = parseInt(itemData.quantity) || 0;
428 itemData.taxable = parseInt(itemData.taxable) || 0;
429
430 // Validate required fields
431 if (!itemData.title || !itemData.title.trim()) {
432 if (typeof EasyInvoiceToast !== 'undefined') {
433 EasyInvoiceToast.error('Please fill in item title before saving to library');
434 } else {
435 alert('Please fill in item title before saving to library');
436 }
437 return;
438 }
439
440 if (!itemData.price || itemData.price <= 0) {
441 if (typeof EasyInvoiceToast !== 'undefined') {
442 EasyInvoiceToast.error('Please fill in item price before saving to library');
443 } else {
444 alert('Please fill in item price before saving to library');
445 }
446 return;
447 }
448
449 // Show loading state
450 var originalHtml = $btn.html();
451 $btn.html('<i class="fas fa-spinner fa-spin"></i>').prop('disabled', true);
452
453 // AJAX call to save to item library
454 $.ajax({
455 url: ajaxurl,
456 type: 'POST',
457 data: {
458 action: 'easy_invoice_pro_add_item',
459 nonce: $btn.data('nonce'),
460 item_data: JSON.stringify(itemData)
461 },
462 success: function(response) {
463 if (response.success) {
464 if (typeof EasyInvoiceToast !== 'undefined') {
465 EasyInvoiceToast.success('Item saved to library successfully!');
466 } else {
467 alert('Item saved to library successfully!');
468 }
469 } else {
470 if (typeof EasyInvoiceToast !== 'undefined') {
471 EasyInvoiceToast.error(response.data.message || 'Failed to save item');
472 } else {
473 alert('Failed to save item: ' + (response.data.message || 'Unknown error'));
474 }
475 }
476 },
477 error: function() {
478 if (typeof EasyInvoiceToast !== 'undefined') {
479 EasyInvoiceToast.error('Error saving item to library');
480 } else {
481 alert('Error saving item to library');
482 }
483 },
484 complete: function() {
485 // Restore button
486 $btn.html(originalHtml).prop('disabled', false);
487 }
488 });
489 });
490 });