PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
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
← All changes | templates/quotes/form.php +112 -95 2.3.42.4.1 View file →
@@ -5,8 +5,11 @@
5 5 * @package EasyInvoice
6 6 * @since 1.0.0
7 7 */
8 8
9 +if ( ! defined( 'ABSPATH' ) ) {
10 + exit;
11 +}
9 12 // Only enqueue scripts if not loaded via AJAX
10 13 if (!wp_doing_ajax()) {
11 14 // Enqueue CSS and JS files
12 15 wp_enqueue_style('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/css/invoice-form.css', array(), EASY_INVOICE_VERSION);
@@ -238,11 +241,11 @@
238 241 <div class="tab-content <?php echo $tab['active'] ? 'active' : 'hidden'; ?> px-6 py-5" id="<?php echo esc_attr($tab['id']); ?>">
239 242 <?php if ($tab['id'] === 'quote-tab'): ?>
240 243 <div class="mb-8 pb-6 border-b border-gray-200">
241 244 <div class="flex justify-between items-center mb-4">
242 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Quote Template', 'easy-invoice'); ?></h3>
245 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Quote Template', 'easy-invoice'); ?></h3>
243 246 <button type="button" id="browse-all-templates" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
244 - <span class="mr-1"><?php _e('Browse Quote Templates', 'easy-invoice'); ?></span>
247 + <span class="mr-1"><?php esc_html_e('Browse Quote Templates', 'easy-invoice'); ?></span>
245 248 <i class="fas fa-chevron-right"></i>
246 249 </button>
247 250 </div>
248 251
@@ -312,10 +315,10 @@
312 315 if ($is_free_version && $is_premium) {
313 316 $premium_classes = 'premium-template-blurred';
314 317 }
315 318 ?>
316 - <div class="template-card <?php echo $selected_class . ' ' . $premium_classes; ?>" data-template-id="<?php echo esc_attr($template_id); ?>" data-is-premium="<?php echo $is_premium ? 'true' : 'false'; ?>">
317 - <input type="radio" id="template-<?php echo esc_attr($template_id); ?>" name="quote_template" value="<?php echo esc_attr($template_id); ?>" class="template-radio" <?php echo $checked; ?> <?php echo ($is_free_version && $is_premium) ? 'disabled' : ''; ?>>
319 + <div class="template-card <?php echo esc_attr($selected_class . ' ' . $premium_classes); ?>" data-template-id="<?php echo esc_attr($template_id); ?>" data-is-premium="<?php echo esc_attr($is_premium ? 'true' : 'false'); ?>">
320 + <input type="radio" id="template-<?php /* translators: %s: template name. */ echo esc_attr($template_id); ?>" name="quote_template" value="<?php echo esc_attr($template_id); ?>" aria-label="<?php echo esc_attr(sprintf(__('Use the %s quote template', 'easy-invoice'), $template['name'])); ?>" class="template-radio" <?php echo esc_html($checked); ?> <?php echo esc_attr(($is_free_version && $is_premium) ? 'disabled' : ''); ?>>
318 321 <div class="p-4 border rounded-lg">
319 322 <div class="flex items-center mb-2">
320 323 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
321 324 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -341,9 +344,9 @@
341 344 // Skip fields that have a section (like notes) - they're rendered separately
342 345 // Also skip the template field as it's rendered manually in the template selection section
343 346 if (!isset($field['section']) && $field['name'] !== 'quote_template') {
344 347
345 - echo $quote_form_manager->renderField($field, $quote);
348 + echo $quote_form_manager->renderField($field, $quote); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
346 349 }
347 350 endforeach;
348 351 ?>
349 352 </div>
@@ -355,9 +358,9 @@
355 358 <i class="fas fa-info-circle text-blue-400"></i>
356 359 </div>
357 360 <div class="ml-3">
358 361 <p class="text-sm text-blue-700">
359 - <?php _e('Quote number will be automatically generated when you save the quote.', 'easy-invoice'); ?>
362 + <?php esc_html_e('Quote number will be automatically generated when you save the quote.', 'easy-invoice'); ?>
360 363 </p>
361 364 </div>
362 365 </div>
363 366 </div>
@@ -363,15 +366,15 @@
363 366 </div>
364 367 <?php endif; ?>
365 368
366 369 <div class="mt-8 border-t border-gray-200 pt-6">
367 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Notes & Terms', 'easy-invoice'); ?></h3>
370 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Notes & Terms', 'easy-invoice'); ?></h3>
368 371 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:col-span-6">
369 372 <?php
370 373 // Only render notes and terms fields, not all quote tab fields
371 374 $notes_fields = $quote_form_manager->getFieldsBySection('quote-tab', 'notes');
372 375 foreach ($notes_fields as $field):
373 - echo $quote_form_manager->renderField($field, $quote);
376 + echo $quote_form_manager->renderField($field, $quote); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
374 377 endforeach;
375 378 ?>
376 379 </div>
377 380 </div>
@@ -377,12 +380,12 @@
377 380 </div>
378 381
379 382 <?php elseif ($tab['id'] === 'items-tab'): ?>
380 383 <div class="flex justify-between items-center mb-6">
381 - <h2 class="text-lg font-medium text-gray-900"><?php _e('Quote Items', 'easy-invoice'); ?></h2>
384 + <h2 class="text-lg font-medium text-gray-900"><?php esc_html_e('Quote Items', 'easy-invoice'); ?></h2>
382 385 <button type="button" id="collapse-all-items" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
383 386 <i class="fas fa-chevron-down mr-1"></i>
384 - <span><?php _e('Collapse All', 'easy-invoice'); ?></span>
387 + <span><?php esc_html_e('Collapse All', 'easy-invoice'); ?></span>
385 388 </button>
386 389 </div>
387 390
388 391 <div class="space-y-4 quote-items-container">
@@ -391,9 +394,9 @@
391 394 if ($quote && $quote->getItems()) :
392 395 $index = 0;
393 396 foreach ($quote->getItems() as $item) :
394 397 // Generate a unique ID for the item
395 - $item_id = 'item_' . time() . '_' . $index . '_' . rand(1000, 9999);
398 + $item_id = 'item_' . time() . '_' . $index . '_' . wp_rand(1000, 9999);
396 399
397 400 // Get display title for header using dynamic field access
398 401 $title = $item->getName();
399 402
@@ -404,12 +407,13 @@
404 407 $title = $item_data['title'];
405 408 }
406 409 }
407 410
411 + /* translators: %d: title. */
408 412 $display_title = !empty($title) ? $title : sprintf(__('Item #%d', 'easy-invoice'), $index + 1);
409 413 $short_title = strlen($display_title) > 30 ? substr($display_title, 0, 30) . '...' : $display_title;
410 414 ?>
411 - <div class="quote-item p-6 rounded-xl shadow-sm border border-blue-100 hover:shadow-md transition-all duration-200 mb-6" style="background-color: #f8fbff;" id="<?php echo $item_id; ?>">
415 + <div class="quote-item p-6 rounded-xl shadow-sm border border-blue-100 hover:shadow-md transition-all duration-200 mb-6" style="background-color: #f8fbff;" id="<?php echo (int) $item_id; ?>">
412 416 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
413 417 <div class="flex items-center">
414 418 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
415 419 <i class="fas fa-chevron-down text-sm"></i>
@@ -415,9 +419,9 @@
415 419 <i class="fas fa-chevron-down text-sm"></i>
416 420 </button>
417 421 <h3 class="text-base font-normal text-gray-800 flex-1">
418 422 <?php
419 - echo !empty($title) ? esc_html($title) : __('New Item', 'easy-invoice');
423 + echo !empty($title) ? esc_html($title) : esc_html__('New Item', 'easy-invoice');
420 424 ?>
421 425 </h3>
422 426 </div>
423 427 <div class="flex items-center gap-3">
@@ -424,9 +428,9 @@
424 428 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
425 429 <span class="quantity-summary"><?php echo esc_html($item->getQuantity()); ?></span> × $<span class="price-summary"><?php echo esc_html(number_format(floatval($item->getPrice()), 2)); ?></span>
426 430 = $<span class="total-summary font-medium"><?php echo esc_html(number_format($item->getAmount(), 2)); ?></span>
427 431 </div>
428 - <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Fill this item with sample data', 'easy-invoice'); ?>">
432 + <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Fill this item with sample data', 'easy-invoice')); ?>">
429 433 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
430 434 <path d="M2 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2zm0 1h16v14H2V3z"/>
431 435 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
432 436 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -433,14 +437,14 @@
433 437 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
434 438 </svg>
435 439 </button>
436 440 <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
437 - <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php _e('Select from Item Library', 'easy-invoice'); ?>">
441 + <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Select from Item Library', 'easy-invoice')); ?>">
438 442 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
439 443 <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
440 444 </svg>
441 445 </button>
442 - <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Save to Item Library', 'easy-invoice'); ?>" data-item-index="<?php echo $index; ?>" data-nonce="<?php echo wp_create_nonce('easy_invoice_pro_item_library'); ?>">
446 + <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Save to Item Library', 'easy-invoice')); ?>" data-item-index="<?php echo esc_attr($index); ?>" data-nonce="<?php echo esc_attr(wp_create_nonce('easy_invoice_pro_item_library')); ?>">
443 447 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
444 448 <path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
445 449 </svg>
446 450 </button>
@@ -445,14 +449,15 @@
445 449 </svg>
446 450 </button>
447 451 <?php endif; ?>
448 452 <button type="button" class="remove-item text-gray-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-lg transition-all duration-200 text-sm font-medium">
449 - <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
453 + <i class="fas fa-trash-alt mr-1"></i> <?php esc_html_e('Remove', 'easy-invoice'); ?>
450 454 </button>
451 455 </div>
452 456 </div>
453 457 <div class="item-content">
454 - <?php echo $quote_form_manager->generateExistingItemHtml($item, $index); ?>
458 + <?php echo $quote_form_manager->generateExistingItemHtml($item, $index); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ItemFieldRenderer escapes every value it emits.
459 +?>
455 460 </div>
456 461 </div>
457 462 <?php
458 463 $index++;
@@ -488,9 +493,9 @@
488 493 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
489 494 <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
490 495 = $<span class="total-summary font-medium">0.00</span>
491 496 </div>
492 - <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Fill this item with sample data', 'easy-invoice'); ?>">
497 + <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Fill this item with sample data', 'easy-invoice')); ?>">
493 498 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
494 499 <path d="M2 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2zm0 1h16v14H2V3z"/>
495 500 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
496 501 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -497,14 +502,14 @@
497 502 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
498 503 </svg>
499 504 </button>
500 505 <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
501 - <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php _e('Select from Item Library', 'easy-invoice'); ?>">
506 + <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Select from Item Library', 'easy-invoice')); ?>">
502 507 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
503 508 <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
504 509 </svg>
505 510 </button>
506 - <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Save to Item Library', 'easy-invoice'); ?>" data-item-index="0" data-nonce="<?php echo wp_create_nonce('easy_invoice_pro_item_library'); ?>">
511 + <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Save to Item Library', 'easy-invoice')); ?>" data-item-index="0" data-nonce="<?php echo esc_attr(wp_create_nonce('easy_invoice_pro_item_library')); ?>">
507 512 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
508 513 <path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
509 514 </svg>
510 515 </button>
@@ -509,14 +514,14 @@
509 514 </svg>
510 515 </button>
511 516 <?php endif; ?>
512 517 <button type="button" class="remove-item text-gray-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-lg transition-all duration-200 text-sm font-medium">
513 - <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
518 + <i class="fas fa-trash-alt mr-1"></i> <?php esc_html_e('Remove', 'easy-invoice'); ?>
514 519 </button>
515 520 </div>
516 521 </div>
517 522 <div class="item-content">
518 - <?php echo $quote_form_manager->generateItemTemplateHtml(); ?>
523 + <?php echo $quote_form_manager->generateItemTemplateHtml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- form markup built by ItemFieldRenderer, which escapes every value. ?>
519 524 </div>
520 525 </div>
521 526 <?php endif; ?>
522 527 </div>
@@ -527,10 +532,10 @@
527 532 <div class="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center mr-3 group-hover:bg-indigo-200 transition-colors">
528 533 <i class="fas fa-plus text-indigo-600 text-lg"></i>
529 534 </div>
530 535 <div class="text-left">
531 - <span class="font-semibold text-base"><?php _e('Add Another Item', 'easy-invoice'); ?></span>
532 - <p class="text-sm text-indigo-600 mt-0.5"><?php _e('Click to add a new line item', 'easy-invoice'); ?></p>
536 + <span class="font-semibold text-base"><?php esc_html_e('Add Another Item', 'easy-invoice'); ?></span>
537 + <p class="text-sm text-indigo-600 mt-0.5"><?php esc_html_e('Click to add a new line item', 'easy-invoice'); ?></p>
533 538 </div>
534 539 </div>
535 540 </button>
536 541 </div>
@@ -536,9 +541,9 @@
536 541 </div>
537 542
538 543 <?php elseif ($tab['id'] === 'client-tab'): ?>
539 544 <div class="mb-6">
540 - <label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('Select Existing Client', 'easy-invoice'); ?></label>
545 + <label class="block text-sm font-medium text-gray-700 mb-3"><?php esc_html_e('Select Existing Client', 'easy-invoice'); ?></label>
541 546
542 547 <div class="relative">
543 548 <div class="flex items-center space-x-3">
544 549 <div class="relative flex-grow">
@@ -548,15 +553,15 @@
548 553 <input
549 554 type="text"
550 555 id="client-search-input"
551 556 class="block w-full rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 pl-10"
552 - placeholder="<?php _e('Search clients by name, email, or company...', 'easy-invoice'); ?>"
557 + placeholder="<?php echo esc_js(__('Search clients by name, email, or company...', 'easy-invoice')); ?>"
553 558 autocomplete="off"
554 559 >
555 560 </div>
556 561 <button type="button" id="add-new-client-btn" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
557 562 <i class="fas fa-plus -ml-1 mr-2"></i>
558 - <?php _e('New Client', 'easy-invoice'); ?>
563 + <?php esc_html_e('New Client', 'easy-invoice'); ?>
559 564 </button>
560 565 </div>
561 566
562 567 <div id="client-search-results" class="hidden absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-xl max-h-80 overflow-y-auto">
@@ -561,13 +566,13 @@
561 566
562 567 <div id="client-search-results" class="hidden absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-xl max-h-80 overflow-y-auto">
563 568 <div id="client-search-loading" class="hidden p-6 text-center text-gray-500">
564 569 <i class="fas fa-spinner fa-spin mr-2"></i>
565 - <?php _e('Searching...', 'easy-invoice'); ?>
570 + <?php esc_html_e('Searching...', 'easy-invoice'); ?>
566 571 </div>
567 572 <div id="client-search-empty" class="hidden p-6 text-center text-gray-500">
568 573 <i class="fas fa-search mr-2"></i>
569 - <?php _e('No clients found', 'easy-invoice'); ?>
574 + <?php esc_html_e('No clients found', 'easy-invoice'); ?>
570 575 </div>
571 576 <div id="client-search-list" class="py-2">
572 577 </div>
573 578 </div>
@@ -618,10 +623,15 @@
618 623 </div>
619 624 </div>
620 625
621 626 <select id="select-client" class="hidden">
622 - <option value=""><?php _e('-- Select a client --', 'easy-invoice'); ?></option>
627 + <option value=""><?php esc_html_e('-- Select a client --', 'easy-invoice'); ?></option>
623 628 <?php
629 + // Only the quote's own client is mirrored here; the picker searches over AJAX.
630 + if (empty($clients) && $quote && method_exists($quote, 'getClientId') && (int) $quote->getClientId() > 0) {
631 + $ei_selected_client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find((int) $quote->getClientId());
632 + if ($ei_selected_client) { $clients = [ $ei_selected_client ]; }
633 + }
624 634 // Output client options
625 635 if (!empty($clients)) {
626 636 foreach ($clients as $client) {
627 637 // Get the WordPress user data directly
@@ -645,9 +655,9 @@
645 655 $selected = $quote && $quote->getClientId() == $client->getId() ? 'selected' : '';
646 656 printf(
647 657 '<option value="%s" %s data-name="%s" data-email="%s">%s (%s)</option>',
648 658 esc_attr($client->getId()),
649 - $selected,
659 + $selected, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output of selected( ..., false ).
650 660 esc_attr($client_name),
651 661 esc_attr($client_email),
652 662 esc_html($client_name),
653 663 esc_html($client_email)
@@ -665,16 +675,16 @@
665 675 <div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
666 676 <div class="flex items-center justify-between pb-4 border-b border-gray-200 mb-4">
667 677 <h4 class="text-lg font-semibold text-gray-800 flex items-center">
668 678 <i class="fas fa-user-check text-indigo-500 mr-3"></i>
669 - <span><?php _e('Client Information', 'easy-invoice'); ?></span>
679 + <span><?php esc_html_e('Client Information', 'easy-invoice'); ?></span>
670 680 </h4>
671 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-client-edit&client_id=' . ($quote && $quote->getClientId() ? $quote->getClientId() : '')); ?>"
681 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-client-edit&client_id=' . ($quote && $quote->getClientId() ? $quote->getClientId() : ''))); ?>"
672 682 id="edit-selected-client"
673 - class="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
683 + class="text-sm text-indigo-600 hover:text-indigo-800 font-medium<?php echo ($quote && $quote->getClientId()) ? '' : ' hidden'; ?>"
674 684 target="_blank">
675 685 <i class="fas fa-pen mr-1"></i>
676 - <?php _e('Edit', 'easy-invoice'); ?>
686 + <?php esc_html_e('Edit', 'easy-invoice'); ?>
677 687 </a>
678 688 </div>
679 689
680 690 <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 pt-2">
@@ -680,9 +690,9 @@
680 690 <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 pt-2">
681 691 <div class="flex items-start group">
682 692 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-user-tie text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
683 693 <div class="ml-3">
684 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Name', 'easy-invoice'); ?></label>
694 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Name', 'easy-invoice'); ?></label>
685 695 <p id="display-client-name" class="text-sm font-semibold text-gray-800 mt-1">
686 696 <?php
687 697 if ($quote && $quote->getClientId()) {
688 698 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -701,9 +711,9 @@
701 711
702 712 <div class="flex items-start group">
703 713 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-building text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
704 714 <div class="ml-3">
705 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Company', 'easy-invoice'); ?></label>
715 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Company', 'easy-invoice'); ?></label>
706 716 <p id="display-client-company" class="text-sm font-semibold text-gray-800 mt-1">
707 717 <?php
708 718 if ($quote && $quote->getClientId()) {
709 719 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -722,9 +732,9 @@
722 732
723 733 <div class="flex items-start group">
724 734 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-envelope text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
725 735 <div class="ml-3">
726 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Email', 'easy-invoice'); ?></label>
736 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Email', 'easy-invoice'); ?></label>
727 737 <p id="display-client-email" class="text-sm text-gray-800 mt-1 break-all">
728 738 <?php
729 739 if ($quote && $quote->getClientId()) {
730 740 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -743,9 +753,9 @@
743 753
744 754 <div class="flex items-start group">
745 755 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-phone-alt text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
746 756 <div class="ml-3">
747 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Phone', 'easy-invoice'); ?></label>
757 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Phone', 'easy-invoice'); ?></label>
748 758 <p id="display-client-phone" class="text-sm text-gray-800 mt-1">
749 759 <?php
750 760 if ($quote && $quote->getClientId()) {
751 761 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -764,9 +774,9 @@
764 774
765 775 <div class="flex items-start group md:col-span-2">
766 776 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-globe-americas text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
767 777 <div class="ml-3">
768 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Website', 'easy-invoice'); ?></label>
778 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Website', 'easy-invoice'); ?></label>
769 779 <p id="display-client-website" class="text-sm text-gray-800 mt-1">
770 780 <?php
771 781 if ($quote && $quote->getClientId()) {
772 782 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -785,9 +795,9 @@
785 795
786 796 <div class="flex items-start group md:col-span-2">
787 797 <div class="flex-shrink-0 w-8 text-center pt-1"><i class="fas fa-map-marker-alt text-gray-400 group-hover:text-indigo-500 transition-colors"></i></div>
788 798 <div class="ml-3">
789 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Address', 'easy-invoice'); ?></label>
799 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Address', 'easy-invoice'); ?></label>
790 800 <p id="display-client-address" class="text-sm text-gray-800 mt-1 whitespace-pre-wrap">
791 801 <?php
792 802 if ($quote && $quote->getClientId()) {
793 803 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -807,15 +817,15 @@
807 817 </div>
808 818 </div>
809 819
810 820 <div id="no-client-message" class="mt-4 text-center py-6 border-2 border-dashed border-gray-300 rounded-lg <?php echo $quote && $quote->getClientId() ? 'hidden' : ''; ?>">
811 - <p class="text-gray-600"><?php _e('Select a client from the dropdown above to view their information.', 'easy-invoice'); ?></p>
821 + <p class="text-gray-600"><?php esc_html_e('Select a client from the dropdown above to view their information.', 'easy-invoice'); ?></p>
812 822 </div>
813 823 </div>
814 824
815 825 <?php elseif ($tab['id'] === 'discounts-taxes-tab'): ?>
816 826 <div>
817 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Discount & Tax Settings', 'easy-invoice'); ?></h3>
827 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Discount & Tax Settings', 'easy-invoice'); ?></h3>
818 828
819 829 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
820 830 <?php
821 831 // Get all discount and tax fields
@@ -829,9 +839,9 @@
829 839 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
830 840 });
831 841
832 842 foreach ($all_fields as $field):
833 - echo $quote_form_manager->renderField($field, $quote);
843 + echo $quote_form_manager->renderField($field, $quote); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
834 844 endforeach;
835 845 ?>
836 846 </div>
837 847 </div>
@@ -837,25 +847,25 @@
837 847 </div>
838 848
839 849 <?php elseif ($tab['id'] === 'payments-tab'): ?>
840 850 <div>
841 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Payment Information', 'easy-invoice'); ?></h3>
851 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Payment Information', 'easy-invoice'); ?></h3>
842 852 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
843 853 <?php
844 854 $payment_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'payment');
845 855 foreach ($payment_fields as $field):
846 - echo $quote_form_manager->renderField($field, $quote);
856 + echo $quote_form_manager->renderField($field, $quote); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
847 857 endforeach;
848 858 ?>
849 859 </div>
850 860
851 861 <div class="mt-8 border-t border-gray-200 pt-6">
852 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Currency', 'easy-invoice'); ?></h3>
862 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Currency', 'easy-invoice'); ?></h3>
853 863 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
854 864 <?php
855 865 $currency_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'currency');
856 866 foreach ($currency_fields as $field):
857 - echo $quote_form_manager->renderField($field, $quote);
867 + echo $quote_form_manager->renderField($field, $quote); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
858 868 endforeach;
859 869 ?>
860 870 </div>
861 871 </div>
@@ -868,11 +878,11 @@
868 878
869 879 <div id="template-gallery-modal" style="display: none;" class="fixed inset-0 bg-gray-600 bg-opacity-75 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
870 880 <div class="mx-auto p-6 border w-full md:w-4/5 max-w-4xl shadow-lg rounded-md bg-white max-h-[80vh] overflow-y-auto">
871 881 <div class="flex justify-between items-center mb-4">
872 - <h3 class="text-xl font-medium text-gray-900"><?php _e('Browse Quote Templates', 'easy-invoice'); ?></h3>
882 + <h3 class="text-xl font-medium text-gray-900"><?php esc_html_e('Browse Quote Templates', 'easy-invoice'); ?></h3>
873 883 <button type="button" class="close-gallery-modal text-gray-400 hover:text-gray-500 focus:outline-none">
874 - <span class="sr-only"><?php _e('Close', 'easy-invoice'); ?></span>
884 + <span class="sr-only"><?php esc_html_e('Close', 'easy-invoice'); ?></span>
875 885 <i class="fas fa-times text-xl"></i>
876 886 </button>
877 887 </div>
878 888
@@ -882,9 +892,9 @@
882 892 foreach ($all_templates as $template_id => $template) :
883 893 $is_current = ($current_template === $template_id);
884 894 $selected_class = $is_current ? 'selected' : '';
885 895 ?>
886 - <div class="gallery-template-card <?php echo $selected_class; ?>" data-template-id="<?php echo esc_attr($template_id); ?>" data-is-premium="<?php echo isset($template['is_free']) && !$template['is_free'] ? 'true' : 'false'; ?>">
896 + <div class="gallery-template-card <?php echo esc_attr($selected_class); ?>" data-template-id="<?php echo esc_attr($template_id); ?>" data-is-premium="<?php echo isset($template['is_free']) && !$template['is_free'] ? 'true' : 'false'; ?>">
887 897 <div class="p-4 border rounded-lg">
888 898 <div class="flex items-center mb-2">
889 899 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
890 900 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -900,12 +910,12 @@
900 910 </div>
901 911
902 912 <div class="flex justify-end mt-4 space-x-3">
903 913 <button type="button" id="apply-selected-template" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
904 - <?php _e('Apply', 'easy-invoice'); ?>
914 + <?php esc_html_e('Apply', 'easy-invoice'); ?>
905 915 </button>
906 916 <button type="button" class="gallery-close-btn inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
907 - <?php _e('Close', 'easy-invoice'); ?>
917 + <?php esc_html_e('Close', 'easy-invoice'); ?>
908 918 </button>
909 919 </div>
910 920 </div>
911 921 </div>
@@ -917,9 +927,9 @@
917 927 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
918 928 <i class="fas fa-chevron-down text-sm"></i>
919 929 </button>
920 930 <h3 class="text-base font-normal text-gray-800 flex-1">
921 - <?php _e('New Item', 'easy-invoice'); ?>
931 + <?php esc_html_e('New Item', 'easy-invoice'); ?>
922 932 </h3>
923 933 </div>
924 934 <div class="flex items-center gap-3">
925 935 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
@@ -925,9 +935,9 @@
925 935 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
926 936 <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
927 937 = $<span class="total-summary">0.00</span>
928 938 </div>
929 - <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Fill this item with sample data', 'easy-invoice'); ?>">
939 + <button type="button" class="fill-sample-data-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Fill this item with sample data', 'easy-invoice')); ?>">
930 940 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
931 941 <path d="M2 2a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2zm0 1h16v14H2V3z"/>
932 942 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
933 943 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -934,14 +944,14 @@
934 944 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
935 945 </svg>
936 946 </button>
937 947 <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
938 - <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php _e('Select from Item Library', 'easy-invoice'); ?>">
948 + <button type="button" class="item-library-btn text-gray-400 hover:text-blue-600 p-2 rounded-lg hover:bg-blue-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Select from Item Library', 'easy-invoice')); ?>">
939 949 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
940 950 <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
941 951 </svg>
942 952 </button>
943 - <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php _e('Save to Item Library', 'easy-invoice'); ?>" data-item-index="" data-nonce="<?php echo wp_create_nonce('easy_invoice_pro_item_library'); ?>">
953 + <button type="button" class="save-to-library-btn text-gray-400 hover:text-green-600 p-2 rounded-lg hover:bg-green-50 transition-all duration-200" data-tooltip="<?php echo esc_js(__('Save to Item Library', 'easy-invoice')); ?>" data-item-index="" data-nonce="<?php echo esc_attr(wp_create_nonce('easy_invoice_pro_item_library')); ?>">
944 954 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
945 955 <path d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"/>
946 956 </svg>
947 957 </button>
@@ -946,14 +956,14 @@
946 956 </svg>
947 957 </button>
948 958 <?php endif; ?>
949 959 <button type="button" class="remove-item text-gray-400 hover:text-red-600 hover:bg-red-50 p-2 rounded-lg transition-all duration-200 text-sm font-medium">
950 - <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
960 + <i class="fas fa-trash-alt mr-1"></i> <?php esc_html_e('Remove', 'easy-invoice'); ?>
951 961 </button>
952 962 </div>
953 963 </div>
954 964 <div class="item-content">
955 - <?php echo $quote_form_manager->generateItemTemplateHtml(); ?>
965 + <?php echo $quote_form_manager->generateItemTemplateHtml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- form markup built by ItemFieldRenderer, which escapes every value. ?>
956 966 </div>
957 967 </div>
958 968 </template>
959 969
@@ -1011,11 +1021,11 @@
1011 1021
1012 1022 if (isFreeVersion) {
1013 1023 // Show premium upgrade modal for free users
1014 1024 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1015 - EasyInvoiceConfirmation.showFeatureUpgrade('<?php _e('Premium Templates', 'easy-invoice'); ?>', '<?php _e('Access all premium templates including Modern, Professional, Classic, and more to give your quotes a unique and professional look. Browse through our complete template library and find the perfect design for your business.', 'easy-invoice'); ?>');
1025 + EasyInvoiceConfirmation.showFeatureUpgrade('<?php echo esc_js(__('Premium Templates', 'easy-invoice')); ?>', '<?php echo esc_js(__('Access all premium templates including Modern, Professional, Classic, and more to give your quotes a unique and professional look. Browse through our complete template library and find the perfect design for your business.', 'easy-invoice')); ?>');
1016 1026 } else {
1017 - alert('<?php _e('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice'); ?>');
1027 + alert('<?php echo esc_js(__('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice')); ?>');
1018 1028 }
1019 1029 } else {
1020 1030 // Pro users can browse all templates
1021 1031 $('#template-gallery-modal').show();
@@ -1042,11 +1052,11 @@
1042 1052
1043 1053 if (isPremium && isFreeVersion) {
1044 1054 // Show premium upgrade modal instead of selecting the template
1045 1055 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1046 - EasyInvoiceConfirmation.showFeatureUpgrade('<?php _e('Premium Templates', 'easy-invoice'); ?>', '<?php _e('Access all premium templates including Modern, Professional, Classic, and more to give your quotes a unique and professional look.', 'easy-invoice'); ?>');
1056 + EasyInvoiceConfirmation.showFeatureUpgrade('<?php echo esc_js(__('Premium Templates', 'easy-invoice')); ?>', '<?php echo esc_js(__('Access all premium templates including Modern, Professional, Classic, and more to give your quotes a unique and professional look.', 'easy-invoice')); ?>');
1047 1057 } else {
1048 - alert('<?php _e('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice'); ?>');
1058 + alert('<?php echo esc_js(__('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice')); ?>');
1049 1059 }
1050 1060 return;
1051 1061 }
1052 1062
@@ -1200,9 +1210,9 @@
1200 1210 });
1201 1211
1202 1212 if (allCollapsed) {
1203 1213 // If all items are collapsed, expand them
1204 - $button.html('<i class="fas fa-chevron-down mr-1"></i> <?php _e('Collapse All', 'easy-invoice'); ?>');
1214 + $button.html('<i class="fas fa-chevron-down mr-1"></i> <?php esc_html_e('Collapse All', 'easy-invoice'); ?>');
1205 1215 $('.quote-item').each(function() {
1206 1216 const $item = $(this);
1207 1217 const itemContent = $item.find('.item-content');
1208 1218 const summaryElement = $item.find('.item-collapsed-summary');
@@ -1220,9 +1230,9 @@
1220 1230 }
1221 1231 });
1222 1232 } else {
1223 1233 // If any items are expanded, collapse them all
1224 - $button.html('<i class="fas fa-chevron-right mr-1"></i> <?php _e('Expand All', 'easy-invoice'); ?>');
1234 + $button.html('<i class="fas fa-chevron-right mr-1"></i> <?php esc_html_e('Expand All', 'easy-invoice'); ?>');
1225 1235 $('.quote-item').each(function() {
1226 1236 const $item = $(this);
1227 1237 const itemContent = $item.find('.item-content');
1228 1238 const summaryElement = $item.find('.item-collapsed-summary');
@@ -1252,9 +1262,10 @@
1252 1262 });
1253 1263
1254 1264 // Client search functionality
1255 1265 let searchTimeout;
1256 - let isSearching = false;
1266 + let isSearching = false; // the in-flight jqXHR, or false
1267 + let searchSeq = 0;
1257 1268 $('#client-search-input').on('input', function() {
1258 1269 const query = $(this).val().trim();
1259 1270 clearTimeout(searchTimeout);
1260 1271
@@ -1486,9 +1497,10 @@
1486 1497 // Update the collapsed summary of a quote item
1487 1498 function updateQuoteItemSummary(itemContainer) {
1488 1499 const quantity = parseFloat(itemContainer.find('input[name*="[quantity]"]').val()) || 0;
1489 1500 const price = parseFloat(itemContainer.find('input[name*="[price]"]').val()) || 0;
1490 - const total = quantity * price;
1501 + // Half-up to cents, matching PHP round($x, 2).
1502 + const total = Number(Math.round(Number((quantity * price) + 'e2')) + 'e-2');
1491 1503 itemContainer.find('.quantity-summary').text(quantity);
1492 1504 itemContainer.find('.price-summary').text(price.toFixed(2));
1493 1505 itemContainer.find('.total-summary').text(total.toFixed(2));
1494 1506 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
@@ -1494,22 +1506,20 @@
1494 1506 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
1495 1507 }
1496 1508
1497 1509 function searchClients(query) {
1498 -
1499 -
1500 - // Prevent multiple simultaneous requests
1501 - if (isSearching) {
1502 - return;
1503 - }
1504 -
1505 - // Clear any existing timeout to prevent multiple requests
1510 + // Latest request wins. Clicking the box fires a "show all" search and the
1511 + // user's first keystrokes arrive while it is in flight; refusing them here
1512 + // (the old isSearching gate) left the picker showing the first 50 clients
1513 + // instead of what was typed. Abort the in-flight one and ignore stale replies.
1506 1514 if (searchTimeout) {
1507 1515 clearTimeout(searchTimeout);
1508 1516 }
1517 + if (isSearching && isSearching.abort) {
1518 + isSearching.abort();
1519 + }
1520 + const requestSeq = ++searchSeq;
1509 1521
1510 - isSearching = true;
1511 -
1512 1522 $('#client-search-loading').show().removeClass('hidden');
1513 1523 $('#client-search-empty').hide().addClass('hidden');
1514 1524 $('#client-search-list').empty();
1515 1525
@@ -1519,9 +1529,9 @@
1519 1529 }
1520 1530
1521 1531
1522 1532
1523 - $.ajax({
1533 + isSearching = $.ajax({
1524 1534 url: easyInvoice.ajaxUrl,
1525 1535 type: 'POST',
1526 1536 data: {
1527 1537 action: 'easy_invoice_search_clients',
@@ -1529,9 +1539,11 @@
1529 1539 nonce: easyInvoice.nonce
1530 1540 },
1531 1541
1532 1542 success: function(response) {
1533 -
1543 + if (requestSeq !== searchSeq) {
1544 + return; // a newer search has replaced this one
1545 + }
1534 1546 isSearching = false;
1535 1547 $('#client-search-loading').hide().addClass('hidden');
1536 1548
1537 1549 if (response.success && response.data.length > 0) {
@@ -1536,21 +1548,22 @@
1536 1548
1537 1549 if (response.success && response.data.length > 0) {
1538 1550
1539 1551 response.data.forEach(function(client) {
1540 - const option = $(`
1541 - <div class="client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0"
1542 - data-client-id="${client.id}"
1543 - data-client-name="${(client.name || '').replace(/"/g, '&quot;')}"
1544 - data-client-email="${(client.email || '').replace(/"/g, '&quot;')}"
1545 - data-client-company="${(client.company || '').replace(/"/g, '&quot;')}"
1546 - data-client-phone="${(client.phone || '').replace(/"/g, '&quot;')}"
1547 - data-client-address="${(client.address || '').replace(/"/g, '&quot;')}"
1548 - onclick="selectClientFromOption(this)">
1549 - <div class="font-medium text-gray-900">${client.name || 'No Name'}</div>
1550 - <div class="text-sm text-gray-600">${client.email || 'No Email'}</div>
1551 - </div>
1552 - `);
1552 + // Built with jQuery, never by string interpolation: the name, company
1553 + // and address are what the client typed into their portal profile.
1554 + const option = $('<div>', {
1555 + 'class': 'client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0',
1556 + 'data-client-id': client.id,
1557 + 'data-client-name': client.name || '',
1558 + 'data-client-email': client.email || '',
1559 + 'data-client-company': client.company || '',
1560 + 'data-client-phone': client.phone || '',
1561 + 'data-client-website': client.website || '',
1562 + 'data-client-address': client.address || ''
1563 + }).on('click', function() { selectClientFromOption(this); })
1564 + .append($('<div>', { 'class': 'font-medium text-gray-900' }).text(client.name || ''))
1565 + .append($('<div>', { 'class': 'text-sm text-gray-600' }).text(client.email || ''));
1553 1566 $('#client-search-list').append(option);
1554 1567 });
1555 1568 $('#client-search-results').show().removeClass('hidden');
1556 1569
@@ -1559,8 +1572,11 @@
1559 1572 $('#client-search-results').show().removeClass('hidden');
1560 1573 }
1561 1574 },
1562 1575 error: function(xhr, status, error) {
1576 + if (status === 'abort' || requestSeq !== searchSeq) {
1577 + return; // superseded by a newer search
1578 + }
1563 1579 console.error('Quote form: AJAX error:', error);
1564 1580 console.error('Quote form: Status:', status);
1565 1581 console.error('Quote form: Response:', xhr.responseText);
1566 1582 isSearching = false;
@@ -1581,9 +1597,10 @@
1581 1597 $('#client-info-display').show().removeClass('hidden');
1582 1598 $('#no-client-message').hide().addClass('hidden');
1583 1599
1584 1600 // Update the edit client button URL
1585 - $('#edit-selected-client').attr('href', '<?php echo admin_url('admin.php?page=easy-invoice-client-edit&client_id='); ?>' + clientId);
1601 + $('#edit-selected-client').attr('href', '<?php echo esc_url(admin_url('admin.php?page=easy-invoice-client-edit&client_id=')); ?>' + clientId);
1602 + $('#edit-selected-client').toggleClass('hidden', !clientId);
1586 1603
1587 1604 // If we have full client data, populate all fields directly
1588 1605 if (clientData) {
1589 1606 $('#display-client-name').text(clientData.name || clientName || '-');
@@ -1786,10 +1803,10 @@
1786 1803 Cancel
1787 1804 </button>
1788 1805 <button type="submit" id="save-quote-btn" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
1789 1806 <i class="fas fa-save mr-2"></i>
1790 - <span>Save Quote</span>
1807 + <span><?php esc_html_e('Save Quote', 'easy-invoice'); ?></span>
1791 1808 </button>
1792 1809 </div>
1793 1810 </div>
1794 1811 </form>
1795 1812 <?php endif; ?>