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 +184 -147 2.1.132.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,15 @@
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>
443 - <?php if (class_exists('EasyInvoicePro\\Services\\ItemLibraryService')): ?>
444 - <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'); ?>">
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')); ?>">
445 442 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
446 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"/>
447 444 </svg>
448 445 </button>
449 - <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')); ?>">
450 447 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
451 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"/>
452 449 </svg>
453 450 </button>
@@ -452,14 +449,15 @@
452 449 </svg>
453 450 </button>
454 451 <?php endif; ?>
455 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">
456 - <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'); ?>
457 454 </button>
458 455 </div>
459 456 </div>
460 457 <div class="item-content">
461 - <?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 +?>
462 460 </div>
463 461 </div>
464 462 <?php
465 463 $index++;
@@ -495,9 +493,9 @@
495 493 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
496 494 <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
497 495 = $<span class="total-summary font-medium">0.00</span>
498 496 </div>
499 - <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')); ?>">
500 498 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
501 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"/>
502 500 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
503 501 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -503,15 +501,15 @@
503 501 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
504 502 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
505 503 </svg>
506 504 </button>
507 - <?php if (class_exists('EasyInvoicePro\\Services\\ItemLibraryService')): ?>
508 - <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'); ?>">
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')); ?>">
509 507 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
510 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"/>
511 509 </svg>
512 510 </button>
513 - <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')); ?>">
514 512 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
515 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"/>
516 514 </svg>
517 515 </button>
@@ -516,14 +514,14 @@
516 514 </svg>
517 515 </button>
518 516 <?php endif; ?>
519 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">
520 - <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'); ?>
521 519 </button>
522 520 </div>
523 521 </div>
524 522 <div class="item-content">
525 - <?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. ?>
526 524 </div>
527 525 </div>
528 526 <?php endif; ?>
529 527 </div>
@@ -534,10 +532,10 @@
534 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">
535 533 <i class="fas fa-plus text-indigo-600 text-lg"></i>
536 534 </div>
537 535 <div class="text-left">
538 - <span class="font-semibold text-base"><?php _e('Add Another Item', 'easy-invoice'); ?></span>
539 - <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>
540 538 </div>
541 539 </div>
542 540 </button>
543 541 </div>
@@ -542,13 +540,11 @@
542 540 </button>
543 541 </div>
544 542
545 543 <?php elseif ($tab['id'] === 'client-tab'): ?>
546 - <!-- Client Tab Content -->
547 544 <div class="mb-6">
548 - <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>
549 546
550 - <!-- Client Search and Selection -->
551 547 <div class="relative">
552 548 <div class="flex items-center space-x-3">
553 549 <div class="relative flex-grow">
554 550 <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -557,34 +553,31 @@
557 553 <input
558 554 type="text"
559 555 id="client-search-input"
560 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"
561 - 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')); ?>"
562 558 autocomplete="off"
563 559 >
564 560 </div>
565 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">
566 562 <i class="fas fa-plus -ml-1 mr-2"></i>
567 - <?php _e('New Client', 'easy-invoice'); ?>
563 + <?php esc_html_e('New Client', 'easy-invoice'); ?>
568 564 </button>
569 565 </div>
570 566
571 - <!-- Search Results Dropdown -->
572 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">
573 568 <div id="client-search-loading" class="hidden p-6 text-center text-gray-500">
574 569 <i class="fas fa-spinner fa-spin mr-2"></i>
575 - <?php _e('Searching...', 'easy-invoice'); ?>
570 + <?php esc_html_e('Searching...', 'easy-invoice'); ?>
576 571 </div>
577 572 <div id="client-search-empty" class="hidden p-6 text-center text-gray-500">
578 573 <i class="fas fa-search mr-2"></i>
579 - <?php _e('No clients found', 'easy-invoice'); ?>
574 + <?php esc_html_e('No clients found', 'easy-invoice'); ?>
580 575 </div>
581 576 <div id="client-search-list" class="py-2">
582 - <!-- Client options will be populated here -->
583 577 </div>
584 578 </div>
585 579
586 - <!-- Selected Client Display -->
587 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'; ?>">
588 581 <div class="flex items-center justify-between">
589 582 <div class="flex items-center">
590 583 <div class="flex-shrink-0">
@@ -629,12 +622,16 @@
629 622 </div>
630 623 </div>
631 624 </div>
632 625
633 - <!-- Hidden select for form submission -->
634 626 <select id="select-client" class="hidden">
635 - <option value=""><?php _e('-- Select a client --', 'easy-invoice'); ?></option>
627 + <option value=""><?php esc_html_e('-- Select a client --', 'easy-invoice'); ?></option>
636 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 + }
637 634 // Output client options
638 635 if (!empty($clients)) {
639 636 foreach ($clients as $client) {
640 637 // Get the WordPress user data directly
@@ -658,9 +655,9 @@
658 655 $selected = $quote && $quote->getClientId() == $client->getId() ? 'selected' : '';
659 656 printf(
660 657 '<option value="%s" %s data-name="%s" data-email="%s">%s (%s)</option>',
661 658 esc_attr($client->getId()),
662 - $selected,
659 + $selected, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output of selected( ..., false ).
663 660 esc_attr($client_name),
664 661 esc_attr($client_email),
665 662 esc_html($client_name),
666 663 esc_html($client_email)
@@ -673,31 +670,29 @@
673 670
674 671 <div class="border-t border-gray-200 pt-4 mt-4">
675 672 <input type="hidden" name="client_id" id="client-id" value="<?php echo $quote ? esc_attr($quote->getClientId()) : ''; ?>">
676 673
677 - <!-- Client Information Display -->
678 674 <div id="client-info-display" class="mt-6 <?php echo $quote && $quote->getClientId() ? '' : 'hidden'; ?>">
679 675 <div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
680 676 <div class="flex items-center justify-between pb-4 border-b border-gray-200 mb-4">
681 677 <h4 class="text-lg font-semibold text-gray-800 flex items-center">
682 678 <i class="fas fa-user-check text-indigo-500 mr-3"></i>
683 - <span><?php _e('Client Information', 'easy-invoice'); ?></span>
679 + <span><?php esc_html_e('Client Information', 'easy-invoice'); ?></span>
684 680 </h4>
685 - <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() : ''))); ?>"
686 682 id="edit-selected-client"
687 - 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'; ?>"
688 684 target="_blank">
689 685 <i class="fas fa-pen mr-1"></i>
690 - <?php _e('Edit', 'easy-invoice'); ?>
686 + <?php esc_html_e('Edit', 'easy-invoice'); ?>
691 687 </a>
692 688 </div>
693 689
694 690 <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 pt-2">
695 - <!-- Client Name -->
696 691 <div class="flex items-start group">
697 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>
698 693 <div class="ml-3">
699 - <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>
700 695 <p id="display-client-name" class="text-sm font-semibold text-gray-800 mt-1">
701 696 <?php
702 697 if ($quote && $quote->getClientId()) {
703 698 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -713,13 +708,12 @@
713 708 </p>
714 709 </div>
715 710 </div>
716 711
717 - <!-- Company Name -->
718 712 <div class="flex items-start group">
719 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>
720 714 <div class="ml-3">
721 - <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>
722 716 <p id="display-client-company" class="text-sm font-semibold text-gray-800 mt-1">
723 717 <?php
724 718 if ($quote && $quote->getClientId()) {
725 719 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -735,13 +729,12 @@
735 729 </p>
736 730 </div>
737 731 </div>
738 732
739 - <!-- Email -->
740 733 <div class="flex items-start group">
741 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>
742 735 <div class="ml-3">
743 - <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>
744 737 <p id="display-client-email" class="text-sm text-gray-800 mt-1 break-all">
745 738 <?php
746 739 if ($quote && $quote->getClientId()) {
747 740 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -757,13 +750,12 @@
757 750 </p>
758 751 </div>
759 752 </div>
760 753
761 - <!-- Phone -->
762 754 <div class="flex items-start group">
763 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>
764 756 <div class="ml-3">
765 - <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>
766 758 <p id="display-client-phone" class="text-sm text-gray-800 mt-1">
767 759 <?php
768 760 if ($quote && $quote->getClientId()) {
769 761 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -779,13 +771,12 @@
779 771 </p>
780 772 </div>
781 773 </div>
782 774
783 - <!-- Website -->
784 775 <div class="flex items-start group md:col-span-2">
785 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>
786 777 <div class="ml-3">
787 - <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>
788 779 <p id="display-client-website" class="text-sm text-gray-800 mt-1">
789 780 <?php
790 781 if ($quote && $quote->getClientId()) {
791 782 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -801,13 +792,12 @@
801 792 </p>
802 793 </div>
803 794 </div>
804 795
805 - <!-- Address -->
806 796 <div class="flex items-start group md:col-span-2">
807 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>
808 798 <div class="ml-3">
809 - <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>
810 800 <p id="display-client-address" class="text-sm text-gray-800 mt-1 whitespace-pre-wrap">
811 801 <?php
812 802 if ($quote && $quote->getClientId()) {
813 803 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($quote->getClientId());
@@ -826,18 +816,16 @@
826 816 </div>
827 817 </div>
828 818 </div>
829 819
830 - <!-- No Client Selected Message -->
831 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' : ''; ?>">
832 - <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>
833 822 </div>
834 823 </div>
835 824
836 825 <?php elseif ($tab['id'] === 'discounts-taxes-tab'): ?>
837 - <!-- Discount & Tax Tab Content -->
838 826 <div>
839 - <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>
840 828
841 829 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
842 830 <?php
843 831 // Get all discount and tax fields
@@ -851,9 +839,9 @@
851 839 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
852 840 });
853 841
854 842 foreach ($all_fields as $field):
855 - 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.
856 844 endforeach;
857 845 ?>
858 846 </div>
859 847 </div>
@@ -858,28 +846,26 @@
858 846 </div>
859 847 </div>
860 848
861 849 <?php elseif ($tab['id'] === 'payments-tab'): ?>
862 - <!-- Payment Tab Content -->
863 850 <div>
864 - <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>
865 852 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
866 853 <?php
867 854 $payment_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'payment');
868 855 foreach ($payment_fields as $field):
869 - 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.
870 857 endforeach;
871 858 ?>
872 859 </div>
873 860
874 - <!-- Currency Section -->
875 861 <div class="mt-8 border-t border-gray-200 pt-6">
876 - <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>
877 863 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
878 864 <?php
879 865 $currency_fields = $quote_form_manager->getFieldsBySection('payments-tab', 'currency');
880 866 foreach ($currency_fields as $field):
881 - 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.
882 868 endforeach;
883 869 ?>
884 870 </div>
885 871 </div>
@@ -889,15 +875,14 @@
889 875 </div>
890 876 <?php endforeach; ?>
891 877 </div>
892 878
893 -<!-- Template Gallery Modal -->
894 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">
895 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">
896 881 <div class="flex justify-between items-center mb-4">
897 - <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>
898 883 <button type="button" class="close-gallery-modal text-gray-400 hover:text-gray-500 focus:outline-none">
899 - <span class="sr-only"><?php _e('Close', 'easy-invoice'); ?></span>
884 + <span class="sr-only"><?php esc_html_e('Close', 'easy-invoice'); ?></span>
900 885 <i class="fas fa-times text-xl"></i>
901 886 </button>
902 887 </div>
903 888
@@ -907,9 +892,9 @@
907 892 foreach ($all_templates as $template_id => $template) :
908 893 $is_current = ($current_template === $template_id);
909 894 $selected_class = $is_current ? 'selected' : '';
910 895 ?>
911 - <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'; ?>">
912 897 <div class="p-4 border rounded-lg">
913 898 <div class="flex items-center mb-2">
914 899 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
915 900 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -925,18 +910,17 @@
925 910 </div>
926 911
927 912 <div class="flex justify-end mt-4 space-x-3">
928 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">
929 - <?php _e('Apply', 'easy-invoice'); ?>
914 + <?php esc_html_e('Apply', 'easy-invoice'); ?>
930 915 </button>
931 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">
932 - <?php _e('Close', 'easy-invoice'); ?>
917 + <?php esc_html_e('Close', 'easy-invoice'); ?>
933 918 </button>
934 919 </div>
935 920 </div>
936 921 </div>
937 922
938 -<!-- Hidden Template for Quote Items -->
939 923 <template id="quote-item-template" style="display: none;">
940 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;">
941 925 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
942 926 <div class="flex items-center">
@@ -943,9 +927,9 @@
943 927 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
944 928 <i class="fas fa-chevron-down text-sm"></i>
945 929 </button>
946 930 <h3 class="text-base font-normal text-gray-800 flex-1">
947 - <?php _e('New Item', 'easy-invoice'); ?>
931 + <?php esc_html_e('New Item', 'easy-invoice'); ?>
948 932 </h3>
949 933 </div>
950 934 <div class="flex items-center gap-3">
951 935 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
@@ -951,9 +935,9 @@
951 935 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
952 936 <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
953 937 = $<span class="total-summary">0.00</span>
954 938 </div>
955 - <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')); ?>">
956 940 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
957 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"/>
958 942 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
959 943 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -959,15 +943,15 @@
959 943 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
960 944 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
961 945 </svg>
962 946 </button>
963 - <?php if (class_exists('EasyInvoicePro\\Services\\ItemLibraryService')): ?>
964 - <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'); ?>">
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')); ?>">
965 949 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
966 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"/>
967 951 </svg>
968 952 </button>
969 - <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')); ?>">
970 954 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
971 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"/>
972 956 </svg>
973 957 </button>
@@ -972,14 +956,14 @@
972 956 </svg>
973 957 </button>
974 958 <?php endif; ?>
975 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">
976 - <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'); ?>
977 961 </button>
978 962 </div>
979 963 </div>
980 964 <div class="item-content">
981 - <?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. ?>
982 966 </div>
983 967 </div>
984 968 </template>
985 969
@@ -1037,11 +1021,11 @@
1037 1021
1038 1022 if (isFreeVersion) {
1039 1023 // Show premium upgrade modal for free users
1040 1024 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1041 - 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')); ?>');
1042 1026 } else {
1043 - 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')); ?>');
1044 1028 }
1045 1029 } else {
1046 1030 // Pro users can browse all templates
1047 1031 $('#template-gallery-modal').show();
@@ -1068,11 +1052,11 @@
1068 1052
1069 1053 if (isPremium && isFreeVersion) {
1070 1054 // Show premium upgrade modal instead of selecting the template
1071 1055 if (typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) {
1072 - 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')); ?>');
1073 1057 } else {
1074 - 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')); ?>');
1075 1059 }
1076 1060 return;
1077 1061 }
1078 1062
@@ -1226,9 +1210,9 @@
1226 1210 });
1227 1211
1228 1212 if (allCollapsed) {
1229 1213 // If all items are collapsed, expand them
1230 - $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'); ?>');
1231 1215 $('.quote-item').each(function() {
1232 1216 const $item = $(this);
1233 1217 const itemContent = $item.find('.item-content');
1234 1218 const summaryElement = $item.find('.item-collapsed-summary');
@@ -1246,9 +1230,9 @@
1246 1230 }
1247 1231 });
1248 1232 } else {
1249 1233 // If any items are expanded, collapse them all
1250 - $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'); ?>');
1251 1235 $('.quote-item').each(function() {
1252 1236 const $item = $(this);
1253 1237 const itemContent = $item.find('.item-content');
1254 1238 const summaryElement = $item.find('.item-collapsed-summary');
@@ -1278,9 +1262,10 @@
1278 1262 });
1279 1263
1280 1264 // Client search functionality
1281 1265 let searchTimeout;
1282 - let isSearching = false;
1266 + let isSearching = false; // the in-flight jqXHR, or false
1267 + let searchSeq = 0;
1283 1268 $('#client-search-input').on('input', function() {
1284 1269 const query = $(this).val().trim();
1285 1270 clearTimeout(searchTimeout);
1286 1271
@@ -1512,9 +1497,10 @@
1512 1497 // Update the collapsed summary of a quote item
1513 1498 function updateQuoteItemSummary(itemContainer) {
1514 1499 const quantity = parseFloat(itemContainer.find('input[name*="[quantity]"]').val()) || 0;
1515 1500 const price = parseFloat(itemContainer.find('input[name*="[price]"]').val()) || 0;
1516 - 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');
1517 1503 itemContainer.find('.quantity-summary').text(quantity);
1518 1504 itemContainer.find('.price-summary').text(price.toFixed(2));
1519 1505 itemContainer.find('.total-summary').text(total.toFixed(2));
1520 1506 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
@@ -1520,22 +1506,20 @@
1520 1506 itemContainer.find('input[name*="[total]"]').val(total.toFixed(2));
1521 1507 }
1522 1508
1523 1509 function searchClients(query) {
1524 -
1525 -
1526 - // Prevent multiple simultaneous requests
1527 - if (isSearching) {
1528 - return;
1529 - }
1530 -
1531 - // 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.
1532 1514 if (searchTimeout) {
1533 1515 clearTimeout(searchTimeout);
1534 1516 }
1517 + if (isSearching && isSearching.abort) {
1518 + isSearching.abort();
1519 + }
1520 + const requestSeq = ++searchSeq;
1535 1521
1536 - isSearching = true;
1537 -
1538 1522 $('#client-search-loading').show().removeClass('hidden');
1539 1523 $('#client-search-empty').hide().addClass('hidden');
1540 1524 $('#client-search-list').empty();
1541 1525
@@ -1545,9 +1529,9 @@
1545 1529 }
1546 1530
1547 1531
1548 1532
1549 - $.ajax({
1533 + isSearching = $.ajax({
1550 1534 url: easyInvoice.ajaxUrl,
1551 1535 type: 'POST',
1552 1536 data: {
1553 1537 action: 'easy_invoice_search_clients',
@@ -1555,9 +1539,11 @@
1555 1539 nonce: easyInvoice.nonce
1556 1540 },
1557 1541
1558 1542 success: function(response) {
1559 -
1543 + if (requestSeq !== searchSeq) {
1544 + return; // a newer search has replaced this one
1545 + }
1560 1546 isSearching = false;
1561 1547 $('#client-search-loading').hide().addClass('hidden');
1562 1548
1563 1549 if (response.success && response.data.length > 0) {
@@ -1562,21 +1548,22 @@
1562 1548
1563 1549 if (response.success && response.data.length > 0) {
1564 1550
1565 1551 response.data.forEach(function(client) {
1566 - const option = $(`
1567 - <div class="client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0"
1568 - data-client-id="${client.id}"
1569 - data-client-name="${(client.name || '').replace(/"/g, '&quot;')}"
1570 - data-client-email="${(client.email || '').replace(/"/g, '&quot;')}"
1571 - data-client-company="${(client.company || '').replace(/"/g, '&quot;')}"
1572 - data-client-phone="${(client.phone || '').replace(/"/g, '&quot;')}"
1573 - data-client-address="${(client.address || '').replace(/"/g, '&quot;')}"
1574 - onclick="selectClientFromOption(this)">
1575 - <div class="font-medium text-gray-900">${client.name || 'No Name'}</div>
1576 - <div class="text-sm text-gray-600">${client.email || 'No Email'}</div>
1577 - </div>
1578 - `);
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 || ''));
1579 1566 $('#client-search-list').append(option);
1580 1567 });
1581 1568 $('#client-search-results').show().removeClass('hidden');
1582 1569
@@ -1585,8 +1572,11 @@
1585 1572 $('#client-search-results').show().removeClass('hidden');
1586 1573 }
1587 1574 },
1588 1575 error: function(xhr, status, error) {
1576 + if (status === 'abort' || requestSeq !== searchSeq) {
1577 + return; // superseded by a newer search
1578 + }
1589 1579 console.error('Quote form: AJAX error:', error);
1590 1580 console.error('Quote form: Status:', status);
1591 1581 console.error('Quote form: Response:', xhr.responseText);
1592 1582 isSearching = false;
@@ -1607,9 +1597,10 @@
1607 1597 $('#client-info-display').show().removeClass('hidden');
1608 1598 $('#no-client-message').hide().addClass('hidden');
1609 1599
1610 1600 // Update the edit client button URL
1611 - $('#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);
1612 1603
1613 1604 // If we have full client data, populate all fields directly
1614 1605 if (clientData) {
1615 1606 $('#display-client-name').text(clientData.name || clientName || '-');
@@ -1641,37 +1632,80 @@
1641 1632 }
1642 1633
1643 1634 function calculateTotals() {
1644 1635 let subtotal = 0;
1636 + let taxableSubtotal = 0;
1645 1637 $('.quote-item').each(function() {
1646 1638 const quantity = parseFloat($(this).find('input[name*="[quantity]"]').val()) || 0;
1647 1639 const price = parseFloat($(this).find('input[name*="[price]"]').val()) || 0;
1648 - 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);
1649 1648 $(this).find('.total-summary').text(total.toFixed(2));
1650 1649 $(this).find('input[name*="[total]"]').val(total.toFixed(2));
1651 1650 subtotal += total;
1651 + if (isTaxable) {
1652 + taxableSubtotal += total;
1653 + }
1652 1654 });
1653 1655 $('input[name="subtotal"]').val(subtotal.toFixed(2));
1654 - calculateTaxAndDiscount();
1656 + calculateTaxAndDiscount(subtotal, taxableSubtotal);
1655 1657 }
1656 1658
1657 - function calculateTaxAndDiscount() {
1658 - 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 +
1659 1663 const discountValue = parseFloat($('input[name="discount_value"]').val()) || 0;
1660 1664 const discountType = $('select[name="discount_type"]').val();
1665 + const calculationMethod = $('select[name="discount_calculation_method"]').val() || 'before_tax';
1661 1666 const taxRate = parseFloat($('input[name="tax_rate"]').val()) || 0;
1662 -
1667 + const pricesIncludeTax = $('select[name="prices_include_tax"]').val() === 'yes';
1668 +
1669 + // Calculate discount
1663 1670 let discountAmount = 0;
1664 1671 if (discountType === 'percentage') {
1665 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;
1666 1704 } else {
1667 - discountAmount = discountValue;
1705 + total = afterDiscountAmount;
1668 1706 }
1669 -
1670 - const taxableAmount = subtotal - discountAmount;
1671 - const taxAmount = taxableAmount * (taxRate / 100);
1672 - const total = taxableAmount + taxAmount;
1673 -
1707 +
1674 1708 $('input[name="discount_amount"]').val(discountAmount.toFixed(2));
1675 1709 $('input[name="tax_amount"]').val(taxAmount.toFixed(2));
1676 1710 $('input[name="total"]').val(total.toFixed(2));
1677 1711 }
@@ -1707,18 +1741,22 @@
1707 1741 // Client initialized successfully
1708 1742 }
1709 1743
1710 1744 // Bind calculation events
1711 - $(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() {
1712 1746 calculateTotals();
1713 1747 });
1748 +
1749 + $(document).on('change', 'input[name*="[taxable]"]', function() {
1750 + calculateTotals();
1751 + });
1714 1752
1715 1753 $(document).on('input', 'input[name="discount_value"], input[name="tax_rate"]', function() {
1716 - calculateTaxAndDiscount();
1754 + calculateTotals();
1717 1755 });
1718 1756
1719 - $(document).on('change', 'select[name="discount_type"]', function() {
1720 - calculateTaxAndDiscount();
1757 + $(document).on('change', 'select[name="discount_type"], select[name="discount_calculation_method"], select[name="prices_include_tax"]', function() {
1758 + calculateTotals();
1721 1759 });
1722 1760
1723 1761 // Click on search input to show all clients
1724 1762 $("#client-search-input").on("click", function() {
@@ -1758,9 +1796,8 @@
1758 1796 // Quote field configuration and pre-loaded data
1759 1797 </script>
1760 1798
1761 1799 <?php if (wp_doing_ajax()): ?>
1762 - <!-- Save Button Section for Modal -->
1763 1800 <div class="mt-8 pt-6 border-t border-gray-200">
1764 1801 <div class="flex justify-end space-x-3">
1765 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">
1766 1803 Cancel
@@ -1766,10 +1803,10 @@
1766 1803 Cancel
1767 1804 </button>
1768 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">
1769 1806 <i class="fas fa-save mr-2"></i>
1770 - <span>Save Quote</span>
1807 + <span><?php esc_html_e('Save Quote', 'easy-invoice'); ?></span>
1771 1808 </button>
1772 1809 </div>
1773 1810 </div>
1774 1811 </form>
1775 1812 <?php endif; ?>