PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.4
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.4
2.4.3 2.4.4 2.4.2 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 All 60 releases
easy-invoice / assets / js / quote-save.js

quote-save.js in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.4, at assets/js/quote-save.js

374 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Quote Save Functionality
3 *
4 * Handles saving and updating quotes in the quote builder page.
5 */
6 (function($) {
7 'use strict';
8
9 function eiBuilderI18n(key, fallback) {
10 if (typeof easyInvoice !== 'undefined' && easyInvoice.i18n && easyInvoice.i18n[key]) {
11 return easyInvoice.i18n[key];
12 }
13 return fallback || '';
14 }
15
16 var spinnerSvg = '<svg class="ei-btn-spinner h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
17
18 // Create a global object for quote save functionality
19 window.EasyQuoteSave = {
20 init: function() {
21
22 this.bindEvents();
23
24 // Load quote data if in edit mode
25 if ($('#quote-id').val()) {
26 this.loadQuoteData();
27 }
28 },
29
30 // Clear all error states
31 clearErrorStates: function() {
32 $('.form-input, .form-select, .form-textarea').removeClass('error');
33 $('.field-error').remove();
34 },
35
36 // Apply error states to specific fields
37 applyErrorStates: function(errors) {
38 this.clearErrorStates();
39
40 if (errors && typeof errors === 'object') {
41 Object.keys(errors).forEach(fieldName => {
42 const field = $(`[name="${fieldName}"]`);
43 if (field.length) {
44 field.addClass('error');
45
46 // Add error message below the field
47 const errorMessage = errors[fieldName];
48 if (errorMessage) {
49 const errorHtml = `<div class="field-error text-red-500 text-sm mt-1">${errorMessage}</div>`;
50 field.closest('div').append(errorHtml);
51 }
52 }
53 });
54 }
55 },
56
57 bindEvents: function() {
58
59
60 // Count save buttons to make sure it exists
61 const saveButtonCount = $('#save-quote-btn').length;
62
63 // Only bind the click event if the button exists
64 if (saveButtonCount > 0) {
65 // Save quote button click handler - jQuery method
66 $('#save-quote-btn').on('click', this.handleSaveButtonClick.bind(this));
67 } else {
68 // Save button not found in the DOM
69 }
70
71 // Add form submit handler to trigger save button
72 $('#quote-form').on('submit', function(e) {
73 e.preventDefault();
74 $('#save-quote-btn').click();
75 });
76
77 // Clear error states when user starts typing
78 $('.form-input, .form-select, .form-textarea').on('input change', function() {
79 const field = $(this);
80 if (field.hasClass('error')) {
81 field.removeClass('error');
82 field.siblings('.field-error').remove();
83 }
84 });
85 },
86
87 // Handle save button click event
88 handleSaveButtonClick: function(e) {
89 e.preventDefault();
90 this.saveQuote();
91 },
92
93 // Function to save or update quote
94 saveQuote: function() {
95
96 // Show loading state
97 const $saveBtn = $('#save-quote-btn');
98 const originalBtnText = $saveBtn.html();
99 $saveBtn.html('<span class="inline-flex items-center gap-2">' + spinnerSvg + '<span>' + eiBuilderI18n('saving', 'Saving…') + '</span></span>');
100 $saveBtn.prop('disabled', true).attr('aria-busy', 'true');
101
102 // Collect form data
103 const formData = this.collectFormData();
104
105
106
107 // Backend validation will handle all field validation
108 // No need for frontend validation since we have comprehensive backend validation
109
110 // Prepare the request data
111 const requestData = {
112 action: 'easy_invoice_save_quote',
113 nonce: $('#quote_nonce').val(),
114 quote_data: formData
115 };
116
117
118
119 // Send AJAX request
120 $.ajax({
121 url: easyInvoice.ajaxUrl,
122 type: 'POST',
123 data: requestData,
124
125 success: (response) => {
126
127 // Restore button state
128 $saveBtn.html(originalBtnText);
129 $saveBtn.prop('disabled', false).removeAttr('aria-busy');
130
131 if (response.success) {
132 // Clear any previous error states
133 this.clearErrorStates();
134
135 // Do not show toast here; global handler will do it
136
137 var qId = $('#quote-id').val();
138 if (response.data.quote && response.data.quote.id) {
139 $('#quote-id').val(response.data.quote.id);
140 qId = response.data.quote.id;
141 }
142 // Same as the invoice builder: a refresh must reopen this quote, not a blank form.
143 if (qId && String(qId) !== '0' && window.history && window.history.replaceState && !/[?&]id=/.test(window.location.search)) {
144 var eiUrl = new URL(window.location.href); eiUrl.searchParams.set('id', String(qId)); window.history.replaceState({}, '', eiUrl.toString());
145 }
146 if (qId && String(qId) !== '0') {
147 $saveBtn.html('<span class="inline-flex items-center gap-2"><svg class="h-4 w-4 text-gray-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" /></svg><span>' + eiBuilderI18n('update_quote', 'Update Quote') + '</span></span>');
148 }
149
150 // Update form with any returned data if needed
151 if (response.data.quote) {
152 // Update the quote status display if available
153 if (response.data.quote.status) {
154 $('#quote-status').val(response.data.quote.status);
155 }
156
157 // Reload client data if client_id is present
158 if (response.data.quote.client_id) {
159 // Update the global client data if we have it
160 if (response.data.client && typeof easyInvoice !== 'undefined') {
161 easyInvoice.clientData = response.data.client;
162
163 // Update client display fields directly
164 $('#client-name').val(response.data.client.name || '');
165 $('#client-email').val(response.data.client.email || '');
166 $('#client-phone').val(response.data.client.phone || '');
167 $('#client-address').val(response.data.client.address || '');
168 }
169 }
170
171 // Update any other fields if needed
172 }
173
174 $(document).trigger('easy-quote-saved', [response]);
175
176 } else {
177 $(document).trigger('easy-quote-save-failed', [response]);
178 // Show error message
179 const errorMessage = response.data && response.data.message ? response.data.message : 'Failed to save quote';
180 this.showNotification('error', errorMessage);
181
182 // Apply error states if validation errors are provided
183 if (response.data && response.data.errors) {
184 this.applyErrorStates(response.data.errors);
185 }
186 }
187 },
188 error: (xhr, status, error) => {
189 $(document).trigger('easy-quote-save-failed', [xhr]);
190
191 // Restore button state
192 $saveBtn.html(originalBtnText);
193 $saveBtn.prop('disabled', false).removeAttr('aria-busy');
194
195 // Show error notification
196 this.showNotification('error', 'Network error occurred while saving quote. Please try again.');
197 }
198 });
199 },
200
201 // Collect all form data
202 collectFormData: function() {
203
204 const formData = {};
205
206 // Get all form inputs
207 $('#quote-form').find('input, select, textarea').each(function() {
208 const $field = $(this);
209 const name = $field.attr('name');
210 const type = $field.attr('type');
211
212 if (name && name !== 'quote_nonce') {
213 let value;
214
215 if (type === 'checkbox') {
216 value = $field.is(':checked') ? '1' : '0';
217 } else if (type === 'radio') {
218 if ($field.is(':checked')) {
219 value = $field.val();
220 }
221 } else {
222 value = $field.val();
223 }
224
225 if (value !== undefined) {
226 formData[name] = value;
227 }
228 }
229 });
230
231 // Collect items data
232 formData.items = this.collectItemsData();
233
234 return formData;
235 },
236
237 // Collect items data
238 collectItemsData: function() {
239 const items = [];
240
241 $('.quote-item').each(function(index) {
242 const $item = $(this);
243
244 const itemData = {
245 title: $item.find('input[name="items[' + index + '][title]"]').val() || '',
246 description: $item.find('textarea[name="items[' + index + '][description]"]').val() || '',
247 quantity: parseFloat($item.find('input[name="items[' + index + '][quantity]"]').val()) || 0,
248 price: parseFloat($item.find('input[name="items[' + index + '][price]"]').val()) || 0,
249 adjust_percentage: parseFloat($item.find('input[name="items[' + index + '][adjust_percentage]"]').val()) || 0,
250 taxable: $item.find('input[name="items[' + index + '][taxable]"]').is(':checked')
251 };
252
253 // Let payment manager calculate totals
254 if (window.EasyInvoicePayment) {
255 const baseTotal = itemData.quantity * itemData.price;
256 let adjustPercentage = 0;
257 // Only apply adjust percentage if the adjust field is enabled
258 if (window.easyInvoice && window.easyInvoice.showAdjustField) {
259 adjustPercentage = itemData.adjust_percentage;
260 }
261 itemData.total = baseTotal * (1 + adjustPercentage / 100);
262
263 // Add tax and discount calculations from payment manager
264 const settings = window.EasyInvoicePayment.settings;
265 if (itemData.taxable && settings.taxRate > 0) {
266 if (settings.calculationMethod === 'before_tax') {
267 // Apply discount first, then tax
268 const discountAmount = settings.discountType === 'percentage'
269 ? itemData.total * (settings.discountValue / 100)
270 : (settings.discountValue / items.length); // Split fixed discount evenly
271 const afterDiscount = itemData.total - discountAmount;
272 itemData.tax_amount = afterDiscount * (settings.taxRate / 100);
273 itemData.total = afterDiscount + itemData.tax_amount;
274 } else {
275 // Calculate tax first, then apply discount
276 itemData.tax_amount = itemData.total * (settings.taxRate / 100);
277 const beforeDiscount = itemData.total + itemData.tax_amount;
278 const discountAmount = settings.discountType === 'percentage'
279 ? beforeDiscount * (settings.discountValue / 100)
280 : (settings.discountValue / items.length); // Split fixed discount evenly
281 itemData.total = beforeDiscount - discountAmount;
282 }
283 }
284 }
285
286 // Only add item if it has a title
287 if (itemData.title.trim()) {
288 items.push(itemData);
289 }
290 });
291
292 return items;
293 },
294
295 // Load quote data for editing
296 loadQuoteData: function() {
297 // This would be implemented if needed for pre-populating form fields
298 },
299
300 // Reload client data
301 reloadClientData: function(clientId) {
302 if (typeof easyInvoice !== 'undefined' && easyInvoice.ajaxUrl) {
303 $.ajax({
304 url: easyInvoice.ajaxUrl,
305 type: 'POST',
306 data: {
307 action: 'easy_invoice_get_client',
308 client_id: clientId,
309 nonce: easyInvoice.nonce
310 },
311 success: function(response) {
312 if (response.success && response.data.client) {
313 // Update client display fields
314 $('#client-name').val(response.data.client.name || '');
315 $('#client-email').val(response.data.client.email || '');
316 $('#client-phone').val(response.data.client.phone || '');
317 $('#client-address').val(response.data.client.address || '');
318
319 // Update global client data
320 if (typeof easyInvoice !== 'undefined') {
321 easyInvoice.clientData = response.data.client;
322 }
323 }
324 }
325 });
326 }
327 },
328
329 // Show notification
330 showNotification: function(type, message) {
331
332 // Create notification element
333 const notification = $(`
334 <div class="fixed top-4 right-4 z-50 p-4 rounded-md shadow-lg max-w-sm ${type === 'success' ? 'bg-green-500' : 'bg-red-500'} text-white">
335 <div class="flex items-center">
336 <div class="flex-shrink-0">
337 <i class="fas ${type === 'success' ? 'fa-check' : 'fa-exclamation-triangle'}"></i>
338 </div>
339 <div class="ml-3">
340 <p class="text-sm font-medium">${message}</p>
341 </div>
342 <div class="ml-auto pl-3">
343 <button class="text-white hover:text-gray-200" onclick="$(this).parent().parent().parent().remove()">
344 <i class="fas fa-times"></i>
345 </button>
346 </div>
347 </div>
348 </div>
349 `);
350
351 // Add to page
352 $('body').append(notification);
353
354 // Auto-remove after 5 seconds
355 setTimeout(() => {
356 notification.fadeOut(() => notification.remove());
357 }, 5000);
358 },
359
360 // Update preview
361 updatePreview: function() {
362 // This would be implemented to update the live preview
363 if (window.EasyInvoiceBuilder && typeof window.EasyInvoiceBuilder.updatePreview === 'function') {
364 window.EasyInvoiceBuilder.updatePreview();
365 }
366 }
367 };
368
369 // Initialize when document is ready
370 $(document).ready(function() {
371 window.EasyQuoteSave.init();
372 });
373
374 })(jQuery);