PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.8
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.8
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 / quote-save.js

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

370 lines 16.4 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 if (qId && String(qId) !== '0') {
143 $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>');
144 }
145
146 // Update form with any returned data if needed
147 if (response.data.quote) {
148 // Update the quote status display if available
149 if (response.data.quote.status) {
150 $('#quote-status').val(response.data.quote.status);
151 }
152
153 // Reload client data if client_id is present
154 if (response.data.quote.client_id) {
155 // Update the global client data if we have it
156 if (response.data.client && typeof easyInvoice !== 'undefined') {
157 easyInvoice.clientData = response.data.client;
158
159 // Update client display fields directly
160 $('#client-name').val(response.data.client.name || '');
161 $('#client-email').val(response.data.client.email || '');
162 $('#client-phone').val(response.data.client.phone || '');
163 $('#client-address').val(response.data.client.address || '');
164 }
165 }
166
167 // Update any other fields if needed
168 }
169
170 $(document).trigger('easy-quote-saved', [response]);
171
172 } else {
173 $(document).trigger('easy-quote-save-failed', [response]);
174 // Show error message
175 const errorMessage = response.data && response.data.message ? response.data.message : 'Failed to save quote';
176 this.showNotification('error', errorMessage);
177
178 // Apply error states if validation errors are provided
179 if (response.data && response.data.errors) {
180 this.applyErrorStates(response.data.errors);
181 }
182 }
183 },
184 error: (xhr, status, error) => {
185 $(document).trigger('easy-quote-save-failed', [xhr]);
186
187 // Restore button state
188 $saveBtn.html(originalBtnText);
189 $saveBtn.prop('disabled', false).removeAttr('aria-busy');
190
191 // Show error notification
192 this.showNotification('error', 'Network error occurred while saving quote. Please try again.');
193 }
194 });
195 },
196
197 // Collect all form data
198 collectFormData: function() {
199
200 const formData = {};
201
202 // Get all form inputs
203 $('#quote-form').find('input, select, textarea').each(function() {
204 const $field = $(this);
205 const name = $field.attr('name');
206 const type = $field.attr('type');
207
208 if (name && name !== 'quote_nonce') {
209 let value;
210
211 if (type === 'checkbox') {
212 value = $field.is(':checked') ? '1' : '0';
213 } else if (type === 'radio') {
214 if ($field.is(':checked')) {
215 value = $field.val();
216 }
217 } else {
218 value = $field.val();
219 }
220
221 if (value !== undefined) {
222 formData[name] = value;
223 }
224 }
225 });
226
227 // Collect items data
228 formData.items = this.collectItemsData();
229
230 return formData;
231 },
232
233 // Collect items data
234 collectItemsData: function() {
235 const items = [];
236
237 $('.quote-item').each(function(index) {
238 const $item = $(this);
239
240 const itemData = {
241 title: $item.find('input[name="items[' + index + '][title]"]').val() || '',
242 description: $item.find('textarea[name="items[' + index + '][description]"]').val() || '',
243 quantity: parseFloat($item.find('input[name="items[' + index + '][quantity]"]').val()) || 0,
244 price: parseFloat($item.find('input[name="items[' + index + '][price]"]').val()) || 0,
245 adjust_percentage: parseFloat($item.find('input[name="items[' + index + '][adjust_percentage]"]').val()) || 0,
246 taxable: $item.find('input[name="items[' + index + '][taxable]"]').is(':checked')
247 };
248
249 // Let payment manager calculate totals
250 if (window.EasyInvoicePayment) {
251 const baseTotal = itemData.quantity * itemData.price;
252 let adjustPercentage = 0;
253 // Only apply adjust percentage if the adjust field is enabled
254 if (window.easyInvoice && window.easyInvoice.showAdjustField) {
255 adjustPercentage = itemData.adjust_percentage;
256 }
257 itemData.total = baseTotal * (1 + adjustPercentage / 100);
258
259 // Add tax and discount calculations from payment manager
260 const settings = window.EasyInvoicePayment.settings;
261 if (itemData.taxable && settings.taxRate > 0) {
262 if (settings.calculationMethod === 'before_tax') {
263 // Apply discount first, then tax
264 const discountAmount = settings.discountType === 'percentage'
265 ? itemData.total * (settings.discountValue / 100)
266 : (settings.discountValue / items.length); // Split fixed discount evenly
267 const afterDiscount = itemData.total - discountAmount;
268 itemData.tax_amount = afterDiscount * (settings.taxRate / 100);
269 itemData.total = afterDiscount + itemData.tax_amount;
270 } else {
271 // Calculate tax first, then apply discount
272 itemData.tax_amount = itemData.total * (settings.taxRate / 100);
273 const beforeDiscount = itemData.total + itemData.tax_amount;
274 const discountAmount = settings.discountType === 'percentage'
275 ? beforeDiscount * (settings.discountValue / 100)
276 : (settings.discountValue / items.length); // Split fixed discount evenly
277 itemData.total = beforeDiscount - discountAmount;
278 }
279 }
280 }
281
282 // Only add item if it has a title
283 if (itemData.title.trim()) {
284 items.push(itemData);
285 }
286 });
287
288 return items;
289 },
290
291 // Load quote data for editing
292 loadQuoteData: function() {
293 // This would be implemented if needed for pre-populating form fields
294 },
295
296 // Reload client data
297 reloadClientData: function(clientId) {
298 if (typeof easyInvoice !== 'undefined' && easyInvoice.ajaxUrl) {
299 $.ajax({
300 url: easyInvoice.ajaxUrl,
301 type: 'POST',
302 data: {
303 action: 'easy_invoice_get_client',
304 client_id: clientId,
305 nonce: easyInvoice.nonce
306 },
307 success: function(response) {
308 if (response.success && response.data.client) {
309 // Update client display fields
310 $('#client-name').val(response.data.client.name || '');
311 $('#client-email').val(response.data.client.email || '');
312 $('#client-phone').val(response.data.client.phone || '');
313 $('#client-address').val(response.data.client.address || '');
314
315 // Update global client data
316 if (typeof easyInvoice !== 'undefined') {
317 easyInvoice.clientData = response.data.client;
318 }
319 }
320 }
321 });
322 }
323 },
324
325 // Show notification
326 showNotification: function(type, message) {
327
328 // Create notification element
329 const notification = $(`
330 <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">
331 <div class="flex items-center">
332 <div class="flex-shrink-0">
333 <i class="fas ${type === 'success' ? 'fa-check' : 'fa-exclamation-triangle'}"></i>
334 </div>
335 <div class="ml-3">
336 <p class="text-sm font-medium">${message}</p>
337 </div>
338 <div class="ml-auto pl-3">
339 <button class="text-white hover:text-gray-200" onclick="$(this).parent().parent().parent().remove()">
340 <i class="fas fa-times"></i>
341 </button>
342 </div>
343 </div>
344 </div>
345 `);
346
347 // Add to page
348 $('body').append(notification);
349
350 // Auto-remove after 5 seconds
351 setTimeout(() => {
352 notification.fadeOut(() => notification.remove());
353 }, 5000);
354 },
355
356 // Update preview
357 updatePreview: function() {
358 // This would be implemented to update the live preview
359 if (window.EasyInvoiceBuilder && typeof window.EasyInvoiceBuilder.updatePreview === 'function') {
360 window.EasyInvoiceBuilder.updatePreview();
361 }
362 }
363 };
364
365 // Initialize when document is ready
366 $(document).ready(function() {
367 window.EasyQuoteSave.init();
368 });
369
370 })(jQuery);