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
← All changes | templates/quotes/form.php +214 -141 2.1.12.4.0 View file →
@@ -5,16 +5,19 @@
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 - wp_enqueue_style('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/css/invoice-form.css', array(), '1.0.0');
13 - wp_enqueue_script('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/js/invoice-form.js', array('jquery'), '1.0.0', true);
14 - wp_enqueue_script('easy-invoice-builder', plugin_dir_url(__FILE__) . '../../assets/js/invoice-builder.js', array('jquery', 'easy-invoice-form'), '1.0.0', true);
15 - wp_enqueue_script('easy-invoice-payment', plugin_dir_url(__FILE__) . '../../assets/js/payment-manager.js', array('jquery'), '1.0.0', true);
16 -wp_enqueue_script('easy-quote-save', plugin_dir_url(__FILE__) . '../../assets/js/quote-save.js', array('jquery', 'easy-invoice-form', 'easy-invoice-payment'), '1.0.0', true);
15 + wp_enqueue_style('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/css/invoice-form.css', array(), EASY_INVOICE_VERSION);
16 + wp_enqueue_script('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/js/invoice-form.js', array('jquery'), EASY_INVOICE_VERSION, true);
17 + wp_enqueue_script('easy-invoice-builder', plugin_dir_url(__FILE__) . '../../assets/js/invoice-builder.js', array('jquery', 'easy-invoice-form'), EASY_INVOICE_VERSION, true);
18 + wp_enqueue_script('easy-invoice-payment', plugin_dir_url(__FILE__) . '../../assets/js/payment-manager.js', array('jquery'), EASY_INVOICE_VERSION, true);
19 + wp_enqueue_script('easy-quote-save', plugin_dir_url(__FILE__) . '../../assets/js/quote-save.js', array('jquery', 'easy-invoice-form', 'easy-invoice-payment'), EASY_INVOICE_VERSION, true);
17 20 }
18 21
19 22 // Trigger form initialization to allow pro version to register new elements
20 23 do_action('easy_invoice_form_init');
@@ -219,14 +222,13 @@
219 222 <input type="hidden" id="quote-id" name="quote_id" value="<?php echo $quote_id ? esc_attr($quote_id) : ''; ?>">
220 223 <?php endif; ?>
221 224
222 225 <div class="bg-white shadow rounded-lg">
223 - <!-- Tab Navigation -->
224 226 <div class="border-b border-gray-200">
225 - <nav class="flex -mb-px">
227 + <nav class="flex -mb-px" aria-label="<?php esc_attr_e( 'Quote form sections', 'easy-invoice' ); ?>">
226 228 <?php foreach ($tabs as $tab): ?>
227 229 <button type="button"
228 - class="tab-button whitespace-nowrap py-3 px-6 border-b-2 <?php echo $tab['active'] ? 'border-indigo-500 font-medium text-sm text-indigo-600' : 'border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?> flex flex-col items-center"
230 + class="tab-button whitespace-nowrap py-3 px-6 border-b-2 <?php echo $tab['active'] ? 'border-indigo-500 text-sm text-indigo-600' : 'border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?> flex flex-col items-center"
229 231 data-tab="<?php echo esc_attr($tab['id']); ?>">
230 232 <i class="<?php echo esc_attr($tab['icon']); ?> mb-1 text-lg"></i>
231 233 <span><?php echo esc_html($tab['label']); ?></span>
232 234 </button>
@@ -233,19 +235,17 @@
233 235 <?php endforeach; ?>
234 236 </nav>
235 237 </div>
236 238
237 - <!-- Form content starts here (form tag removed to avoid nesting) -->
238 239
239 240 <?php foreach ($tabs as $tab): ?>
240 241 <div class="tab-content <?php echo $tab['active'] ? 'active' : 'hidden'; ?> px-6 py-5" id="<?php echo esc_attr($tab['id']); ?>">
241 242 <?php if ($tab['id'] === 'quote-tab'): ?>
242 - <!-- Template Selection Section -->
243 243 <div class="mb-8 pb-6 border-b border-gray-200">
244 244 <div class="flex justify-between items-center mb-4">
245 - <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>
246 246 <button type="button" id="browse-all-templates" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
247 - <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>
248 248 <i class="fas fa-chevron-right"></i>
249 249 </button>
250 250 </div>
251 251
@@ -315,10 +315,10 @@
315 315 if ($is_free_version && $is_premium) {
316 316 $premium_classes = 'premium-template-blurred';
317 317 }
318 318 ?>
319 - <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'; ?>">
320 - <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' : ''); ?>>
321 321 <div class="p-4 border rounded-lg">
322 322 <div class="flex items-center mb-2">
323 323 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
324 324 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -333,9 +333,8 @@
333 333 <?php endforeach; ?>
334 334 </div>
335 335 </div>
336 336
337 - <!-- Dynamic Fields Section -->
338 337 <div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
339 338 <?php
340 339 $main_fields = $quote_form_manager->getFields('quote-tab');
341 340
@@ -345,15 +344,14 @@
345 344 // Skip fields that have a section (like notes) - they're rendered separately
346 345 // Also skip the template field as it's rendered manually in the template selection section
347 346 if (!isset($field['section']) && $field['name'] !== 'quote_template') {
348 347
349 - 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.
350 349 }
351 350 endforeach;
352 351 ?>
353 352 </div>
354 353
355 - <!-- Auto-generation notice for new quotes -->
356 354 <?php if (!$quote): ?>
357 355 <div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
358 356 <div class="flex">
359 357 <div class="flex-shrink-0">
@@ -360,9 +358,9 @@
360 358 <i class="fas fa-info-circle text-blue-400"></i>
361 359 </div>
362 360 <div class="ml-3">
363 361 <p class="text-sm text-blue-700">
364 - <?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'); ?>
365 363 </p>
366 364 </div>
367 365 </div>
368 366 </div>
@@ -367,17 +365,16 @@
367 365 </div>
368 366 </div>
369 367 <?php endif; ?>
370 368
371 - <!-- Notes & Terms Section -->
372 369 <div class="mt-8 border-t border-gray-200 pt-6">
373 - <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>
374 371 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:col-span-6">
375 372 <?php
376 373 // Only render notes and terms fields, not all quote tab fields
377 374 $notes_fields = $quote_form_manager->getFieldsBySection('quote-tab', 'notes');
378 375 foreach ($notes_fields as $field):
379 - 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.
380 377 endforeach;
381 378 ?>
382 379 </div>
383 380 </div>
@@ -382,14 +379,13 @@
382 379 </div>
383 380 </div>
384 381
385 382 <?php elseif ($tab['id'] === 'items-tab'): ?>
386 - <!-- Items Tab Content -->
387 383 <div class="flex justify-between items-center mb-6">
388 - <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>
389 385 <button type="button" id="collapse-all-items" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
390 386 <i class="fas fa-chevron-down mr-1"></i>
391 - <span><?php _e('Collapse All', 'easy-invoice'); ?></span>
387 + <span><?php esc_html_e('Collapse All', 'easy-invoice'); ?></span>
392 388 </button>
393 389 </div>
394 390
395 391 <div class="space-y-4 quote-items-container">
@@ -398,9 +394,9 @@
398 394 if ($quote && $quote->getItems()) :
399 395 $index = 0;
400 396 foreach ($quote->getItems() as $item) :
401 397 // Generate a unique ID for the item
402 - $item_id = 'item_' . time() . '_' . $index . '_' . rand(1000, 9999);
398 + $item_id = 'item_' . time() . '_' . $index . '_' . wp_rand(1000, 9999);
403 399
404 400 // Get display title for header using dynamic field access
405 401 $title = $item->getName();
406 402
@@ -411,12 +407,13 @@
411 407 $title = $item_data['title'];
412 408 }
413 409 }
414 410
411 + /* translators: %d: title. */
415 412 $display_title = !empty($title) ? $title : sprintf(__('Item #%d', 'easy-invoice'), $index + 1);
416 413 $short_title = strlen($display_title) > 30 ? substr($display_title, 0, 30) . '...' : $display_title;
417 414 ?>
418 - <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; ?>">
419 416 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
420 417 <div class="flex items-center">
421 418 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
422 419 <i class="fas fa-chevron-down text-sm"></i>
@@ -422,9 +419,9 @@
422 419 <i class="fas fa-chevron-down text-sm"></i>
423 420 </button>
424 421 <h3 class="text-base font-normal text-gray-800 flex-1">
425 422 <?php
426 - echo !empty($title) ? esc_html($title) : __('New Item', 'easy-invoice');
423 + echo !empty($title) ? esc_html($title) : esc_html__('New Item', 'easy-invoice');
427 424 ?>
428 425 </h3>
429 426 </div>
430 427 <div class="flex items-center gap-3">
@@ -431,9 +428,9 @@
431 428 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
432 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>
433 430 = $<span class="total-summary font-medium"><?php echo esc_html(number_format($item->getAmount(), 2)); ?></span>
434 431 </div>
435 - <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')); ?>">
436 433 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
437 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"/>
438 435 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
439 436 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -439,15 +436,28 @@
439 436 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
440 437 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
441 438 </svg>
442 439 </button>
440 + <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
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')); ?>">
442 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
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"/>
444 + </svg>
445 + </button>
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')); ?>">
447 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
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"/>
449 + </svg>
450 + </button>
451 + <?php endif; ?>
443 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">
444 - <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'); ?>
445 454 </button>
446 455 </div>
447 456 </div>
448 457 <div class="item-content">
449 - <?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 +?>
450 460 </div>
451 461 </div>
452 462 <?php
453 463 $index++;
@@ -480,12 +490,12 @@
480 490 </h3>
481 491 </div>
482 492 <div class="flex items-center gap-3">
483 493 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
484 - <span class="quantity-summary">1</span> × $<span class="price-summary">0.00</span>
485 - = $<span class="total-summary">0.00</span>
494 + <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
495 + = $<span class="total-summary font-medium">0.00</span>
486 496 </div>
487 - <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')); ?>">
488 498 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
489 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"/>
490 500 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
491 501 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -491,15 +501,27 @@
491 501 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
492 502 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
493 503 </svg>
494 504 </button>
505 + <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
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')); ?>">
507 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
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"/>
509 + </svg>
510 + </button>
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')); ?>">
512 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
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"/>
514 + </svg>
515 + </button>
516 + <?php endif; ?>
495 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">
496 - <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'); ?>
497 519 </button>
498 520 </div>
499 521 </div>
500 522 <div class="item-content">
501 - <?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. ?>
502 524 </div>
503 525 </div>
504 526 <?php endif; ?>
505 527 </div>
@@ -510,10 +532,10 @@
510 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">
511 533 <i class="fas fa-plus text-indigo-600 text-lg"></i>
512 534 </div>
513 535 <div class="text-left">
514 - <span class="font-semibold text-base"><?php _e('Add Another Item', 'easy-invoice'); ?></span>
515 - <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>
516 538 </div>
517 539 </div>
518 540 </button>
519 541 </div>
@@ -518,13 +540,11 @@
518 540 </button>
519 541 </div>
520 542
521 543 <?php elseif ($tab['id'] === 'client-tab'): ?>
522 - <!-- Client Tab Content -->
523 544 <div class="mb-6">
524 - <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>
525 546
526 - <!-- Client Search and Selection -->
527 547 <div class="relative">
528 548 <div class="flex items-center space-x-3">
529 549 <div class="relative flex-grow">
530 550 <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -533,34 +553,31 @@
533 553 <input
534 554 type="text"
535 555 id="client-search-input"
536 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"
537 - 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')); ?>"
538 558 autocomplete="off"
539 559 >
540 560 </div>
541 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">
542 562 <i class="fas fa-plus -ml-1 mr-2"></i>
543 - <?php _e('New Client', 'easy-invoice'); ?>
563 + <?php esc_html_e('New Client', 'easy-invoice'); ?>
544 564 </button>
545 565 </div>
546 566
547 - <!-- Search Results Dropdown -->
548 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">
549 568 <div id="client-search-loading" class="hidden p-6 text-center text-gray-500">
550 569 <i class="fas fa-spinner fa-spin mr-2"></i>
551 - <?php _e('Searching...', 'easy-invoice'); ?>
570 + <?php esc_html_e('Searching...', 'easy-invoice'); ?>
552 571 </div>
553 572 <div id="client-search-empty" class="hidden p-6 text-center text-gray-500">
554 573 <i class="fas fa-search mr-2"></i>
555 - <?php _e('No clients found', 'easy-invoice'); ?>
574 + <?php esc_html_e('No clients found', 'easy-invoice'); ?>
556 575 </div>
557 576 <div id="client-search-list" class="py-2">
558 - <!-- Client options will be populated here -->
559 577 </div>
560 578 </div>
561 579
562 - <!-- Selected Client Display -->
563 580 <div id="selected-client-display" class="mt-4 p-4 bg-gradient-to-r from-indigo-50 to-blue-50 border border-indigo-200 rounded-lg shadow-sm <?php echo ($quote && $quote->getClientId()) ? '' : 'hidden'; ?>">
564 581 <div class="flex items-center justify-between">
565 582 <div class="flex items-center">
566 583 <div class="flex-shrink-0">
@@ -605,12 +622,16 @@
605 622 </div>
606 623 </div>
607 624 </div>
608 625
609 - <!-- Hidden select for form submission -->
610 626 <select id="select-client" class="hidden">
611 - <option value=""><?php _e('-- Select a client --', 'easy-invoice'); ?></option>
627 + <option value=""><?php esc_html_e('-- Select a client --', 'easy-invoice'); ?></option>
612 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 + }
613 634 // Output client options
614 635 if (!empty($clients)) {
615 636 foreach ($clients as $client) {
616 637 // Get the WordPress user data directly
@@ -634,9 +655,9 @@
634 655 $selected = $quote && $quote->getClientId() == $client->getId() ? 'selected' : '';
635 656 printf(
636 657 '<option value="%s" %s data-name="%s" data-email="%s">%s (%s)</option>',
637 658 esc_attr($client->getId()),
638 - $selected,
659 + $selected, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output of selected( ..., false ).
639 660 esc_attr($client_name),
640 661 esc_attr($client_email),
641 662 esc_html($client_name),
642 663 esc_html($client_email)
@@ -649,31 +670,29 @@
649 670
650 671 <div class="border-t border-gray-200 pt-4 mt-4">
651 672 <input type="hidden" name="client_id" id="client-id" value="<?php echo $quote ? esc_attr($quote->getClientId()) : ''; ?>">
652 673
653 - <!-- Client Information Display -->
654 674 <div id="client-info-display" class="mt-6 <?php echo $quote && $quote->getClientId() ? '' : 'hidden'; ?>">
655 675 <div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
656 676 <div class="flex items-center justify-between pb-4 border-b border-gray-200 mb-4">
657 677 <h4 class="text-lg font-semibold text-gray-800 flex items-center">
658 678 <i class="fas fa-user-check text-indigo-500 mr-3"></i>
659 - <span><?php _e('Client Information', 'easy-invoice'); ?></span>
679 + <span><?php esc_html_e('Client Information', 'easy-invoice'); ?></span>
660 680 </h4>
661 - <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() : ''))); ?>"
662 682 id="edit-selected-client"
663 - 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'; ?>"
664 684 target="_blank">
665 685 <i class="fas fa-pen mr-1"></i>
666 - <?php _e('Edit', 'easy-invoice'); ?>
686 + <?php esc_html_e('Edit', 'easy-invoice'); ?>
667 687 </a>
668 688 </div>
669 689
670 690 <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 pt-2">
671 - <!-- Client Name -->
672 691 <div class="flex items-start group">
673 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>
674 693 <div class="ml-3">
675 - <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>
676 695 <p id="display-client-name" class="text-sm font-semibold text-gray-800 mt-1">
677 696 <?php
678 697 if ($quote && $quote->getClientId()) {
679 698 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -689,13 +708,12 @@
689 708 </p>
690 709 </div>
691 710 </div>
692 711
693 - <!-- Company Name -->
694 712 <div class="flex items-start group">
695 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>
696 714 <div class="ml-3">
697 - <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>
698 716 <p id="display-client-company" class="text-sm font-semibold text-gray-800 mt-1">
699 717 <?php
700 718 if ($quote && $quote->getClientId()) {
701 719 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -711,13 +729,12 @@
711 729 </p>
712 730 </div>
713 731 </div>
714 732
715 - <!-- Email -->
716 733 <div class="flex items-start group">
717 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>
718 735 <div class="ml-3">
719 - <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>
720 737 <p id="display-client-email" class="text-sm text-gray-800 mt-1 break-all">
721 738 <?php
722 739 if ($quote && $quote->getClientId()) {
723 740 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -733,13 +750,12 @@
733 750 </p>
734 751 </div>
735 752 </div>
736 753
737 - <!-- Phone -->
738 754 <div class="flex items-start group">
739 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>
740 756 <div class="ml-3">
741 - <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>
742 758 <p id="display-client-phone" class="text-sm text-gray-800 mt-1">
743 759 <?php
744 760 if ($quote && $quote->getClientId()) {
745 761 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -755,13 +771,12 @@
755 771 </p>
756 772 </div>
757 773 </div>
758 774
759 - <!-- Website -->
760 775 <div class="flex items-start group md:col-span-2">
761 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>
762 777 <div class="ml-3">
763 - <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>
764 779 <p id="display-client-website" class="text-sm text-gray-800 mt-1">
765 780 <?php
766 781 if ($quote && $quote->getClientId()) {
767 782 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -777,13 +792,12 @@
777 792 </p>
778 793 </div>
779 794 </div>
780 795
781 - <!-- Address -->
782 796 <div class="flex items-start group md:col-span-2">
783 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>
784 798 <div class="ml-3">
785 - <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>
786 800 <p id="display-client-address" class="text-sm text-gray-800 mt-1 whitespace-pre-wrap">
787 801 <?php
788 802 if ($quote && $quote->getClientId()) {
789 803 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -802,18 +816,16 @@
802 816 </div>
803 817 </div>
804 818 </div>
805 819
806 - <!-- No Client Selected Message -->
807 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' : ''; ?>">
808 - <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>
809 822 </div>
810 823 </div>
811 824
812 825 <?php elseif ($tab['id'] === 'discounts-taxes-tab'): ?>
813 - <!-- Discount & Tax Tab Content -->
814 826 <div>
815 - <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>
816 828
817 829 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
818 830 <?php
819 831 // Get all discount and tax fields
@@ -827,9 +839,9 @@
827 839 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
828 840 });
829 841
830 842 foreach ($all_fields as $field):
831 - 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.
832 844 endforeach;
833 845 ?>
834 846 </div>
835 847 </div>
@@ -834,28 +846,26 @@
834 846 </div>
835 847 </div>
836 848
837 849 <?php elseif ($tab['id'] === 'payments-tab'): ?>
838 - <!-- Payment Tab Content -->
839 850 <div>
840 - <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>
841 852 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
842 853 <?php
843 854 $payment_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'payment');
844 855 foreach ($payment_fields as $field):
845 - 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.
846 857 endforeach;
847 858 ?>
848 859 </div>
849 860
850 - <!-- Currency Section -->
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>
@@ -865,15 +875,14 @@
865 875 </div>
866 876 <?php endforeach; ?>
867 877 </div>
868 878
869 -<!-- Template Gallery Modal -->
870 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">
871 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">
872 881 <div class="flex justify-between items-center mb-4">
873 - <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>
874 883 <button type="button" class="close-gallery-modal text-gray-400 hover:text-gray-500 focus:outline-none">
875 - <span class="sr-only"><?php _e('Close', 'easy-invoice'); ?></span>
884 + <span class="sr-only"><?php esc_html_e('Close', 'easy-invoice'); ?></span>
876 885 <i class="fas fa-times text-xl"></i>
877 886 </button>
878 887 </div>
879 888
@@ -883,9 +892,9 @@
883 892 foreach ($all_templates as $template_id => $template) :
884 893 $is_current = ($current_template === $template_id);
885 894 $selected_class = $is_current ? 'selected' : '';
886 895 ?>
887 - <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'; ?>">
888 897 <div class="p-4 border rounded-lg">
889 898 <div class="flex items-center mb-2">
890 899 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
891 900 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -901,18 +910,17 @@
901 910 </div>
902 911
903 912 <div class="flex justify-end mt-4 space-x-3">
904 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">
905 - <?php _e('Apply', 'easy-invoice'); ?>
914 + <?php esc_html_e('Apply', 'easy-invoice'); ?>
906 915 </button>
907 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">
908 - <?php _e('Close', 'easy-invoice'); ?>
917 + <?php esc_html_e('Close', 'easy-invoice'); ?>
909 918 </button>
910 919 </div>
911 920 </div>
912 921 </div>
913 922
914 -<!-- Hidden Template for Quote Items -->
915 923 <template id="quote-item-template" style="display: none;">
916 924 <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;">
917 925 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
918 926 <div class="flex items-center">
@@ -919,17 +927,17 @@
919 927 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
920 928 <i class="fas fa-chevron-down text-sm"></i>
921 929 </button>
922 930 <h3 class="text-base font-normal text-gray-800 flex-1">
923 - <?php _e('New Item', 'easy-invoice'); ?>
931 + <?php esc_html_e('New Item', 'easy-invoice'); ?>
924 932 </h3>
925 933 </div>
926 934 <div class="flex items-center gap-3">
927 935 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
928 - <span class="quantity-summary">1</span> × $<span class="price-summary">0.00</span>
936 + <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
929 937 = $<span class="total-summary">0.00</span>
930 938 </div>
931 - <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')); ?>">
932 940 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
933 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"/>
934 942 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
935 943 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -935,15 +943,27 @@
935 943 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
936 944 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
937 945 </svg>
938 946 </button>
947 + <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
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')); ?>">
949 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
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"/>
951 + </svg>
952 + </button>
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')); ?>">
954 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
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"/>
956 + </svg>
957 + </button>
958 + <?php endif; ?>
939 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">
940 - <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'); ?>
941 961 </button>
942 962 </div>
943 963 </div>
944 964 <div class="item-content">
945 - <?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. ?>
946 966 </div>
947 967 </div>
948 968 </template>
949 969
@@ -1001,11 +1021,11 @@
1001 1021
1002 1022 if (isFreeVersion) {
1003 1023 // Show premium upgrade modal for free users
1004 1024 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1005 - 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')); ?>');
1006 1026 } else {
1007 - 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')); ?>');
1008 1028 }
1009 1029 } else {
1010 1030 // Pro users can browse all templates
1011 1031 $('#template-gallery-modal').show();
@@ -1032,11 +1052,11 @@
1032 1052
1033 1053 if (isPremium && isFreeVersion) {
1034 1054 // Show premium upgrade modal instead of selecting the template
1035 1055 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1036 - 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')); ?>');
1037 1057 } else {
1038 - 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')); ?>');
1039 1059 }
1040 1060 return;
1041 1061 }
1042 1062
@@ -1190,9 +1210,9 @@
1190 1210 });
1191 1211
1192 1212 if (allCollapsed) {
1193 1213 // If all items are collapsed, expand them
1194 - $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'); ?>');
1195 1215 $('.quote-item').each(function() {
1196 1216 const $item = $(this);
1197 1217 const itemContent = $item.find('.item-content');
1198 1218 const summaryElement = $item.find('.item-collapsed-summary');
@@ -1210,9 +1230,9 @@
1210 1230 }
1211 1231 });
1212 1232 } else {
1213 1233 // If any items are expanded, collapse them all
1214 - $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'); ?>');
1215 1235 $('.quote-item').each(function() {
1216 1236 const $item = $(this);
1217 1237 const itemContent = $item.find('.item-content');
1218 1238 const summaryElement = $item.find('.item-collapsed-summary');
@@ -1242,9 +1262,10 @@
1242 1262 });
1243 1263
1244 1264 // Client search functionality
1245 1265 let searchTimeout;
1246 - let isSearching = false;
1266 + let isSearching = false; // the in-flight jqXHR, or false
1267 + let searchSeq = 0;
1247 1268 $('#client-search-input').on('input', function() {
1248 1269 const query = $(this).val().trim();
1249 1270 clearTimeout(searchTimeout);
1250 1271
@@ -1476,9 +1497,10 @@
1476 1497 // Update the collapsed summary of a quote item
1477 1498 function updateQuoteItemSummary(itemContainer) {
1478 1499 const quantity = parseFloat(itemContainer.find('input[name*="[quantity]"]').val()) || 0;
1479 1500 const price = parseFloat(itemContainer.find('input[name*="[price]"]').val()) || 0;
1480 - 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');
1481 1503 itemContainer.find('.quantity-summary').text(quantity);
1482 1504 itemContainer.find('.price-summary').text(price.toFixed(2));
1483 1505 itemContainer.find('.total-summary').text(total.toFixed(2));
1484 1506 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
@@ -1484,22 +1506,20 @@
1484 1506 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
1485 1507 }
1486 1508
1487 1509 function searchClients(query) {
1488 -
1489 -
1490 - // Prevent multiple simultaneous requests
1491 - if (isSearching) {
1492 - return;
1493 - }
1494 -
1495 - // 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.
1496 1514 if (searchTimeout) {
1497 1515 clearTimeout(searchTimeout);
1498 1516 }
1517 + if (isSearching && isSearching.abort) {
1518 + isSearching.abort();
1519 + }
1520 + const requestSeq = ++searchSeq;
1499 1521
1500 - isSearching = true;
1501 -
1502 1522 $('#client-search-loading').show().removeClass('hidden');
1503 1523 $('#client-search-empty').hide().addClass('hidden');
1504 1524 $('#client-search-list').empty();
1505 1525
@@ -1509,9 +1529,9 @@
1509 1529 }
1510 1530
1511 1531
1512 1532
1513 - $.ajax({
1533 + isSearching = $.ajax({
1514 1534 url: easyInvoice.ajaxUrl,
1515 1535 type: 'POST',
1516 1536 data: {
1517 1537 action: 'easy_invoice_search_clients',
@@ -1519,9 +1539,11 @@
1519 1539 nonce: easyInvoice.nonce
1520 1540 },
1521 1541
1522 1542 success: function(response) {
1523 -
1543 + if (requestSeq !== searchSeq) {
1544 + return; // a newer search has replaced this one
1545 + }
1524 1546 isSearching = false;
1525 1547 $('#client-search-loading').hide().addClass('hidden');
1526 1548
1527 1549 if (response.success && response.data.length > 0) {
@@ -1526,21 +1548,22 @@
1526 1548
1527 1549 if (response.success && response.data.length > 0) {
1528 1550
1529 1551 response.data.forEach(function(client) {
1530 - const option = $(`
1531 - <div class="client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0"
1532 - data-client-id="${client.id}"
1533 - data-client-name="${(client.name || '').replace(/"/g, '&quot;')}"
1534 - data-client-email="${(client.email || '').replace(/"/g, '&quot;')}"
1535 - data-client-company="${(client.company || '').replace(/"/g, '&quot;')}"
1536 - data-client-phone="${(client.phone || '').replace(/"/g, '&quot;')}"
1537 - data-client-address="${(client.address || '').replace(/"/g, '&quot;')}"
1538 - onclick="selectClientFromOption(this)">
1539 - <div class="font-medium text-gray-900">${client.name || 'No Name'}</div>
1540 - <div class="text-sm text-gray-600">${client.email || 'No Email'}</div>
1541 - </div>
1542 - `);
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 || ''));
1543 1566 $('#client-search-list').append(option);
1544 1567 });
1545 1568 $('#client-search-results').show().removeClass('hidden');
1546 1569
@@ -1549,8 +1572,11 @@
1549 1572 $('#client-search-results').show().removeClass('hidden');
1550 1573 }
1551 1574 },
1552 1575 error: function(xhr, status, error) {
1576 + if (status === 'abort' || requestSeq !== searchSeq) {
1577 + return; // superseded by a newer search
1578 + }
1553 1579 console.error('Quote form: AJAX error:', error);
1554 1580 console.error('Quote form: Status:', status);
1555 1581 console.error('Quote form: Response:', xhr.responseText);
1556 1582 isSearching = false;
@@ -1571,9 +1597,10 @@
1571 1597 $('#client-info-display').show().removeClass('hidden');
1572 1598 $('#no-client-message').hide().addClass('hidden');
1573 1599
1574 1600 // Update the edit client button URL
1575 - $('#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);
1576 1603
1577 1604 // If we have full client data, populate all fields directly
1578 1605 if (clientData) {
1579 1606 $('#display-client-name').text(clientData.name || clientName || '-');
@@ -1605,37 +1632,80 @@
1605 1632 }
1606 1633
1607 1634 function calculateTotals() {
1608 1635 let subtotal = 0;
1636 + let taxableSubtotal = 0;
1609 1637 $('.quote-item').each(function() {
1610 1638 const quantity = parseFloat($(this).find('input[name*="[quantity]"]').val()) || 0;
1611 1639 const price = parseFloat($(this).find('input[name*="[price]"]').val()) || 0;
1612 - const total = quantity * price;
1640 + let adjustPercentage = 0;
1641 + // Only apply adjust percentage if the adjust field is enabled
1642 + if (window.easyInvoice && window.easyInvoice.showAdjustField) {
1643 + adjustPercentage = parseFloat($(this).find('input[name*="[adjust_percentage]"]').val()) || 0;
1644 + }
1645 + const isTaxable = $(this).find('input[name*="[taxable]"]').is(':checked');
1646 + const baseTotal = quantity * price;
1647 + const total = baseTotal * (1 + adjustPercentage / 100);
1613 1648 $(this).find('.total-summary').text(total.toFixed(2));
1614 1649 $(this).find('input[name*="[total]"]').val(total.toFixed(2));
1615 1650 subtotal += total;
1651 + if (isTaxable) {
1652 + taxableSubtotal += total;
1653 + }
1616 1654 });
1617 1655 $('input[name="subtotal"]').val(subtotal.toFixed(2));
1618 - calculateTaxAndDiscount();
1656 + calculateTaxAndDiscount(subtotal, taxableSubtotal);
1619 1657 }
1620 1658
1621 - function calculateTaxAndDiscount() {
1622 - const subtotal = parseFloat($('input[name="subtotal"]').val()) || 0;
1659 + function calculateTaxAndDiscount(subtotal, taxableSubtotal) {
1660 + subtotal = (subtotal !== undefined) ? subtotal : (parseFloat($('input[name="subtotal"]').val()) || 0);
1661 + taxableSubtotal = (taxableSubtotal !== undefined) ? taxableSubtotal : subtotal;
1662 +
1623 1663 const discountValue = parseFloat($('input[name="discount_value"]').val()) || 0;
1624 1664 const discountType = $('select[name="discount_type"]').val();
1665 + const calculationMethod = $('select[name="discount_calculation_method"]').val() || 'before_tax';
1625 1666 const taxRate = parseFloat($('input[name="tax_rate"]').val()) || 0;
1626 -
1667 + const pricesIncludeTax = $('select[name="prices_include_tax"]').val() === 'yes';
1668 +
1669 + // Calculate discount
1627 1670 let discountAmount = 0;
1628 1671 if (discountType === 'percentage') {
1629 1672 discountAmount = subtotal * (discountValue / 100);
1673 + } else if (discountType === 'fixed') {
1674 + discountAmount = Math.min(discountValue, subtotal);
1675 + }
1676 + // discountType === 'none': discountAmount stays 0
1677 +
1678 + const afterDiscountAmount = subtotal - discountAmount;
1679 + const discountRatio = (subtotal > 0) ? (discountAmount / subtotal) : 0;
1680 +
1681 + // Calculate tax
1682 + let taxAmount = 0;
1683 + let total = 0;
1684 +
1685 + if (pricesIncludeTax && taxRate > 0) {
1686 + // Extract tax from taxable portion (reverse calculation)
1687 + const taxableAfterDiscount = taxableSubtotal * (1 - discountRatio);
1688 + taxAmount = taxableAfterDiscount - (taxableAfterDiscount / (1 + taxRate / 100));
1689 + total = afterDiscountAmount;
1690 + } else if (taxRate > 0) {
1691 + if (calculationMethod === 'before_tax') {
1692 + // Discount first, then tax on remaining taxable amount
1693 + const taxableAfterDiscount = taxableSubtotal * (1 - discountRatio);
1694 + taxAmount = taxableAfterDiscount * (taxRate / 100);
1695 + } else {
1696 + // after_tax: Tax first on full taxable subtotal, then discount
1697 + taxAmount = taxableSubtotal * (taxRate / 100);
1698 + if (discountType === 'percentage') {
1699 + const totalBeforeDiscount = subtotal + taxAmount;
1700 + discountAmount = totalBeforeDiscount * (discountValue / 100);
1701 + }
1702 + }
1703 + total = (subtotal - discountAmount) + taxAmount;
1630 1704 } else {
1631 - discountAmount = discountValue;
1705 + total = afterDiscountAmount;
1632 1706 }
1633 -
1634 - const taxableAmount = subtotal - discountAmount;
1635 - const taxAmount = taxableAmount * (taxRate / 100);
1636 - const total = taxableAmount + taxAmount;
1637 -
1707 +
1638 1708 $('input[name="discount_amount"]').val(discountAmount.toFixed(2));
1639 1709 $('input[name="tax_amount"]').val(taxAmount.toFixed(2));
1640 1710 $('input[name="total"]').val(total.toFixed(2));
1641 1711 }
@@ -1671,18 +1741,22 @@
1671 1741 // Client initialized successfully
1672 1742 }
1673 1743
1674 1744 // Bind calculation events
1675 - $(document).on('input', 'input[name*="[quantity]"], input[name*="[price]"]', function() {
1745 + $(document).on('input', 'input[name*="[quantity]"], input[name*="[price]"], input[name*="[adjust_percentage]"]', function() {
1676 1746 calculateTotals();
1677 1747 });
1748 +
1749 + $(document).on('change', 'input[name*="[taxable]"]', function() {
1750 + calculateTotals();
1751 + });
1678 1752
1679 1753 $(document).on('input', 'input[name="discount_value"], input[name="tax_rate"]', function() {
1680 - calculateTaxAndDiscount();
1754 + calculateTotals();
1681 1755 });
1682 1756
1683 - $(document).on('change', 'select[name="discount_type"]', function() {
1684 - calculateTaxAndDiscount();
1757 + $(document).on('change', 'select[name="discount_type"], select[name="discount_calculation_method"], select[name="prices_include_tax"]', function() {
1758 + calculateTotals();
1685 1759 });
1686 1760
1687 1761 // Click on search input to show all clients
1688 1762 $("#client-search-input").on("click", function() {
@@ -1722,9 +1796,8 @@
1722 1796 // Quote field configuration and pre-loaded data
1723 1797 </script>
1724 1798
1725 1799 <?php if (wp_doing_ajax()): ?>
1726 - <!-- Save Button Section for Modal -->
1727 1800 <div class="mt-8 pt-6 border-t border-gray-200">
1728 1801 <div class="flex justify-end space-x-3">
1729 1802 <button type="button" id="cancel-quote-btn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">
1730 1803 Cancel
@@ -1730,10 +1803,10 @@
1730 1803 Cancel
1731 1804 </button>
1732 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">
1733 1806 <i class="fas fa-save mr-2"></i>
1734 - <span>Save Quote</span>
1807 + <span><?php esc_html_e('Save Quote', 'easy-invoice'); ?></span>
1735 1808 </button>
1736 1809 </div>
1737 1810 </div>
1738 1811 </form>
1739 1812 <?php endif; ?>