PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.4
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.4
2.4.3 2.4.4 2.4.2 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 All 60 releases
← All changes | templates/invoices/form.php +325 -273 2.1.1 → 2.4.4 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +if ( ! defined( 'ABSPATH' ) ) {
3 + exit;
4 +}
2 5 // Enqueue CSS and JS files
3 6 wp_enqueue_style('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/css/invoice-form.css', array(), '1.0.0');
4 7 wp_enqueue_script('easy-invoice-form', plugin_dir_url(__FILE__) . '../../assets/js/invoice-form.js', array('jquery'), '1.0.0', true);
5 8
@@ -15,9 +18,9 @@
15 18 $tabs = $form_manager->getTabs();
16 19 $templates = $form_manager->getTemplates();
17 20
18 21 // Create a separate array for browse templates modal with all available templates
19 -$all_templates = [
22 +$all_templates = apply_filters('easy_invoice_invoice_templates', [
20 23 'standard' => [
21 24 'name' => __('Standard', 'easy-invoice'),
22 25 'icon' => 'fas fa-file-invoice',
23 26 'description' => __('Clean and professional standard template', 'easy-invoice'),
@@ -80,9 +83,9 @@
80 83 'description' => __('Creative and artistic design template', 'easy-invoice'),
81 84 'order' => 9,
82 85 'is_free' => false
83 86 ]
84 -];
87 +]);
85 88
86 89
87 90
88 91 // Load client data directly in PHP if invoice has a client
@@ -166,14 +169,14 @@
166 169 }
167 170 </style>
168 171
169 172 <div class="bg-white shadow rounded-lg">
170 -
173 +
171 174 <div class="border-b border-gray-200">
172 - <nav class="flex -mb-px">
175 + <nav class="flex -mb-px" aria-label="<?php esc_attr_e( 'Invoice form sections', 'easy-invoice' ); ?>">
173 176 <?php foreach ($tabs as $tab): ?>
174 - <button type="button"
175 - 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"
177 + <button type="button"
178 + 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"
176 179 data-tab="<?php echo esc_attr($tab['id']); ?>">
177 180 <i class="<?php echo esc_attr($tab['icon']); ?> mb-1 text-lg"></i>
178 181 <span><?php echo esc_html($tab['label']); ?></span>
179 182 </button>
@@ -179,46 +182,83 @@
179 182 </button>
180 183 <?php endforeach; ?>
181 184 </nav>
182 185 </div>
183 - <form id="invoice-form">
184 - <!-- Hidden invoice ID field for edit mode -->
186 + <?php
187 + // Lifecycle-stage lock check. Addons (PartialPayments)
188 + // can return false here to mark this invoice as
189 + // immutable — currently used to freeze paid deposit
190 + // invoices so admins don't accidentally invalidate
191 + // the linked balance pair. Default: true (no listener).
192 + $ei_can_edit = ! $invoice_id || apply_filters('easy_invoice_can_edit_invoice', true, (int) $invoice_id);
193 + if (! $ei_can_edit && $invoice_id):
194 + $ei_balance_id = (int) get_post_meta((int) $invoice_id, '_balance_invoice_id', true);
195 + $ei_paid_date = (string) get_post_meta((int) $invoice_id, '_easy_invoice_payment_date', true);
196 + ?>
197 + <div class="mx-6 mt-4 mb-4 rounded-md border border-amber-200 bg-amber-50 p-4">
198 + <div class="flex items-start">
199 + <i class="fas fa-lock text-amber-500 mt-0.5 mr-3"></i>
200 + <div class="flex-1 text-sm text-amber-800">
201 + <p class="font-semibold mb-1"><?php esc_html_e('This deposit invoice is locked.', 'easy-invoice'); ?></p>
202 + <p class="mb-2">
203 + <?php
204 + if ($ei_paid_date) {
205 + printf(
206 + /* translators: %s: payment date */
207 + esc_html__('A payment was recorded on %s. Items and totals are frozen so the deposit receipt the customer already saw stays accurate. To add new charges, open the linked balance invoice instead.', 'easy-invoice'),
208 + esc_html($ei_paid_date)
209 + );
210 + } else {
211 + esc_html_e('A payment has been recorded against this deposit. Items and totals are frozen so the deposit receipt the customer already saw stays accurate. To add new charges, open the linked balance invoice instead.', 'easy-invoice');
212 + }
213 + ?>
214 + </p>
215 + <?php if ($ei_balance_id > 0): ?>
216 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-builder&invoice_id=' . $ei_balance_id)); ?>"
217 + class="inline-flex items-center px-3 py-1.5 rounded text-xs font-medium bg-amber-600 text-white hover:bg-amber-700">
218 + <i class="fas fa-arrow-right mr-1"></i>
219 + <?php esc_html_e('Open the balance invoice', 'easy-invoice'); ?>
220 + </a>
221 + <?php endif; ?>
222 + </div>
223 + </div>
224 + </div>
225 + <?php endif; ?>
226 + <form id="invoice-form"<?php echo $ei_can_edit ? '' : ' data-ei-locked="1"'; ?>>
185 227 <input type="hidden" id="invoice-id" name="invoice_id" value="<?php echo $invoice_id ? esc_attr($invoice_id) : ''; ?>">
186 -
187 - <!-- Security nonce -->
188 - <input type="hidden" id="invoice_nonce" name="invoice_nonce" value="<?php echo wp_create_nonce('easy_invoice_nonce'); ?>">
189 -
228 +
229 + <input type="hidden" id="invoice_nonce" name="invoice_nonce" value="<?php echo esc_attr(wp_create_nonce('easy_invoice_nonce')); ?>">
230 +
190 231 <?php foreach ($tabs as $tab): ?>
191 232 <div class="tab-content <?php echo $tab['active'] ? 'active' : 'hidden'; ?> px-6 py-5" id="<?php echo esc_attr($tab['id']); ?>">
192 233 <?php if ($tab['id'] === 'invoice-tab'): ?>
193 - <!-- Template Selection Section -->
194 234 <div class="mb-8 pb-6 border-b border-gray-200">
195 235 <div class="flex justify-between items-center mb-4">
196 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Invoice Template', 'easy-invoice'); ?></h3>
236 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Invoice Template', 'easy-invoice'); ?></h3>
197 237 <button type="button" id="browse-all-templates" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
198 - <span class="mr-1"><?php _e('Browse Invoice Templates', 'easy-invoice'); ?></span>
238 + <span class="mr-1"><?php esc_html_e('Browse Invoice Templates', 'easy-invoice'); ?></span>
199 239 <i class="fas fa-chevron-right"></i>
200 240 </button>
201 241 </div>
202 -
242 +
203 243 <?php
204 244 // Get the current template (default to 'standard')
205 245 $current_template = $invoice ? $invoice->getTemplate() : 'standard';
206 -
246 +
207 247 if (empty($current_template)) {
208 248 $current_template = 'standard';
209 249 }
210 -
250 +
211 251 // Always include the hidden input for the template field
212 252 ?>
213 253 <input type="hidden" name="invoice_template" value="<?php echo esc_attr($current_template); ?>">
214 254 <?php
215 -
255 +
216 256 // Check if this is the free version
217 257 $is_free_version = !easy_invoice_has_pro();
218 -
219 258
220 -
259 +
260 +
221 261 // Show all templates grid
222 262 ?>
223 263 <div class="grid grid-cols-2 md:grid-cols-3 gap-4" id="main-templates-grid">
224 264 <?php
@@ -223,9 +263,9 @@
223 263 <div class="grid grid-cols-2 md:grid-cols-3 gap-4" id="main-templates-grid">
224 264 <?php
225 265 // Build the main grid templates array - always include current template plus 2 others
226 266 $main_grid_templates = [];
227 -
267 +
228 268 // First, add the current template if it exists in all_templates
229 269 if (isset($all_templates[$current_template])) {
230 270 // For free users, if current template is premium, default to standard
231 271 if ($is_free_version && isset($all_templates[$current_template]['is_free']) && !$all_templates[$current_template]['is_free']) {
@@ -234,9 +274,9 @@
234 274 } else {
235 275 $main_grid_templates[$current_template] = $all_templates[$current_template];
236 276 }
237 277 }
238 -
278 +
239 279 // Add other templates to fill up to 3, prioritizing standard templates
240 280 $default_templates = ['standard', 'legacy', 'professional'];
241 281 foreach ($default_templates as $template_id) {
242 282 if (count($main_grid_templates) >= 3) break;
@@ -243,9 +283,9 @@
243 283 if ($template_id !== $current_template && isset($all_templates[$template_id])) {
244 284 $main_grid_templates[$template_id] = $all_templates[$template_id];
245 285 }
246 286 }
247 -
287 +
248 288 // If we still don't have 3 templates, add any remaining templates
249 289 foreach ($all_templates as $template_id => $template) {
250 290 if (count($main_grid_templates) >= 3) break;
251 291 if (!isset($main_grid_templates[$template_id])) {
@@ -251,17 +291,17 @@
251 291 if (!isset($main_grid_templates[$template_id])) {
252 292 $main_grid_templates[$template_id] = $template;
253 293 }
254 294 }
255 -
295 +
256 296 // Display the main grid templates
257 297 foreach ($main_grid_templates as $template_id => $template) :
258 298 $checked = ($current_template === $template_id) ? 'checked' : '';
259 299 $selected_class = ($current_template === $template_id) ? 'selected' : '';
260 -
300 +
261 301 // Check if this template is premium (not free)
262 302 $is_premium = isset($template['is_free']) && !$template['is_free'];
263 -
303 +
264 304 // Add premium overlay classes
265 305 $premium_classes = '';
266 306 if ($is_free_version && $is_premium) {
267 307 $premium_classes = 'premium-template-blurred';
@@ -266,10 +306,12 @@
266 306 if ($is_free_version && $is_premium) {
267 307 $premium_classes = 'premium-template-blurred';
268 308 }
269 309 ?>
270 - <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'; ?>">
271 - <input type="radio" id="template-<?php echo esc_attr($template_id); ?>" name="invoice_template" value="<?php echo esc_attr($template_id); ?>" class="template-radio" <?php echo $checked; ?> <?php echo ($is_free_version && $is_premium) ? 'disabled' : ''; ?>>
310 + <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'); ?>">
311 + <?php // aria-label: the visible template name sits in sibling markup, not in a
312 + // <label for>, so assistive tech announced these radios as unnamed. ?>
313 + <input type="radio" id="template-<?php /* translators: %s: template name. */ echo esc_attr($template_id); ?>" name="invoice_template" value="<?php echo esc_attr($template_id); ?>" aria-label="<?php echo esc_attr(sprintf(__('Use the %s invoice template', 'easy-invoice'), $template['name'])); ?>" class="template-radio" <?php echo esc_html($checked); ?> <?php echo esc_attr(($is_free_version && $is_premium) ? 'disabled' : ''); ?>>
272 314 <div class="p-4 border rounded-lg">
273 315 <div class="flex items-center mb-2">
274 316 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
275 317 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -283,24 +325,22 @@
283 325 </div>
284 326 <?php endforeach; ?>
285 327 </div>
286 328 </div>
287 -
288 - <!-- Dynamic Fields Section -->
329 +
289 330 <div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
290 - <?php
331 + <?php
291 332 $main_fields = $form_manager->getFields('invoice-tab');
292 - foreach ($main_fields as $field):
333 + foreach ($main_fields as $field):
293 334 // Skip fields that have a section (like notes) - they're rendered separately
294 335 // Also skip the template field as it's rendered manually in the template selection section
295 336 if (!isset($field['section']) && $field['name'] !== 'invoice_template') {
296 - echo $form_manager->renderField($field, $invoice);
337 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
297 338 }
298 - endforeach;
339 + endforeach;
299 340 ?>
300 341 </div>
301 -
302 - <!-- Auto-generation notice for new invoices -->
342 +
303 343 <?php if (!$invoice): ?>
304 344 <div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
305 345 <div class="flex">
306 346 <div class="flex-shrink-0">
@@ -307,34 +347,32 @@
307 347 <i class="fas fa-info-circle text-blue-400"></i>
308 348 </div>
309 349 <div class="ml-3">
310 350 <p class="text-sm text-blue-700">
311 - <?php _e('Invoice number will be automatically generated when you save the invoice.', 'easy-invoice'); ?>
351 + <?php esc_html_e('Invoice number will be automatically generated when you save the invoice.', 'easy-invoice'); ?>
312 352 </p>
313 353 </div>
314 354 </div>
315 355 </div>
316 356 <?php endif; ?>
317 -
318 - <!-- Recurring Invoice Section -->
357 +
319 358 <?php if (easy_invoice_has_pro()): ?>
320 - <?php
359 + <?php
321 360 $recurring_fields = $form_manager->getFieldsBySection('invoice-tab', 'recurring');
322 - if (!empty($recurring_fields)):
361 + if (!empty($recurring_fields)):
323 362 ?>
324 363 <div class="mt-8 border-t border-gray-200 pt-6">
325 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Recurring Invoice Settings', 'easy-invoice-pro'); ?></h3>
364 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Recurring Invoice Settings', 'easy-invoice'); ?></h3>
326 365 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:col-span-6">
327 - <?php
328 - foreach ($recurring_fields as $field):
329 - echo $form_manager->renderField($field, $invoice);
330 - endforeach;
366 + <?php
367 + foreach ($recurring_fields as $field):
368 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
369 + endforeach;
331 370 ?>
332 371 </div>
333 372 </div>
334 373 <?php endif; ?>
335 374 <?php else: ?>
336 - <!-- Recurring Invoice Promotion for Free Users -->
337 375 <div class="mt-6 border-t border-gray-200 pt-4">
338 376 <div class="bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-200 rounded-lg p-4">
339 377 <div class="flex items-start">
340 378 <div class="flex-shrink-0">
@@ -342,27 +380,27 @@
342 380 <i class="fas fa-sync-alt text-white text-sm"></i>
343 381 </div>
344 382 </div>
345 383 <div class="ml-3 flex-1">
346 - <h3 class="text-base font-semibold text-gray-900 mb-1"><?php _e('Recurring Invoices', 'easy-invoice'); ?></h3>
347 - <p class="text-sm text-gray-600 mb-3"><?php _e('Automate your billing with recurring invoices. Set up weekly, monthly, or custom intervals to automatically generate and send invoices to your clients.', 'easy-invoice'); ?></p>
384 + <h3 class="text-base font-semibold text-gray-900 mb-1"><?php esc_html_e('Recurring Invoices', 'easy-invoice'); ?></h3>
385 + <p class="text-sm text-gray-600 mb-3"><?php esc_html_e('Automate your billing with recurring invoices. Set up weekly, monthly, or custom intervals to automatically generate and send invoices to your clients.', 'easy-invoice'); ?></p>
348 386 <div class="flex items-center space-x-3 mb-3">
349 387 <div class="flex items-center text-xs text-gray-500">
350 388 <i class="fas fa-check text-green-500 mr-1"></i>
351 - <span><?php _e('Automated billing cycles', 'easy-invoice'); ?></span>
389 + <span><?php esc_html_e('Automated billing cycles', 'easy-invoice'); ?></span>
352 390 </div>
353 391 <div class="flex items-center text-xs text-gray-500">
354 392 <i class="fas fa-check text-green-500 mr-1"></i>
355 - <span><?php _e('Custom intervals', 'easy-invoice'); ?></span>
393 + <span><?php esc_html_e('Custom intervals', 'easy-invoice'); ?></span>
356 394 </div>
357 395 <div class="flex items-center text-xs text-gray-500">
358 396 <i class="fas fa-check text-green-500 mr-1"></i>
359 - <span><?php _e('Auto-send notifications', 'easy-invoice'); ?></span>
397 + <span><?php esc_html_e('Auto-send notifications', 'easy-invoice'); ?></span>
360 398 </div>
361 399 </div>
362 - <button type="button" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" onclick="if(typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) { EasyInvoiceConfirmation.showFeatureUpgrade('<?php _e('Recurring Invoices', 'easy-invoice'); ?>', '<?php _e('Automate your billing with recurring invoices. Set up weekly, monthly, or custom intervals to automatically generate and send invoices to your clients.', 'easy-invoice'); ?>'); } else { alert('<?php _e('This is a premium feature. Please upgrade to Pro to access recurring invoices.', 'easy-invoice'); ?>'); }">
400 + <button type="button" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" onclick="if(typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) { EasyInvoiceConfirmation.showFeatureUpgrade('<?php echo esc_js(__('Recurring Invoices', 'easy-invoice')); ?>', '<?php echo esc_js(__('Automate your billing with recurring invoices. Set up weekly, monthly, or custom intervals to automatically generate and send invoices to your clients.', 'easy-invoice')); ?>'); } else { alert('<?php echo esc_js(__('This is a premium feature. Please upgrade to Pro to access recurring invoices.', 'easy-invoice')); ?>'); }">
363 401 <i class="fas fa-crown mr-1.5"></i>
364 - <?php _e('Upgrade to Pro', 'easy-invoice'); ?>
402 + <?php esc_html_e('Upgrade to Pro', 'easy-invoice'); ?>
365 403 </button>
366 404 </div>
367 405 </div>
368 406 </div>
@@ -367,25 +405,24 @@
367 405 </div>
368 406 </div>
369 407 </div>
370 408 <?php endif; ?>
371 -
372 - <!-- Subscription Invoice Section -->
409 +
373 410 <?php if (easy_invoice_has_pro()): ?>
374 - <?php
411 + <?php
375 412 // Check if subscription is actually enabled
376 413 $subscription_enabled = get_option('easy_invoice_pro_subscription_enabled', '0');
377 414 if ($subscription_enabled):
378 415 $subscription_fields = $form_manager->getFieldsBySection('invoice-tab', 'subscription');
379 - if (!empty($subscription_fields)):
416 + if (!empty($subscription_fields)):
380 417 ?>
381 418 <div class="mt-8 border-t border-gray-200 pt-6">
382 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Subscription Settings', 'easy-invoice-pro'); ?></h3>
419 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Subscription Settings', 'easy-invoice'); ?></h3>
383 420 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:col-span-6">
384 - <?php
385 - foreach ($subscription_fields as $field):
386 - echo $form_manager->renderField($field, $invoice);
387 - endforeach;
421 + <?php
422 + foreach ($subscription_fields as $field):
423 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
424 + endforeach;
388 425 ?>
389 426 </div>
390 427 </div>
391 428 <?php endif; ?>
@@ -390,9 +427,8 @@
390 427 </div>
391 428 <?php endif; ?>
392 429 <?php endif; ?>
393 430 <?php else: ?>
394 - <!-- Subscription Invoice Promotion for Free Users -->
395 431 <div class="mt-6 border-t border-gray-200 pt-4">
396 432 <div class="bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-200 rounded-lg p-4">
397 433 <div class="flex items-start">
398 434 <div class="flex-shrink-0">
@@ -400,27 +436,27 @@
400 436 <i class="fas fa-calendar-alt text-white text-sm"></i>
401 437 </div>
402 438 </div>
403 439 <div class="ml-3 flex-1">
404 - <h3 class="text-base font-semibold text-gray-900 mb-1"><?php _e('Partial Payment', 'easy-invoice'); ?></h3>
405 - <p class="text-sm text-gray-600 mb-3"><?php _e('Accept partial payments from clients with flexible payment schedules, payment tracking, and outstanding balance management.', 'easy-invoice'); ?></p>
440 + <h3 class="text-base font-semibold text-gray-900 mb-1"><?php esc_html_e('Partial Payment', 'easy-invoice'); ?></h3>
441 + <p class="text-sm text-gray-600 mb-3"><?php esc_html_e('Accept partial payments from clients with flexible payment schedules, payment tracking, and outstanding balance management.', 'easy-invoice'); ?></p>
406 442 <div class="flex items-center space-x-3 mb-3">
407 443 <div class="flex items-center text-xs text-gray-500">
408 444 <i class="fas fa-check text-green-500 mr-1"></i>
409 - <span><?php _e('Flexible payment schedules', 'easy-invoice'); ?></span>
445 + <span><?php esc_html_e('Flexible payment schedules', 'easy-invoice'); ?></span>
410 446 </div>
411 447 <div class="flex items-center text-xs text-gray-500">
412 448 <i class="fas fa-check text-green-500 mr-1"></i>
413 - <span><?php _e('Payment tracking', 'easy-invoice'); ?></span>
449 + <span><?php esc_html_e('Payment tracking', 'easy-invoice'); ?></span>
414 450 </div>
415 451 <div class="flex items-center text-xs text-gray-500">
416 452 <i class="fas fa-check text-green-500 mr-1"></i>
417 - <span><?php _e('Balance management', 'easy-invoice'); ?></span>
453 + <span><?php esc_html_e('Balance management', 'easy-invoice'); ?></span>
418 454 </div>
419 455 </div>
420 - <button type="button" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" onclick="if(typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) { EasyInvoiceConfirmation.showFeatureUpgrade('<?php _e('Partial Payment', 'easy-invoice'); ?>', '<?php _e('Accept partial payments from clients with flexible payment schedules, payment tracking, and outstanding balance management.', 'easy-invoice'); ?>'); } else { alert('<?php _e('This is a premium feature. Please upgrade to Pro to access partial payment functionality.', 'easy-invoice'); ?>'); }">
456 + <button type="button" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" onclick="if(typeof EasyInvoiceConfirmation !== 'undefined' && EasyInvoiceConfirmation.showFeatureUpgrade) { EasyInvoiceConfirmation.showFeatureUpgrade('<?php echo esc_js(__('Partial Payment', 'easy-invoice')); ?>', '<?php echo esc_js(__('Accept partial payments from clients with flexible payment schedules, payment tracking, and outstanding balance management.', 'easy-invoice')); ?>'); } else { alert('<?php echo esc_js(__('This is a premium feature. Please upgrade to Pro to access partial payment functionality.', 'easy-invoice')); ?>'); }">
421 457 <i class="fas fa-crown mr-1.5"></i>
422 - <?php _e('Upgrade to Pro', 'easy-invoice'); ?>
458 + <?php esc_html_e('Upgrade to Pro', 'easy-invoice'); ?>
423 459 </button>
424 460 </div>
425 461 </div>
426 462 </div>
@@ -425,46 +461,44 @@
425 461 </div>
426 462 </div>
427 463 </div>
428 464 <?php endif; ?>
429 -
430 - <!-- Notes & Terms Section -->
465 +
431 466 <div class="mt-8 border-t border-gray-200 pt-6">
432 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Notes & Terms', 'easy-invoice'); ?></h3>
467 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Notes & Terms', 'easy-invoice'); ?></h3>
433 468 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:col-span-6">
434 - <?php
469 + <?php
435 470 // Only render notes and terms fields, not all invoice tab fields
436 471 $notes_fields = $form_manager->getFieldsBySection('invoice-tab', 'notes');
437 - foreach ($notes_fields as $field):
438 - echo $form_manager->renderField($field, $invoice);
439 - endforeach;
472 + foreach ($notes_fields as $field):
473 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
474 + endforeach;
440 475 ?>
441 476 </div>
442 477 </div>
443 -
478 +
444 479 <?php elseif ($tab['id'] === 'items-tab'): ?>
445 - <!-- Items Tab Content -->
446 480 <div class="flex justify-between items-center mb-6">
447 - <h2 class="text-lg font-medium text-gray-900"><?php _e('Invoice Items', 'easy-invoice'); ?></h2>
481 + <h2 class="text-lg font-medium text-gray-900"><?php esc_html_e('Invoice Items', 'easy-invoice'); ?></h2>
448 482 <button type="button" id="collapse-all-items" class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center">
449 483 <i class="fas fa-chevron-down mr-1"></i>
450 - <span><?php _e('Collapse All', 'easy-invoice'); ?></span>
484 + <span><?php esc_html_e('Collapse All', 'easy-invoice'); ?></span>
451 485 </button>
452 486 </div>
453 487
454 488 <div class="space-y-4 invoice-items-container">
455 - <?php
489 + <?php
456 490 // If editing an existing invoice, directly output invoice items
457 491 if ($invoice && $invoice->getItems()) :
458 492 $index = 0;
459 493 foreach ($invoice->getItems() as $item) :
460 494 // Generate a unique ID for the item
461 - $item_id = 'item_' . time() . '_' . $index . '_' . rand(1000, 9999);
462 -
495 + $item_id = 'item_' . time() . '_' . $index . '_' . wp_rand(1000, 9999);
496 +
463 497 // Get display title for header using dynamic field access
464 498 $title = $item->getName();
465 -
466 -
499 +
500 +
467 501 if (empty($title) && method_exists($item, 'getData')) {
468 502 $item_data = $item->getData();
469 503 if (is_array($item_data) && isset($item_data['title'])) {
470 504 $title = $item_data['title'];
@@ -469,13 +503,14 @@
469 503 if (is_array($item_data) && isset($item_data['title'])) {
470 504 $title = $item_data['title'];
471 505 }
472 506 }
473 -
507 +
508 + /* translators: %d: title. */
474 509 $display_title = !empty($title) ? $title : sprintf(__('Item #%d', 'easy-invoice'), $index + 1);
475 510 $short_title = strlen($display_title) > 30 ? substr($display_title, 0, 30) . '...' : $display_title;
476 511 ?>
477 - <div class="invoice-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; ?>">
512 + <div class="invoice-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; ?>">
478 513 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
479 514 <div class="flex items-center">
480 515 <button type="button" class="item-collapse-toggle mr-3 text-gray-400 hover:text-indigo-600 focus:outline-none transition-colors">
481 516 <i class="fas fa-chevron-down text-sm"></i>
@@ -481,9 +516,9 @@
481 516 <i class="fas fa-chevron-down text-sm"></i>
482 517 </button>
483 518 <h3 class="text-base font-normal text-gray-800 flex-1">
484 519 <?php
485 - echo !empty($title) ? esc_html($title) : __('New Item', 'easy-invoice');
520 + echo !empty($title) ? esc_html($title) : esc_html__('New Item', 'easy-invoice');
486 521 ?>
487 522 </h3>
488 523 </div>
489 524 <div class="flex items-center gap-3">
@@ -490,9 +525,9 @@
490 525 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
491 526 <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>
492 527 = $<span class="total-summary font-medium"><?php echo esc_html(number_format($item->getAmount(), 2)); ?></span>
493 528 </div>
494 - <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'); ?>">
529 + <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')); ?>">
495 530 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
496 531 <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"/>
497 532 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
498 533 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -498,15 +533,28 @@
498 533 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
499 534 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
500 535 </svg>
501 536 </button>
537 + <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
538 + <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')); ?>">
539 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
540 + <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"/>
541 + </svg>
542 + </button>
543 + <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')); ?>">
544 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
545 + <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"/>
546 + </svg>
547 + </button>
548 + <?php endif; ?>
502 549 <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">
503 - <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
550 + <i class="fas fa-trash-alt mr-1"></i> <?php esc_html_e('Remove', 'easy-invoice'); ?>
504 551 </button>
505 552 </div>
506 553 </div>
507 554 <div class="item-content">
508 - <?php echo $form_manager->generateExistingItemHtml($item, $index); ?>
555 + <?php echo $form_manager->generateExistingItemHtml($item, $index); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ItemFieldRenderer escapes every value it emits.
556 +?>
509 557 </div>
510 558 </div>
511 559 <?php
512 560 $index++;
@@ -539,12 +587,12 @@
539 587 </h3>
540 588 </div>
541 589 <div class="flex items-center gap-3">
542 590 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
543 - <span class="quantity-summary">1</span> × $<span class="price-summary">0.00</span>
591 + <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
544 592 = $<span class="total-summary">0.00</span>
545 593 </div>
546 - <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'); ?>">
594 + <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')); ?>">
547 595 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
548 596 <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"/>
549 597 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
550 598 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -550,20 +598,32 @@
550 598 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
551 599 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
552 600 </svg>
553 601 </button>
602 + <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
603 + <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')); ?>">
604 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
605 + <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"/>
606 + </svg>
607 + </button>
608 + <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')); ?>">
609 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
610 + <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"/>
611 + </svg>
612 + </button>
613 + <?php endif; ?>
554 614 <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">
555 - <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
615 + <i class="fas fa-trash-alt mr-1"></i> <?php esc_html_e('Remove', 'easy-invoice'); ?>
556 616 </button>
557 617 </div>
558 618 </div>
559 619 <div class="item-content">
560 - <?php echo $form_manager->generateItemTemplateHtml(); ?>
620 + <?php echo $form_manager->generateItemTemplateHtml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- form markup built by ItemFieldRenderer, which escapes every value. ?>
561 621 </div>
562 622 </div>
563 623 <?php endif; ?>
564 624 </div>
565 -
625 +
566 626 <div class="mt-6">
567 627 <button type="button" class="add-item-button w-full flex justify-center items-center px-6 py-4 bg-white border border-blue-100 rounded-xl text-indigo-700 hover:bg-blue-50 hover:border-blue-200 hover:shadow-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 group">
568 628 <div class="flex items-center">
569 629 <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">
@@ -569,20 +629,18 @@
569 629 <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">
570 630 <i class="fas fa-plus text-indigo-600 text-lg"></i>
571 631 </div>
572 632 <div class="text-left">
573 - <span class="font-semibold text-base"><?php _e('Add Another Item', 'easy-invoice'); ?></span>
574 - <p class="text-sm text-indigo-600 mt-0.5"><?php _e('Click to add a new line item', 'easy-invoice'); ?></p>
633 + <span class="font-semibold text-base"><?php esc_html_e('Add Another Item', 'easy-invoice'); ?></span>
634 + <p class="text-sm text-indigo-600 mt-0.5"><?php esc_html_e('Click to add a new line item', 'easy-invoice'); ?></p>
575 635 </div>
576 636 </div>
577 637 </button>
578 638 </div>
579 639 <?php elseif ($tab['id'] === 'client-tab'): ?>
580 - <!-- Client Tab Content -->
581 640 <div class="mb-6">
582 - <label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('Select Existing Client', 'easy-invoice'); ?></label>
583 -
584 - <!-- Client Search and Selection -->
641 + <label class="block text-sm font-medium text-gray-700 mb-3"><?php esc_html_e('Select Existing Client', 'easy-invoice'); ?></label>
642 +
585 643 <div class="relative">
586 644 <div class="flex items-center space-x-3">
587 645 <div class="relative flex-grow">
588 646 <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -587,38 +645,35 @@
587 645 <div class="relative flex-grow">
588 646 <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
589 647 <i class="fas fa-search text-gray-400"></i>
590 648 </div>
591 - <input
592 - type="text"
593 - id="client-search-input"
649 + <input
650 + type="text"
651 + id="client-search-input"
594 652 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"
595 - placeholder="<?php _e('Search clients by name, email, or company...', 'easy-invoice'); ?>"
653 + placeholder="<?php echo esc_js(__('Search clients by name, email, or company...', 'easy-invoice')); ?>"
596 654 autocomplete="off"
597 655 >
598 656 </div>
599 657 <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">
600 658 <i class="fas fa-plus -ml-1 mr-2"></i>
601 - <?php _e('New Client', 'easy-invoice'); ?>
659 + <?php esc_html_e('New Client', 'easy-invoice'); ?>
602 660 </button>
603 661 </div>
604 -
605 - <!-- Search Results Dropdown -->
662 +
606 663 <div id="client-search-results" class="absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-xl max-h-80 overflow-y-auto" style="display: none;">
607 664 <div id="client-search-loading" class="p-6 text-center text-gray-500" style="display: none;">
608 665 <i class="fas fa-spinner fa-spin mr-2"></i>
609 - <?php _e('Searching...', 'easy-invoice'); ?>
666 + <?php esc_html_e('Searching...', 'easy-invoice'); ?>
610 667 </div>
611 668 <div id="client-search-empty" class="p-6 text-center text-gray-500" style="display: none;">
612 669 <i class="fas fa-search mr-2"></i>
613 - <?php _e('No clients found', 'easy-invoice'); ?>
670 + <?php esc_html_e('No clients found', 'easy-invoice'); ?>
614 671 </div>
615 672 <div id="client-search-list" class="py-2">
616 - <!-- Client options will be populated here -->
617 673 </div>
618 674 </div>
619 -
620 - <!-- Selected Client Display -->
675 +
621 676 <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 ($invoice && $invoice->getClientId()) ? '' : 'hidden'; ?>">
622 677 <div class="flex items-center justify-between">
623 678 <div class="flex items-center">
624 679 <div class="flex-shrink-0">
@@ -627,9 +682,9 @@
627 682 </div>
628 683 </div>
629 684 <div class="ml-4">
630 685 <p id="selected-client-name" class="text-sm font-semibold text-gray-900">
631 - <?php
686 + <?php
632 687 if ($invoice && $invoice->getClientId()) {
633 688 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
634 689 if ($client) {
635 690 echo esc_html($client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()));
@@ -641,9 +696,9 @@
641 696 }
642 697 ?>
643 698 </p>
644 699 <p id="selected-client-email" class="text-sm text-gray-600">
645 - <?php
700 + <?php
646 701 if ($invoice && $invoice->getClientId()) {
647 702 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
648 703 if ($client) {
649 704 echo esc_html($client->getEmail() ?: esc_html__('—', 'easy-invoice'));
@@ -662,19 +717,22 @@
662 717 </button>
663 718 </div>
664 719 </div>
665 720 </div>
666 -
667 - <!-- Hidden select for form submission -->
721 +
668 722 <select id="select-client" class="hidden">
669 - <option value=""><?php _e('-- Select a client --', 'easy-invoice'); ?></option>
670 - <?php
723 + <option value=""><?php esc_html_e('-- Select a client --', 'easy-invoice'); ?></option>
724 + <?php
671 725 // Ensure $clients is defined
672 - if (!isset($clients)) {
673 - $client_repository = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository();
674 - $clients = $client_repository->all();
726 + if (!isset($clients) || empty($clients)) {
727 + $clients = [];
728 + $ei_selected_client_id = ($invoice && method_exists($invoice, 'getClientId')) ? (int) $invoice->getClientId() : 0;
729 + if ($ei_selected_client_id > 0) {
730 + $ei_selected_client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($ei_selected_client_id);
731 + if ($ei_selected_client) { $clients = [ $ei_selected_client ]; }
732 + }
675 733 }
676 -
734 +
677 735 // Output client options
678 736 if (!empty($clients)) {
679 737 foreach ($clients as $client) {
680 738 // Get the WordPress user data directly
@@ -681,26 +739,26 @@
681 739 $user = get_user_by('id', $client->getId());
682 740 if (!$user) {
683 741 continue;
684 742 }
685 -
743 +
686 744 // Use Client model properties first, fallback to WordPress user fields
687 745 $business_name = $client->business_client_name ?: '';
688 746 $first_name = $client->first_name ?: $user->first_name ?: '';
689 747 $last_name = $client->last_name ?: $user->last_name ?: '';
690 748 $client_email = $client->email ?: $user->user_email ?: '';
691 -
749 +
692 750 // Create display name
693 751 $client_name = $business_name ?: ($first_name . ' ' . $last_name);
694 752 if (empty(trim($client_name))) {
695 753 $client_name = $user->display_name ?: 'User ' . $client->getId();
696 754 }
697 -
755 +
698 756 $selected = $invoice && $invoice->getClientId() == $client->getId() ? 'selected' : '';
699 757 printf(
700 758 '<option value="%s" %s data-name="%s" data-email="%s">%s (%s)</option>',
701 759 esc_attr($client->getId()),
702 - $selected,
760 + $selected, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output of selected( ..., false ).
703 761 esc_attr($client_name),
704 762 esc_attr($client_email),
705 763 esc_html($client_name),
706 764 esc_html($client_email)
@@ -712,34 +770,32 @@
712 770 </div>
713 771
714 772 <div class="border-t border-gray-200 pt-4 mt-4">
715 773 <input type="hidden" name="client_id" id="client-id" value="<?php echo $invoice ? esc_attr($invoice->getClientId()) : ''; ?>">
716 -
717 - <!-- Client Information Display -->
774 +
718 775 <div id="client-info-display" class="mt-6 <?php echo $invoice && $invoice->getClientId() ? '' : 'hidden'; ?>">
719 776 <div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
720 777 <div class="flex items-center justify-between pb-4 border-b border-gray-200 mb-4">
721 778 <h4 class="text-lg font-semibold text-gray-800 flex items-center">
722 779 <i class="fas fa-user-check text-indigo-500 mr-3"></i>
723 - <span><?php _e('Client Information', 'easy-invoice'); ?></span>
780 + <span><?php esc_html_e('Client Information', 'easy-invoice'); ?></span>
724 781 </h4>
725 - <a href="<?php echo admin_url('admin.php?page=easy-invoice-client-edit&client_id=' . ($invoice && $invoice->getClientId() ? $invoice->getClientId() : '')); ?>"
726 - id="edit-selected-client"
727 - class="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
782 + <a href="<?php echo esc_url(admin_url('admin.php?page=easy-invoice-client-edit&client_id=' . ($invoice && $invoice->getClientId() ? $invoice->getClientId() : ''))); ?>"
783 + id="edit-selected-client"
784 + class="text-sm text-indigo-600 hover:text-indigo-800 font-medium<?php echo ($invoice && $invoice->getClientId()) ? '' : ' hidden'; ?>"
728 785 target="_blank">
729 786 <i class="fas fa-pen mr-1"></i>
730 - <?php _e('Edit', 'easy-invoice'); ?>
787 + <?php esc_html_e('Edit', 'easy-invoice'); ?>
731 788 </a>
732 789 </div>
733 -
790 +
734 791 <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 pt-2">
735 - <!-- Client Name -->
736 792 <div class="flex items-start group">
737 793 <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>
738 794 <div class="ml-3">
739 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Name', 'easy-invoice'); ?></label>
795 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Name', 'easy-invoice'); ?></label>
740 796 <p id="display-client-name" class="text-sm font-semibold text-gray-800 mt-1">
741 - <?php
797 + <?php
742 798 if ($invoice && $invoice->getClientId()) {
743 799 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
744 800 if ($client) {
745 801 echo esc_html($client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()));
@@ -752,16 +808,15 @@
752 808 ?>
753 809 </p>
754 810 </div>
755 811 </div>
756 -
757 - <!-- Company Name -->
812 +
758 813 <div class="flex items-start group">
759 814 <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>
760 815 <div class="ml-3">
761 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Company', 'easy-invoice'); ?></label>
816 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Company', 'easy-invoice'); ?></label>
762 817 <p id="display-client-company" class="text-sm font-semibold text-gray-800 mt-1">
763 - <?php
818 + <?php
764 819 if ($invoice && $invoice->getClientId()) {
765 820 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
766 821 if ($client) {
767 822 echo esc_html($client->getBusinessClientName() ?: esc_html__('—', 'easy-invoice'));
@@ -774,16 +829,15 @@
774 829 ?>
775 830 </p>
776 831 </div>
777 832 </div>
778 -
779 - <!-- Email -->
833 +
780 834 <div class="flex items-start group">
781 835 <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>
782 836 <div class="ml-3">
783 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Email', 'easy-invoice'); ?></label>
837 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Email', 'easy-invoice'); ?></label>
784 838 <p id="display-client-email" class="text-sm text-gray-800 mt-1 break-all">
785 - <?php
839 + <?php
786 840 if ($invoice && $invoice->getClientId()) {
787 841 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
788 842 if ($client) {
789 843 echo esc_html($client->getEmail() ?: esc_html__('—', 'easy-invoice'));
@@ -797,15 +851,14 @@
797 851 </p>
798 852 </div>
799 853 </div>
800 854
801 - <!-- Phone -->
802 855 <div class="flex items-start group">
803 856 <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>
804 857 <div class="ml-3">
805 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Phone', 'easy-invoice'); ?></label>
858 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Phone', 'easy-invoice'); ?></label>
806 859 <p id="display-client-phone" class="text-sm text-gray-800 mt-1">
807 - <?php
860 + <?php
808 861 if ($invoice && $invoice->getClientId()) {
809 862 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
810 863 if ($client) {
811 864 echo esc_html($client->getExtraInfo() ?: esc_html__('—', 'easy-invoice'));
@@ -818,16 +871,15 @@
818 871 ?>
819 872 </p>
820 873 </div>
821 874 </div>
822 -
823 - <!-- Website -->
875 +
824 876 <div class="flex items-start group md:col-span-2">
825 877 <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>
826 878 <div class="ml-3">
827 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Website', 'easy-invoice'); ?></label>
879 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Website', 'easy-invoice'); ?></label>
828 880 <p id="display-client-website" class="text-sm text-gray-800 mt-1">
829 - <?php
881 + <?php
830 882 if ($invoice && $invoice->getClientId()) {
831 883 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
832 884 if ($client) {
833 885 echo esc_html($client->getWebsite() ?: esc_html__('—', 'easy-invoice'));
@@ -840,16 +892,15 @@
840 892 ?>
841 893 </p>
842 894 </div>
843 895 </div>
844 -
845 - <!-- Address -->
896 +
846 897 <div class="flex items-start group md:col-span-2">
847 898 <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>
848 899 <div class="ml-3">
849 - <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php _e('Address', 'easy-invoice'); ?></label>
900 + <label class="block text-xs font-medium text-gray-500 uppercase tracking-wider"><?php esc_html_e('Address', 'easy-invoice'); ?></label>
850 901 <p id="display-client-address" class="text-sm text-gray-800 mt-1 whitespace-pre-wrap">
851 - <?php
902 + <?php
852 903 if ($invoice && $invoice->getClientId()) {
853 904 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
854 905 if ($client) {
855 906 echo esc_html($client->getAddress() ?: esc_html__('—', 'easy-invoice'));
@@ -865,59 +916,55 @@
865 916 </div>
866 917 </div>
867 918 </div>
868 919 </div>
869 -
870 - <!-- No Client Selected Message -->
920 +
871 921 <div id="no-client-message" class="mt-4 text-center py-6 border-2 border-dashed border-gray-300 rounded-lg <?php echo $invoice && $invoice->getClientId() ? 'hidden' : ''; ?>">
872 - <p class="text-gray-600"><?php _e('Select a client from the dropdown above to view their information.', 'easy-invoice'); ?></p>
922 + <p class="text-gray-600"><?php esc_html_e('Select a client from the dropdown above to view their information.', 'easy-invoice'); ?></p>
873 923 </div>
874 924 </div>
875 925 <?php elseif ($tab['id'] === 'discounts-taxes-tab'): ?>
876 - <!-- Discount & Tax Tab Content -->
877 926 <div>
878 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Discount & Tax Settings', 'easy-invoice'); ?></h3>
927 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Discount & Tax Settings', 'easy-invoice'); ?></h3>
879 928 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
880 - <?php
929 + <?php
881 930 // Get all discount and tax fields
882 931 $discount_fields = $form_manager->getFieldsBySection('discounts-taxes-tab', 'discount');
883 932 $tax_fields = $form_manager->getFieldsBySection('discounts-taxes-tab', 'tax');
884 933 $additional_tax_fields = $form_manager->getFieldsBySection('discounts-taxes-tab', 'additional_tax');
885 -
934 +
886 935 // Combine and sort by order
887 936 $all_fields = array_merge($discount_fields, $tax_fields, $additional_tax_fields);
888 937 usort($all_fields, function($a, $b) {
889 938 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
890 939 });
891 -
892 - foreach ($all_fields as $field):
893 - echo $form_manager->renderField($field, $invoice);
894 - endforeach;
940 +
941 + foreach ($all_fields as $field):
942 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
943 + endforeach;
895 944 ?>
896 945 </div>
897 946 </div>
898 947 <?php elseif ($tab['id'] === 'payments-tab'): ?>
899 - <!-- Payment Tab Content -->
900 948 <div>
901 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Payment Information', 'easy-invoice'); ?></h3>
949 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Payment Information', 'easy-invoice'); ?></h3>
902 950 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
903 - <?php
951 + <?php
904 952 $payment_fields = $form_manager->getFieldsBySection('payments-tab', 'payment');
905 - foreach ($payment_fields as $field):
906 - echo $form_manager->renderField($field, $invoice);
907 - endforeach;
953 + foreach ($payment_fields as $field):
954 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
955 + endforeach;
908 956 ?>
909 957 </div>
910 -
911 - <!-- Currency Section -->
958 +
912 959 <div class="mt-8 border-t border-gray-200 pt-6">
913 - <h3 class="text-lg font-medium text-gray-900"><?php _e('Currency', 'easy-invoice'); ?></h3>
960 + <h3 class="text-lg font-medium text-gray-900"><?php esc_html_e('Currency', 'easy-invoice'); ?></h3>
914 961 <div class="mt-4 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
915 - <?php
962 + <?php
916 963 $currency_fields = $form_manager->getFieldsBySection('payments-tab', 'currency');
917 - foreach ($currency_fields as $field):
918 - echo $form_manager->renderField($field, $invoice);
919 - endforeach;
964 + foreach ($currency_fields as $field):
965 + echo $form_manager->renderField($field, $invoice); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- FieldRenderer escapes every attribute and label it emits.
966 + endforeach;
920 967 ?>
921 968 </div>
922 969 </div>
923 970 </div>
@@ -926,19 +973,18 @@
926 973 <?php endforeach; ?>
927 974 </form>
928 975 </div>
929 976
930 - <!-- Template Gallery Modal -->
931 977 <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">
932 978 <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">
933 979 <div class="flex justify-between items-center mb-4">
934 - <h3 class="text-xl font-medium text-gray-900"><?php _e('Browse Invoice Templates', 'easy-invoice'); ?></h3>
980 + <h3 class="text-xl font-medium text-gray-900"><?php esc_html_e('Browse Invoice Templates', 'easy-invoice'); ?></h3>
935 981 <button type="button" class="close-gallery-modal text-gray-400 hover:text-gray-500 focus:outline-none">
936 - <span class="sr-only"><?php _e('Close', 'easy-invoice'); ?></span>
982 + <span class="sr-only"><?php esc_html_e('Close', 'easy-invoice'); ?></span>
937 983 <i class="fas fa-times text-xl"></i>
938 984 </button>
939 985 </div>
940 -
986 +
941 987 <div class="templates-grid grid grid-cols-2 md:grid-cols-3 gap-4 mb-4 max-h-[45vh] overflow-y-auto p-2">
942 988 <?php
943 989 // Display all template options in the modal
944 990 foreach ($all_templates as $template_id => $template) :
@@ -944,9 +990,9 @@
944 990 foreach ($all_templates as $template_id => $template) :
945 991 $is_current = ($current_template === $template_id);
946 992 $selected_class = $is_current ? 'selected' : '';
947 993 ?>
948 - <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'; ?>">
994 + <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'; ?>">
949 995 <div class="p-4 border rounded-lg">
950 996 <div class="flex items-center mb-2">
951 997 <i class="<?php echo esc_attr($template['icon']); ?> text-indigo-600 mr-2"></i>
952 998 <span class="text-sm font-medium"><?php echo esc_html($template['name']); ?></span>
@@ -959,21 +1005,20 @@
959 1005 </div>
960 1006 </div>
961 1007 <?php endforeach; ?>
962 1008 </div>
963 -
1009 +
964 1010 <div class="flex justify-end mt-4 space-x-3">
965 1011 <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">
966 - <?php _e('Apply', 'easy-invoice'); ?>
1012 + <?php esc_html_e('Apply', 'easy-invoice'); ?>
967 1013 </button>
968 1014 <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">
969 - <?php _e('Close', 'easy-invoice'); ?>
1015 + <?php esc_html_e('Close', 'easy-invoice'); ?>
970 1016 </button>
971 1017 </div>
972 1018 </div>
973 1019 </div>
974 1020
975 - <!-- Hidden Template for Invoice Items -->
976 1021 <template id="invoice-item-template" style="display: none;">
977 1022 <div class="invoice-item p-6 rounded-xl shadow-sm border border-blue-100 hover:shadow-md transition-all duration-200 mb-6" style="background-color: #f8fbff;">
978 1023 <div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
979 1024 <div class="flex items-center">
@@ -981,18 +1026,18 @@
981 1026 <i class="fas fa-chevron-down text-sm"></i>
982 1027 </button>
983 1028 <h3 class="text-base font-normal text-gray-800 flex-1">
984 1029 <?php
985 - echo !empty($title) ? esc_html($title) : __('New Item', 'easy-invoice');
1030 + echo !empty($title) ? esc_html($title) : esc_html__('New Item', 'easy-invoice');
986 1031 ?>
987 1032 </h3>
988 1033 </div>
989 1034 <div class="flex items-center gap-3">
990 1035 <div class="item-collapsed-summary hidden text-sm text-gray-600 bg-gray-50 px-3 py-1 rounded-full">
991 - <span class="quantity-summary">1</span> × $<span class="price-summary">0.00</span>
1036 + <span class="quantity-summary">0</span> × $<span class="price-summary">0.00</span>
992 1037 = $<span class="total-summary">0.00</span>
993 1038 </div>
994 - <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'); ?>">
1039 + <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')); ?>">
995 1040 <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
996 1041 <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"/>
997 1042 <path d="M4 5h12v1.5H4V5zm0 3h10v1.5H4V8zm0 3h11v1.5H4V11zm0 3h8v1.5H4V14z"/>
998 1043 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
@@ -998,40 +1043,47 @@
998 1043 <path d="M14 14l3 3-3 3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
999 1044 <path d="M14 17h3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
1000 1045 </svg>
1001 1046 </button>
1047 + <?php if (easy_invoice_has_pro() && \EasyInvoice\Addons\AddonManager::shouldLoad('item_library')): ?>
1048 + <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')); ?>">
1049 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
1050 + <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"/>
1051 + </svg>
1052 + </button>
1053 + <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-nonce="<?php echo esc_attr(wp_create_nonce('easy_invoice_pro_item_library')); ?>">
1054 + <svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" class="inline-block">
1055 + <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"/>
1056 + </svg>
1057 + </button>
1058 + <?php endif; ?>
1002 1059 <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">
1003 - <i class="fas fa-trash-alt mr-1"></i> <?php _e('Remove', 'easy-invoice'); ?>
1060 + <i class="fas fa-trash-alt mr-1"></i> <?php esc_html_e('Remove', 'easy-invoice'); ?>
1004 1061 </button>
1005 1062 </div>
1006 1063 </div>
1007 1064 <div class="item-content">
1008 - <?php echo $form_manager->generateItemTemplateHtml(); ?>
1065 + <?php echo $form_manager->generateItemTemplateHtml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- form markup built by ItemFieldRenderer, which escapes every value. ?>
1009 1066 </div>
1010 1067 </div>
1011 1068 </template>
1012 1069
1013 - <!-- Add Client Modal -->
1014 - <div id="add_client_modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-75 overflow-y-auto h-full w-full z-50">
1015 - <div class="relative top-20 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white">
1016 - <div class="flex justify-between items-center mb-4">
1017 - <h3 class="text-lg font-medium text-gray-900">Add New Client</h3>
1018 - <button type="button" class="close-modal text-gray-400 hover:text-gray-500">
1019 - <span class="sr-only">Close</span>
1020 - <i class="fas fa-times"></i>
1021 - </button>
1022 - </div>
1023 -
1024 - <?php
1025 - // Unset the $client variable from the foreach loop to ensure clean add form
1026 - unset($client);
1027 - include_once EASY_INVOICE_PLUGIN_DIR . 'templates/client-form.php';
1028 - ?>
1029 - </div>
1030 - </div>
1070 + <?php
1071 + // The "Add New Client" modal used to be duplicated here. This file is
1072 + // included by templates/invoices/builder.php, which renders its own
1073 + // copy of the modal a few lines later — so #add_client_modal existed
1074 + // twice in the DOM. Every `$('#add_client_modal')` binding matched the
1075 + // first one, which was this one: the older markup, missing the
1076 + // role="dialog" / aria-modal / aria-labelledby attributes the builder's
1077 + // copy carries. The accessible modal was inert dead markup.
1078 + //
1079 + // Removed here, keeping the builder's version — which is exactly how
1080 + // the quote side is already structured (quotes/form.php has no modal;
1081 + // quotes/builder.php owns the only one).
1082 + ?>
1031 1083
1032 1084 <?php
1033 -
1085 +
1034 1086 // Always localize script with AJAX URL and nonce
1035 1087 wp_localize_script('easy-invoice-form', 'easyInvoice', array(
1036 1088 'ajaxUrl' => admin_url('admin-ajax.php'),
1037 1089 'nonce' => wp_create_nonce('easy_invoice_nonce'),
@@ -1041,15 +1093,15 @@
1041 1093
1042 1094 <script>
1043 1095 jQuery(document).ready(function($) {
1044 1096
1045 -
1097 +
1046 1098 // Client search functionality for invoice form
1047 1099 var searchTimeout;
1048 -
1100 +
1049 1101 // Initialize existing client data
1050 1102 var existingClientId = '<?php echo $invoice ? esc_js($invoice->getClientId()) : ''; ?>';
1051 - var existingClientName = '<?php
1103 + var existingClientName = '<?php
1052 1104 if ($invoice && $invoice->getClientId()) {
1053 1105 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
1054 1106 if ($client) {
1055 1107 echo esc_js($client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()));
@@ -1055,9 +1107,9 @@
1055 1107 echo esc_js($client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()));
1056 1108 }
1057 1109 }
1058 1110 ?>';
1059 - var existingClientEmail = '<?php
1111 + var existingClientEmail = '<?php
1060 1112 if ($invoice && $invoice->getClientId()) {
1061 1113 $client = \EasyInvoice\Providers\ClientServiceProvider::getClientRepository()->find($invoice->getClientId());
1062 1114 if ($client) {
1063 1115 echo esc_js($client->getEmail() ?: '');
@@ -1064,29 +1116,29 @@
1064 1116 }
1065 1117 }
1066 1118 ?>';
1067 1119 var existingClientData = <?php echo json_encode($client_data); ?>;
1068 -
1120 +
1069 1121 if (existingClientId && existingClientName && existingClientName !== '-') {
1070 1122 // Show the selected client display and client info display
1071 1123 $('#selected-client-display').show();
1072 1124 $('#client-info-display').show();
1073 1125 $('#no-client-message').hide();
1074 -
1126 +
1075 1127 // Set the select client dropdown value
1076 1128 $('#select-client').val(existingClientId);
1077 -
1129 +
1078 1130 // Populate the search input with the existing client name
1079 1131 $('#client-search-input').val(existingClientName);
1080 -
1081 1132
1133 +
1082 1134 }
1083 -
1135 +
1084 1136 // Client search input functionality
1085 1137 $('#client-search-input').on('input', function() {
1086 1138 const query = $(this).val().trim();
1087 1139 clearTimeout(searchTimeout);
1088 -
1140 +
1089 1141 // Don't hide results immediately for short queries - let the search function handle it
1090 1142 if (query.length === 0) {
1091 1143 // Show all clients when input is cleared
1092 1144 searchClients('');
@@ -1091,9 +1143,9 @@
1091 1143 // Show all clients when input is cleared
1092 1144 searchClients('');
1093 1145 return;
1094 1146 }
1095 -
1147 +
1096 1148 searchTimeout = setTimeout(function() {
1097 1149 searchClients(query);
1098 1150 }, 300);
1099 1151 });
@@ -1117,9 +1169,9 @@
1117 1169 // Show the dropdown and call AJAX to populate it
1118 1170 $('#client-search-results').show();
1119 1171 $('#client-search-loading').show();
1120 1172 $('#client-search-empty').hide();
1121 -
1173 +
1122 1174 // Call the AJAX method to populate the dropdown
1123 1175 if (typeof easyInvoice !== 'undefined' && easyInvoice.ajaxUrl) {
1124 1176 searchClients('');
1125 1177 } else {
@@ -1131,9 +1183,9 @@
1131 1183 // Client selection
1132 1184 $(document).on('click', '.client-option', function(e) {
1133 1185 e.preventDefault();
1134 1186 e.stopPropagation();
1135 -
1187 +
1136 1188 const clientId = $(this).data('client-id');
1137 1189 const clientName = $(this).data('client-name');
1138 1190 const clientEmail = $(this).data('client-email');
1139 1191 const clientCompany = $(this).data('client-company');
@@ -1139,9 +1191,9 @@
1139 1191 const clientCompany = $(this).data('client-company');
1140 1192 const clientPhone = $(this).data('client-phone');
1141 1193 const clientWebsite = $(this).data('client-website');
1142 1194 const clientAddress = $(this).data('client-address');
1143 -
1195 +
1144 1196 // Create client data object with all available information
1145 1197 const clientData = {
1146 1198 name: clientName,
1147 1199 email: clientEmail,
@@ -1149,19 +1201,19 @@
1149 1201 phone: clientPhone,
1150 1202 website: clientWebsite,
1151 1203 address: clientAddress
1152 1204 };
1153 -
1205 +
1154 1206 selectClient(clientId, clientName, clientEmail, clientData);
1155 1207 $('#client-search-results').hide();
1156 1208 $('#client-search-input').val('');
1157 1209 });
1158 -
1210 +
1159 1211 // Alternative click handler for client options
1160 1212 $('#client-search-list').on('click', '.client-option', function(e) {
1161 1213 e.preventDefault();
1162 1214 e.stopPropagation();
1163 -
1215 +
1164 1216 const clientId = $(this).data('client-id');
1165 1217 const clientName = $(this).data('client-name');
1166 1218 const clientEmail = $(this).data('client-email');
1167 1219 const clientCompany = $(this).data('client-company');
@@ -1167,9 +1219,9 @@
1167 1219 const clientCompany = $(this).data('client-company');
1168 1220 const clientPhone = $(this).data('client-phone');
1169 1221 const clientWebsite = $(this).data('client-website');
1170 1222 const clientAddress = $(this).data('client-address');
1171 -
1223 +
1172 1224 // Create client data object with all available information
1173 1225 const clientData = {
1174 1226 name: clientName,
1175 1227 email: clientEmail,
@@ -1177,9 +1229,9 @@
1177 1229 phone: clientPhone,
1178 1230 website: clientWebsite,
1179 1231 address: clientAddress
1180 1232 };
1181 -
1233 +
1182 1234 selectClient(clientId, clientName, clientEmail, clientData);
1183 1235 $('#client-search-results').hide();
1184 1236 $('#client-search-input').val('');
1185 1237 });
@@ -1203,14 +1255,14 @@
1203 1255 function searchClients(query) {
1204 1256 $('#client-search-loading').show();
1205 1257 $('#client-search-empty').hide();
1206 1258 $('#client-search-list').empty();
1207 -
1259 +
1208 1260 // If query is empty or less than 2 characters, show all clients
1209 1261 if (!query || query.trim().length < 2) {
1210 1262 query = ''; // Set to empty string to get all clients
1211 1263 }
1212 -
1264 +
1213 1265 $.ajax({
1214 1266 url: easyInvoice.ajaxUrl,
1215 1267 type: 'POST',
1216 1268 data: {
@@ -1219,26 +1271,26 @@
1219 1271 nonce: easyInvoice.nonce
1220 1272 },
1221 1273 success: function(response) {
1222 1274 $('#client-search-loading').hide();
1223 -
1275 +
1224 1276 if (response.success && response.data.length > 0) {
1225 1277 response.data.forEach(function(client) {
1226 - const option = $(`
1227 - <div class="client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0"
1228 - data-client-id="${client.id}"
1229 - data-client-name="${client.name.replace(/"/g, '&quot;')}"
1230 - data-client-email="${client.email.replace(/"/g, '&quot;')}"
1231 - data-client-company="${(client.company || '').replace(/"/g, '&quot;')}"
1232 - data-client-phone="${(client.phone || '').replace(/"/g, '&quot;')}"
1233 - data-client-website="${(client.website || '').replace(/"/g, '&quot;')}"
1234 - data-client-address="${(client.address || '').replace(/"/g, '&quot;')}"
1235 - onclick="selectClientFromOption(this)">
1236 - <div class="font-medium text-gray-900">${client.name}</div>
1237 - <div class="text-sm text-gray-600">${client.email}</div>
1238 - </div>
1239 - `);
1240 -
1278 + // Built with jQuery, never by string interpolation: the name, company
1279 + // and address are what the client typed into their portal profile.
1280 + const option = $('<div>', {
1281 + 'class': 'client-option px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0',
1282 + 'data-client-id': client.id,
1283 + 'data-client-name': client.name || '',
1284 + 'data-client-email': client.email || '',
1285 + 'data-client-company': client.company || '',
1286 + 'data-client-phone': client.phone || '',
1287 + 'data-client-website': client.website || '',
1288 + 'data-client-address': client.address || ''
1289 + }).on('click', function() { selectClientFromOption(this); })
1290 + .append($('<div>', { 'class': 'font-medium text-gray-900' }).text(client.name || ''))
1291 + .append($('<div>', { 'class': 'text-sm text-gray-600' }).text(client.email || ''));
1292 +
1241 1293 $('#client-search-list').append(option);
1242 1294 });
1243 1295 $('#client-search-results').show();
1244 1296 } else {
@@ -1254,21 +1306,22 @@
1254 1306 });
1255 1307 }
1256 1308
1257 1309 function selectClient(clientId, clientName, clientEmail, clientData = null) {
1258 -
1310 +
1259 1311 $('#select-client').val(clientId);
1260 1312 $('#client-id').val(clientId);
1261 1313 $('#selected-client-name').text(clientName);
1262 1314 $('#selected-client-email').text(clientEmail);
1263 -
1315 +
1264 1316 $('#selected-client-display').show().removeClass('hidden');
1265 1317 $('#client-info-display').show().removeClass('hidden');
1266 1318 $('#no-client-message').hide().removeClass('hidden');
1267 -
1319 +
1268 1320 // Update the edit client button URL
1269 - $('#edit-selected-client').attr('href', '<?php echo admin_url('admin.php?page=easy-invoice-client-edit&client_id='); ?>' + clientId);
1270 -
1321 + $('#edit-selected-client').attr('href', '<?php echo esc_url(admin_url('admin.php?page=easy-invoice-client-edit&client_id=')); ?>' + clientId);
1322 + $('#edit-selected-client').toggleClass('hidden', !clientId);
1323 +
1271 1324 // If we have full client data, populate all fields directly
1272 1325 if (clientData) {
1273 1326 $('#display-client-name').text(clientData.name || clientName || '-');
1274 1327 $('#display-client-company').text(clientData.company || '-');
@@ -1292,13 +1345,13 @@
1292 1345 $('#client-id').val('');
1293 1346 $('#selected-client-display').hide();
1294 1347 $('#client-info-display').hide();
1295 1348 $('#no-client-message').show();
1296 -
1349 +
1297 1350 // Clear display fields
1298 1351 $('#display-client-name, #display-client-email, #display-client-company, #display-client-phone, #display-client-website, #display-client-address').text('-');
1299 1352 }
1300 -
1353 +
1301 1354 // Global function for inline onclick
1302 1355 window.selectClientFromOption = function(element) {
1303 1356 const $element = $(element);
1304 1357 const clientId = $element.data('client-id');
@@ -1307,9 +1360,9 @@
1307 1360 const clientCompany = $element.data('client-company');
1308 1361 const clientPhone = $element.data('client-phone');
1309 1362 const clientWebsite = $element.data('client-website');
1310 1363 const clientAddress = $element.data('client-address');
1311 -
1364 +
1312 1365 // Create client data object with all available information
1313 1366 const clientData = {
1314 1367 name: clientName,
1315 1368 email: clientEmail,
@@ -1317,9 +1370,9 @@
1317 1370 phone: clientPhone,
1318 1371 website: clientWebsite,
1319 1372 address: clientAddress
1320 1373 };
1321 -
1374 +
1322 1375 selectClient(clientId, clientName, clientEmail, clientData);
1323 1376 $('#client-search-results').hide();
1324 1377 $('#client-search-input').val('');
1325 1378 };
@@ -1328,21 +1381,20 @@
1328 1381
1329 1382 <script>
1330 1383 // Invoice field configuration and pre-loaded data
1331 1384 window.easyInvoice = window.easyInvoice || {};
1332 -<?php
1333 -$json_field_config = json_encode($invoice_field_config);
1385 +<?php
1386 +$json_field_config = wp_json_encode($invoice_field_config);
1334 1387 ?>
1335 -window.easyInvoice.fieldConfig = <?php echo $json_field_config; ?>;
1336 -window.easyInvoice.adminUrl = '<?php echo admin_url(); ?>';
1388 +window.easyInvoice.fieldConfig = <?php echo $json_field_config; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode() output. ?>;
1389 +window.easyInvoice.adminUrl = '<?php echo esc_url(admin_url()); ?>';
1337 1390 window.easyInvoice.isPro = <?php echo easy_invoice_has_pro() ? 'true' : 'false'; ?>;
1338 1391 window.easyInvoice.translations = {
1339 - premiumTemplates: '<?php _e('Premium Templates', 'easy-invoice'); ?>',
1340 - premiumTemplatesDescription: '<?php _e('Access all premium templates including Modern, Professional, Classic, and more to give your invoices a unique and professional look.', 'easy-invoice'); ?>',
1341 - premiumFeatureMessage: '<?php _e('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice'); ?>',
1342 - selectTemplateFirst: '<?php _e('Please select a template first', 'easy-invoice'); ?>'
1392 + premiumTemplates: '<?php echo esc_js(__('Premium Templates', 'easy-invoice')); ?>',
1393 + premiumTemplatesDescription: '<?php echo esc_js(__('Access all premium templates including Modern, Professional, Classic, and more to give your invoices a unique and professional look.', 'easy-invoice')); ?>',
1394 + premiumFeatureMessage: '<?php echo esc_js(__('This is a premium feature. Please upgrade to Pro to access all templates.', 'easy-invoice')); ?>',
1395 + selectTemplateFirst: '<?php echo esc_js(__('Please select a template first', 'easy-invoice')); ?>'
1343 1396 };
1344 1397 <?php if ($client_data): ?>
1345 1398 window.easyInvoice.clientData = <?php echo json_encode($client_data); ?>;
1346 1399 <?php endif; ?>
1347 1400 </script>
1348 -