PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.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 / client-manager.js

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

637 lines 32.9 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 esc = function(v){ return $('<div>').text(v == null ? '' : String(v)).html(); };
376 var newRow = `
377 <tr>
378 <td class="px-6 py-4 whitespace-nowrap">
379 <div class="text-sm text-gray-500 font-mono font-semibold">${esc(response.data.client_id)}</div>
380 </td>
381 <td class="px-6 py-4 whitespace-nowrap">
382 <div class="text-sm font-medium text-gray-900">${esc(businessName)}</div>
383 </td>
384 <td class="px-6 py-4 whitespace-nowrap">
385 <div class="text-sm font-medium text-gray-900">${esc(contactName)}</div>
386 </td>
387 <td class="px-6 py-4 whitespace-nowrap">
388 <div class="text-sm text-gray-500">${esc(clientData.email || '')}</div>
389 </td>
390 <td class="px-6 py-4 whitespace-nowrap">
391 <div class="text-sm text-gray-500">${esc(clientData.phone || '')}</div>
392 </td>
393 <td class="px-6 py-4 whitespace-nowrap">
394 <div class="text-sm text-gray-500">${esc(clientData.username || '')}</div>
395 </td>
396 <td class="px-6 py-4 whitespace-nowrap">
397 <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
398 <i class="fas fa-user mr-1"></i>
399 ${esc(roleLabel)}
400 </span>
401 </td>
402 <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
403 <div class="flex items-center space-x-2">
404 <a href="?page=easy-invoice-client-view&client_id=${esc(response.data.client_id)}"
405 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"
406 title="View Client">
407 <i class="fas fa-eye mr-1"></i>
408 View
409 </a>
410 <a href="?page=easy-invoice-client-edit&client_id=${esc(response.data.client_id)}"
411 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"
412 title="Edit Client">
413 <i class="fas fa-edit mr-1"></i>
414 Edit
415 </a>
416 <button type="button"
417 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"
418 data-id="${esc(response.data.client_id)}"
419 title="Delete Client">
420 <i class="fas fa-trash mr-1"></i>
421 Delete
422 </button>
423 </div>
424 </td>
425 </tr>
426 `;
427
428 // Remove "No clients found" placeholder row if it exists.
429 // The colspan must match what's emitted by the PHP empty
430 // state and the JS delete handler (currently 8).
431 $(".client-list-table tbody tr td[colspan='8']").closest("tr").remove();
432
433 // Add new row at the top of the table (since we order by ID DESC)
434 $(".client-list-table tbody").prepend(newRow);
435
436 // Update total clients count
437 var currentCount = parseInt($("h3:contains('Total Clients')").next().text()) || 0;
438 $("h3:contains('Total Clients')").next().text(currentCount + 1);
439
440 // Show server's success message. The request sends
441 // suppress_global_toast=true (so the global ajaxSuccess
442 // handler in easy-invoice-toast.js doesn't auto-render a
443 // duplicate toast); we surface the message manually here.
444 if (typeof EasyInvoiceToast !== 'undefined' && response.data.message) {
445 EasyInvoiceToast.success(response.data.message);
446 }
447 } else {
448 if (typeof EasyInvoiceToast !== 'undefined') {
449 EasyInvoiceToast.error((response.data && response.data.message) || "Error adding client");
450 }
451 }
452 },
453 error: function(xhr, status, error) {
454 submitBtn.prop("disabled", false).html(originalText);
455 if (typeof EasyInvoiceToast !== 'undefined') {
456 EasyInvoiceToast.error("Error connecting to server. Please try again.");
457 } else if (typeof showToast === 'function') {
458 showToast("Error connecting to server. Please try again.", "error");
459 } else {
460 console.error("Error connecting to server. Please try again.");
461 }
462 },
463 timeout: function() {
464 submitBtn.prop("disabled", false).html(originalText);
465 if (typeof EasyInvoiceToast !== 'undefined') {
466 EasyInvoiceToast.error("Request timed out. Please try again.");
467 } else if (typeof showToast === 'function') {
468 showToast("Request timed out. Please try again.", "error");
469 } else {
470 console.error("Request timed out. Please try again.");
471 }
472 }
473 });
474 });
475 }
476
477 // Setup password field functionality
478 function setupPasswordFields() {
479 // Show/hide password functionality for add client modal
480 $(document).on('click', '#show-password', function() {
481 const passwordInput = $('#client-password');
482 const icon = $(this).find('i');
483 if (passwordInput.attr('type') === 'password') {
484 passwordInput.attr('type', 'text');
485 icon.removeClass('fa-eye').addClass('fa-eye-slash');
486 } else {
487 passwordInput.attr('type', 'password');
488 icon.removeClass('fa-eye-slash').addClass('fa-eye');
489 }
490 });
491
492 // Show/hide password functionality for edit client page
493 $(document).on('click', '#edit-show-password', function() {
494 const passwordInput = $('#edit-client-password');
495 const icon = $(this).find('i');
496 if (passwordInput.attr('type') === 'password') {
497 passwordInput.attr('type', 'text');
498 icon.removeClass('fa-eye').addClass('fa-eye-slash');
499 } else {
500 passwordInput.attr('type', 'password');
501 icon.removeClass('fa-eye-slash').addClass('fa-eye');
502 }
503 });
504
505 // Generate password functionality for add client modal
506 $(document).on('click', '#add-generate-password, #edit-generate-password', function() {
507 const button = $(this);
508 const isEditMode = button.attr('id') === 'edit-generate-password';
509 const passwordInput = isEditMode ? $('#edit-client-password') : $('#add-client-password');
510 const showPasswordBtn = isEditMode ? $('#edit-show-password') : $('#add-show-password');
511
512 button.prop('disabled', true);
513
514 $.ajax({
515 url: easyInvoice.ajaxUrl,
516 type: 'POST',
517 data: {
518 action: 'easy_invoice_generate_password',
519 nonce: easyInvoice.nonce,
520 suppress_global_toast: true // Prevent global success toast
521 },
522 success: function(response) {
523 if (response.success) {
524 passwordInput.val(response.data.password);
525 passwordInput.attr('type', 'text');
526 showPasswordBtn.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
527 } else {
528
529 }
530 },
531 error: function() {
532 if (typeof EasyInvoiceToast !== 'undefined') {
533 EasyInvoiceToast.error('Error connecting to server');
534 } else if (typeof showToast === 'function') {
535 showToast('Error connecting to server', 'error');
536 } else {
537 console.error('Error connecting to server');
538 }
539 },
540 complete: function() {
541 button.prop('disabled', false);
542 }
543 });
544 });
545
546
547 }
548
549 // Setup client edit functionality
550 function setupClientEdit() {
551 // Disable HTML5 validation for password field in edit mode
552 if ($("#edit-client-form").length) {
553 $("#edit-client-password").removeAttr("required");
554 }
555
556 // Handle client edit form submission
557 $(document).on("submit", "#edit-client-form", function(e) {
558 e.preventDefault();
559
560 let isSubmitting = false;
561 if (isSubmitting) return;
562
563 isSubmitting = true;
564
565 const clientData = {
566 client_id: $("#edit-client-id").val(),
567 business_client_name: $("#edit-client-business-name").val(),
568 email: $("#edit-client-email").val(),
569 username: $("#edit-client-username").val(),
570 password: $("#edit-client-password").val(),
571 address: $("#edit-client-address").val(),
572 phone: $("#edit-client-phone").val(),
573 first_name: $("#edit-client-first-name").val(),
574 last_name: $("#edit-client-last-name").val(),
575 website: $("#edit-client-website").val(),
576 action: "easy_invoice_update_client",
577 nonce: easyInvoice.nonce
578 };
579
580 const submitBtn = $("#edit-save-client-btn");
581 const originalText = submitBtn.text();
582 submitBtn.prop("disabled", true).html("<i class=\"fas fa-spinner fa-spin mr-2\"></i> Saving...");
583
584 $.ajax({
585 url: easyInvoice.ajaxUrl,
586 type: "POST",
587 data: clientData,
588 success: function(response) {
589 // Re-enable the button regardless of success/failure
590 submitBtn.prop("disabled", false).text(originalText);
591 isSubmitting = false;
592 },
593 error: function() {
594 if (typeof EasyInvoiceToast !== 'undefined') {
595 EasyInvoiceToast.error("Error connecting to server");
596 } else if (typeof showToast === 'function') {
597 showToast("Error connecting to server", "error");
598 } else {
599 console.error("Error connecting to server");
600 }
601 submitBtn.prop("disabled", false).text(originalText);
602 isSubmitting = false;
603 }
604 });
605 });
606 }
607
608 // Initialize all client functionality
609 function initializeClientManager() {
610 // Prevent multiple initializations
611 if (window.clientManagerInitialized) {
612 return;
613 }
614
615 setupClientSelection();
616 setupClientModal();
617 setupPasswordFields();
618 setupClientEdit();
619
620 // Mark as initialized
621 window.clientManagerInitialized = true;
622 }
623
624 // Initialize the client manager
625 initializeClientManager();
626
627 // Expose client manager functions globally
628 window.clientManager = {
629 initializeClientData: initializeClientData,
630 refreshClientList: function(newClientData) {
631 clientsData = newClientData || clientsData;
632 window.easyInvoiceClients = clientsData;
633 },
634 setupModal: setupClientModal
635 };
636 });
637 })(jQuery);