| @@ -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, '"')}" | |
| 1570 | - data-client-email="${(client.email || '').replace(/"/g, '"')}" | |
| 1571 | - data-client-company="${(client.company || '').replace(/"/g, '"')}" | |
| 1572 | - data-client-phone="${(client.phone || '').replace(/"/g, '"')}" | |
| 1573 | - data-client-address="${(client.address || '').replace(/"/g, '"')}" | |
| 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; ?> |