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 / client-manager.js

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

636 lines 32.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Easy Invoice - Client Management
3 * Handles all client-related functionality including selection, adding new clients,
4 * and managing client data.
5 */
6
7 (function($) {
8 $(document).ready(function() {
9 // Store client data in a JavaScript object
10 var clientsData = {};
11
12 // Load clients data from the global object if available
13 function initializeClientData() {
14 if (typeof window.easyInvoiceClients !== 'undefined') {
15 clientsData = window.easyInvoiceClients;
16 }
17
18 // Make clientsData available globally
19 window.easyInvoiceClients = clientsData;
20 }
21
22 // Initialize client data
23 initializeClientData();
24
25 // Handle client selection change
26 function setupClientSelection() {
27 var isInitialLoad = true; // Flag to prevent AJAX calls on initial load
28
29 $("#select-client").on("change", function() {
30 var clientId = $(this).val();
31
32 // Skip AJAX call if this is the initial load or if we're initializing client
33 if (isInitialLoad || (typeof window.isInitializingClient !== 'undefined' && window.isInitializingClient)) {
34 isInitialLoad = false;
35 return;
36 }
37
38 if (clientId) {
39 // Load client data via AJAX
40 loadClientData(clientId);
41 } else {
42 // Clear client display
43 clearClientDisplay();
44 }
45 });
46
47 // Auto-select client if client-id field has a value (for existing invoices)
48 var existingClientId = $("#client-id").val();
49 if (existingClientId && existingClientId !== '') {
50 // Set the dropdown value without triggering change event
51 $("#select-client").val(existingClientId);
52
53 // Check if we have client data from PHP first
54 if (typeof easyInvoice !== 'undefined' && easyInvoice.clientData && easyInvoice.clientData.id == existingClientId) {
55 loadClientDataFromPHP(easyInvoice.clientData);
56 }
57 }
58
59 // Reset the flag after a short delay to allow for user interactions
60 setTimeout(function() {
61 isInitialLoad = false;
62 }, 1000);
63 }
64
65 // Load client data from PHP (no AJAX needed)
66 function loadClientDataFromPHP(clientData) {
67 // Set client ID
68 $("#client-id").val(clientData.id);
69
70 // Update display fields
71 $("#display-client-name").text(clientData.business_client_name || (clientData.first_name + ' ' + clientData.last_name) || 'N/A');
72 $("#display-client-email").text(clientData.email || 'N/A');
73 $("#display-client-phone").text(clientData.phone || 'N/A');
74 $("#display-client-company").text(clientData.business_client_name || 'N/A');
75 $("#display-client-address").text(clientData.address || 'N/A');
76 $("#display-client-website").text(clientData.website || 'N/A');
77
78 // Show client info display and hide no-client message
79 $("#client-info-display").removeClass("hidden");
80 $("#no-client-message").addClass("hidden");
81
82 // Update the new selected client display
83 var clientName = clientData.business_client_name || (clientData.first_name + ' ' + clientData.last_name) || 'N/A';
84 $("#selected-client-name").text(clientName);
85 $("#selected-client-email").text(clientData.email || 'N/A');
86 $("#selected-client-display").removeClass("hidden");
87
88 // Update the edit client link
89 var editUrl = 'admin.php?page=easy-invoice-client-edit&client_id=' + clientData.id;
90 $('#edit-selected-client').attr('href', editUrl).removeClass('hidden');
91
92 // Store client data for later use
93 clientsData[clientData.id] = {
94 name: clientData.business_client_name || (clientData.first_name + ' ' + clientData.last_name),
95 email: clientData.email,
96 phone: clientData.phone,
97 address: clientData.address,
98 website: clientData.website,
99 business_name: clientData.business_client_name,
100 first_name: clientData.first_name,
101 last_name: clientData.last_name
102 };
103 }
104
105 // Load client data via AJAX
106 function loadClientData(clientId) {
107 $.ajax({
108 url: easyInvoice.ajaxUrl,
109 type: 'POST',
110 data: {
111 action: 'easy_invoice_get_client',
112 client_id: clientId,
113 nonce: easyInvoice.nonce
114 },
115 success: function(response) {
116 if (response.success) {
117 var client = response.data;
118
119 // Set client ID
120 $("#client-id").val(clientId);
121
122 // Update display fields
123 $("#display-client-name").text(client.business_client_name || (client.first_name + ' ' + client.last_name) || 'N/A');
124 $("#display-client-email").text(client.email || 'N/A');
125 $("#display-client-phone").text(client.phone || 'N/A');
126 $("#display-client-company").text(client.business_client_name || 'N/A');
127 $("#display-client-address").text(client.address || 'N/A');
128 $("#display-client-website").text(client.website || 'N/A');
129
130 // Show client info display and hide no-client message
131 $("#client-info-display").removeClass("hidden");
132 $("#no-client-message").addClass("hidden");
133
134 // Update the new selected client display
135 var clientName = client.business_client_name || (client.first_name + ' ' + client.last_name) || 'N/A';
136 $("#selected-client-name").text(clientName);
137 $("#selected-client-email").text(client.email || 'N/A');
138 $("#selected-client-display").removeClass("hidden");
139
140 // Update the edit client link
141 var editUrl = 'admin.php?page=easy-invoice-client-edit&client_id=' + clientId;
142 $('#edit-selected-client').attr('href', editUrl).removeClass('hidden');
143
144 // Store client data for later use
145 clientsData[clientId] = {
146 name: client.business_client_name || (client.first_name + ' ' + client.last_name),
147 email: client.email,
148 phone: client.phone,
149 address: client.address,
150 website: client.website,
151 business_name: client.business_client_name,
152 first_name: client.first_name,
153 last_name: client.last_name
154 };
155 } else {
156 clearClientDisplay();
157 }
158 },
159 error: function(xhr, status, error) {
160 clearClientDisplay();
161 }
162 });
163 }
164
165 // Clear client display
166 function clearClientDisplay() {
167 $("#client-id").val("");
168 $("#display-client-name").text("-");
169 $("#display-client-email").text("-");
170 $("#display-client-phone").text("-");
171 $("#display-client-company").text("-");
172 $("#display-client-address").text("-");
173 $("#display-client-website").text("-");
174
175 $("#client-info-display").addClass("hidden");
176 $("#no-client-message").removeClass("hidden");
177 $("#selected-client-display").addClass("hidden");
178 $("#edit-selected-client").addClass("hidden");
179 }
180
181 // Clean up existing handlers to prevent duplicates
182 function cleanUpExistingHandlers() {
183 $("#add-new-client-btn").off("click");
184 $(document).off("click", "#add-new-client-btn");
185 $(".close-modal").off("click");
186 $(document).off("click", ".close-modal");
187 $(document).off("click", "#add-save-client-btn");
188 $(document).off("click", "#add-cancel-add-client");
189 }
190
191 // Setup client modal functionality
192 function setupClientModal() {
193 // Clean up existing handlers first
194 cleanUpExistingHandlers();
195
196 // Simple function to toggle modal visibility
197 function toggleClientModal(show) {
198 if (show) {
199 $("#add_client_modal").removeClass("hidden");
200 } else {
201 $("#add_client_modal").addClass("hidden");
202
203 // Reset form when closing
204 $("#add-client-form")[0].reset();
205 }
206 }
207
208 // Simple button click handler
209 $(document).on("click", "#add-new-client-btn", function(e) {
210 e.preventDefault();
211 e.stopPropagation();
212
213 // Simple modal show
214 $("#add_client_modal").removeClass("hidden");
215
216 return false;
217 });
218
219 // Use event delegation for close buttons
220 $(document).on("click", ".close-modal", function(e) {
221 e.preventDefault();
222 e.stopPropagation();
223 toggleClientModal(false);
224 return false;
225 });
226
227 // Handle cancel button
228 $(document).on("click", "#add-cancel-add-client", function(e) {
229 e.preventDefault();
230 e.stopPropagation();
231 toggleClientModal(false);
232 return false;
233 });
234
235 // Close when clicking on the background
236 $(document).on("click", "#add_client_modal", function(e) {
237 if (e.target === this) {
238 toggleClientModal(false);
239 }
240 });
241
242 // Handle client form submission via button click
243 $(document).on("click", "#add-save-client-btn", function(e) {
244 e.preventDefault();
245 e.stopPropagation(); // Prevent event bubbling to parent form
246
247 // Check if we can find the form fields directly (since form tag might not be rendering)
248 var $businessNameField = $("#add-client-business-name");
249 var $emailField = $("#add-client-email");
250 var $usernameField = $("#add-client-username");
251 var $passwordField = $("#add-client-password");
252
253 if ($businessNameField.length === 0 || $emailField.length === 0 || $usernameField.length === 0 || $passwordField.length === 0) {
254 if (typeof EasyInvoiceToast !== 'undefined') {
255 EasyInvoiceToast.error("Client form fields not found! Please refresh the page and try again.");
256 } else {
257 console.error("Client form fields not found! Please refresh the page and try again.");
258 }
259 return;
260 }
261
262 // Check if easyInvoice object exists
263 if (typeof easyInvoice === 'undefined') {
264 if (typeof EasyInvoiceToast !== 'undefined') {
265 EasyInvoiceToast.error("Error: easyInvoice object is not defined. Please refresh the page.");
266 } else {
267 console.error("Error: easyInvoice object is not defined. Please refresh the page.");
268 }
269 return;
270 }
271
272 // Check if required fields are filled
273 var businessName = $businessNameField.val();
274 var email = $emailField.val();
275 var username = $usernameField.val();
276 var password = $passwordField.val();
277
278 // Validate required fields
279 if (!businessName || !email || !username ) {
280 if (typeof EasyInvoiceToast !== 'undefined') {
281 EasyInvoiceToast.error("Please fill in all required fields (Business Name, Email, Username, Password)");
282 } else {
283 console.error("Please fill in all required fields (Business Name, Email, Username, Password)");
284 }
285 return;
286 }
287
288 // Get form data using new field names - ensure no conflicts with main form
289 var clientData = {
290 business_client_name: businessName,
291 email: email,
292 username: username,
293 password: password,
294 address: $("#add-client-address").val(),
295 phone: $("#add-client-phone").val(),
296 first_name: $("#add-client-first-name").val(),
297 last_name: $("#add-client-last-name").val(),
298 website: $("#add-client-website").val(),
299 action: "easy_invoice_add_client",
300 nonce: easyInvoice.nonce,
301 suppress_global_toast: true, // Prevent global toasts
302 is_client_form: true // Flag to identify this is client form data
303 };
304
305 // Show loading state
306 var submitBtn = $(this);
307 var originalText = submitBtn.text();
308 submitBtn.prop("disabled", true).html("<i class=\"fas fa-spinner fa-spin mr-2\"></i> Saving...");
309
310 // Send AJAX request
311 $.ajax({
312 url: easyInvoice.ajaxUrl,
313 type: "POST",
314 data: clientData,
315 timeout: 10000, // 10 second timeout
316 success: function(response) {
317 // Always reset the button and clear the form
318 submitBtn.prop("disabled", false).html(originalText);
319 $("#add-client-business-name, #add-client-email, #add-client-username, #add-client-password, #add-client-first-name, #add-client-last-name, #add-client-address, #add-client-phone, #add-client-website").val("");
320
321 if (response.success) {
322 // Close modal
323 $("#add_client_modal").addClass("hidden");
324
325 // If we're in an invoice or quote builder, select the new client
326 if (typeof window.selectClientFromOption === 'function') {
327 // Create client data object for selection
328 var newClientData = {
329 name: clientData.business_client_name || (clientData.first_name + ' ' + clientData.last_name),
330 email: clientData.email,
331 company: clientData.business_client_name,
332 phone: clientData.phone,
333 website: clientData.website,
334 address: clientData.address
335 };
336
337 // Select the new client in the main form
338 window.selectClientFromOption({
339 getAttribute: function(attr) {
340 if (attr === 'data-client-id') return response.data.client_id;
341 if (attr === 'data-client-name') return newClientData.name;
342 if (attr === 'data-client-email') return newClientData.email;
343 if (attr === 'data-client-company') return newClientData.company;
344 if (attr === 'data-client-phone') return newClientData.phone;
345 if (attr === 'data-client-website') return newClientData.website;
346 if (attr === 'data-client-address') return newClientData.address;
347 return '';
348 }
349 });
350 }
351
352 // Compose the two name columns separately — the table has
353 // Business Name and Client Name as DISTINCT columns, so we
354 // can't collapse them into one or the cells shift left.
355 var businessName = (clientData.business_client_name || '').trim() || '-';
356 var contactName = ((clientData.first_name || '') + ' ' + (clientData.last_name || '')).trim();
357 if (contactName === '') {
358 contactName = clientData.username || '';
359 }
360
361 // Use the role label the server returned with the create
362 // response. PHP renders the same row on the next refresh
363 // by reading $user->roles[0] (currently 'customer'); using
364 // the server-supplied label here keeps both states in sync
365 // so the badge doesn't flicker from "Client" -> "Customer"
366 // on first page reload.
367 var roleLabel = (response.data && response.data.role_label)
368 ? response.data.role_label
369 : 'Customer';
370
371 // Create new table row — MUST match the 8 columns in the
372 // <thead> exactly (id, business name, contact name, email,
373 // phone, username, role, actions). Skipping any of these
374 // makes every later cell shift one column to the left.
375 var newRow = `
376 <tr>
377 <td class="px-6 py-4 whitespace-nowrap">
378 <div class="text-sm text-gray-500 font-mono font-semibold">${response.data.client_id}</div>
379 </td>
380 <td class="px-6 py-4 whitespace-nowrap">
381 <div class="text-sm font-medium text-gray-900">${businessName}</div>
382 </td>
383 <td class="px-6 py-4 whitespace-nowrap">
384 <div class="text-sm font-medium text-gray-900">${contactName}</div>
385 </td>
386 <td class="px-6 py-4 whitespace-nowrap">
387 <div class="text-sm text-gray-500">${clientData.email || ''}</div>
388 </td>
389 <td class="px-6 py-4 whitespace-nowrap">
390 <div class="text-sm text-gray-500">${clientData.phone || ''}</div>
391 </td>
392 <td class="px-6 py-4 whitespace-nowrap">
393 <div class="text-sm text-gray-500">${clientData.username || ''}</div>
394 </td>
395 <td class="px-6 py-4 whitespace-nowrap">
396 <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
397 <i class="fas fa-user mr-1"></i>
398 ${roleLabel}
399 </span>
400 </td>
401 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
402 <div class="flex items-center space-x-2">
403 <a href="?page=easy-invoice-client-view&client_id=${response.data.client_id}"
404 class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
405 title="View Client">
406 <i class="fas fa-eye mr-1"></i>
407 View
408 </a>
409 <a href="?page=easy-invoice-client-edit&client_id=${response.data.client_id}"
410 class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
411 title="Edit Client">
412 <i class="fas fa-edit mr-1"></i>
413 Edit
414 </a>
415 <button type="button"
416 class="delete-client inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
417 data-id="${response.data.client_id}"
418 title="Delete Client">
419 <i class="fas fa-trash mr-1"></i>
420 Delete
421 </button>
422 </div>
423 </td>
424 </tr>
425 `;
426
427 // Remove "No clients found" placeholder row if it exists.
428 // The colspan must match what's emitted by the PHP empty
429 // state and the JS delete handler (currently 8).
430 $(".client-list-table tbody tr td[colspan='8']").closest("tr").remove();
431
432 // Add new row at the top of the table (since we order by ID DESC)
433 $(".client-list-table tbody").prepend(newRow);
434
435 // Update total clients count
436 var currentCount = parseInt($("h3:contains('Total Clients')").next().text()) || 0;
437 $("h3:contains('Total Clients')").next().text(currentCount + 1);
438
439 // Show server's success message. The request sends
440 // suppress_global_toast=true (so the global ajaxSuccess
441 // handler in easy-invoice-toast.js doesn't auto-render a
442 // duplicate toast); we surface the message manually here.
443 if (typeof EasyInvoiceToast !== 'undefined' && response.data.message) {
444 EasyInvoiceToast.success(response.data.message);
445 }
446 } else {
447 if (typeof EasyInvoiceToast !== 'undefined') {
448 EasyInvoiceToast.error((response.data && response.data.message) || "Error adding client");
449 }
450 }
451 },
452 error: function(xhr, status, error) {
453 submitBtn.prop("disabled", false).html(originalText);
454 if (typeof EasyInvoiceToast !== 'undefined') {
455 EasyInvoiceToast.error("Error connecting to server. Please try again.");
456 } else if (typeof showToast === 'function') {
457 showToast("Error connecting to server. Please try again.", "error");
458 } else {
459 console.error("Error connecting to server. Please try again.");
460 }
461 },
462 timeout: function() {
463 submitBtn.prop("disabled", false).html(originalText);
464 if (typeof EasyInvoiceToast !== 'undefined') {
465 EasyInvoiceToast.error("Request timed out. Please try again.");
466 } else if (typeof showToast === 'function') {
467 showToast("Request timed out. Please try again.", "error");
468 } else {
469 console.error("Request timed out. Please try again.");
470 }
471 }
472 });
473 });
474 }
475
476 // Setup password field functionality
477 function setupPasswordFields() {
478 // Show/hide password functionality for add client modal
479 $(document).on('click', '#show-password', function() {
480 const passwordInput = $('#client-password');
481 const icon = $(this).find('i');
482 if (passwordInput.attr('type') === 'password') {
483 passwordInput.attr('type', 'text');
484 icon.removeClass('fa-eye').addClass('fa-eye-slash');
485 } else {
486 passwordInput.attr('type', 'password');
487 icon.removeClass('fa-eye-slash').addClass('fa-eye');
488 }
489 });
490
491 // Show/hide password functionality for edit client page
492 $(document).on('click', '#edit-show-password', function() {
493 const passwordInput = $('#edit-client-password');
494 const icon = $(this).find('i');
495 if (passwordInput.attr('type') === 'password') {
496 passwordInput.attr('type', 'text');
497 icon.removeClass('fa-eye').addClass('fa-eye-slash');
498 } else {
499 passwordInput.attr('type', 'password');
500 icon.removeClass('fa-eye-slash').addClass('fa-eye');
501 }
502 });
503
504 // Generate password functionality for add client modal
505 $(document).on('click', '#add-generate-password, #edit-generate-password', function() {
506 const button = $(this);
507 const isEditMode = button.attr('id') === 'edit-generate-password';
508 const passwordInput = isEditMode ? $('#edit-client-password') : $('#add-client-password');
509 const showPasswordBtn = isEditMode ? $('#edit-show-password') : $('#add-show-password');
510
511 button.prop('disabled', true);
512
513 $.ajax({
514 url: easyInvoice.ajaxUrl,
515 type: 'POST',
516 data: {
517 action: 'easy_invoice_generate_password',
518 nonce: easyInvoice.nonce,
519 suppress_global_toast: true // Prevent global success toast
520 },
521 success: function(response) {
522 if (response.success) {
523 passwordInput.val(response.data.password);
524 passwordInput.attr('type', 'text');
525 showPasswordBtn.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
526 } else {
527
528 }
529 },
530 error: function() {
531 if (typeof EasyInvoiceToast !== 'undefined') {
532 EasyInvoiceToast.error('Error connecting to server');
533 } else if (typeof showToast === 'function') {
534 showToast('Error connecting to server', 'error');
535 } else {
536 console.error('Error connecting to server');
537 }
538 },
539 complete: function() {
540 button.prop('disabled', false);
541 }
542 });
543 });
544
545
546 }
547
548 // Setup client edit functionality
549 function setupClientEdit() {
550 // Disable HTML5 validation for password field in edit mode
551 if ($("#edit-client-form").length) {
552 $("#edit-client-password").removeAttr("required");
553 }
554
555 // Handle client edit form submission
556 $(document).on("submit", "#edit-client-form", function(e) {
557 e.preventDefault();
558
559 let isSubmitting = false;
560 if (isSubmitting) return;
561
562 isSubmitting = true;
563
564 const clientData = {
565 client_id: $("#edit-client-id").val(),
566 business_client_name: $("#edit-client-business-name").val(),
567 email: $("#edit-client-email").val(),
568 username: $("#edit-client-username").val(),
569 password: $("#edit-client-password").val(),
570 address: $("#edit-client-address").val(),
571 phone: $("#edit-client-phone").val(),
572 first_name: $("#edit-client-first-name").val(),
573 last_name: $("#edit-client-last-name").val(),
574 website: $("#edit-client-website").val(),
575 action: "easy_invoice_update_client",
576 nonce: easyInvoice.nonce
577 };
578
579 const submitBtn = $("#edit-save-client-btn");
580 const originalText = submitBtn.text();
581 submitBtn.prop("disabled", true).html("<i class=\"fas fa-spinner fa-spin mr-2\"></i> Saving...");
582
583 $.ajax({
584 url: easyInvoice.ajaxUrl,
585 type: "POST",
586 data: clientData,
587 success: function(response) {
588 // Re-enable the button regardless of success/failure
589 submitBtn.prop("disabled", false).text(originalText);
590 isSubmitting = false;
591 },
592 error: function() {
593 if (typeof EasyInvoiceToast !== 'undefined') {
594 EasyInvoiceToast.error("Error connecting to server");
595 } else if (typeof showToast === 'function') {
596 showToast("Error connecting to server", "error");
597 } else {
598 console.error("Error connecting to server");
599 }
600 submitBtn.prop("disabled", false).text(originalText);
601 isSubmitting = false;
602 }
603 });
604 });
605 }
606
607 // Initialize all client functionality
608 function initializeClientManager() {
609 // Prevent multiple initializations
610 if (window.clientManagerInitialized) {
611 return;
612 }
613
614 setupClientSelection();
615 setupClientModal();
616 setupPasswordFields();
617 setupClientEdit();
618
619 // Mark as initialized
620 window.clientManagerInitialized = true;
621 }
622
623 // Initialize the client manager
624 initializeClientManager();
625
626 // Expose client manager functions globally
627 window.clientManager = {
628 initializeClientData: initializeClientData,
629 refreshClientList: function(newClientData) {
630 clientsData = newClientData || clientsData;
631 window.easyInvoiceClients = clientsData;
632 },
633 setupModal: setupClientModal
634 };
635 });
636 })(jQuery);