PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Forms / FieldRenderer.php

FieldRenderer.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/Forms/FieldRenderer.php

915 lines 38.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Field Renderer Class
4 *
5 * @package EasyInvoice
6 * @author Your Name
7 * @copyright Copyright (c) 2023, Your Company
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Forms;
13
14 /**
15 * Field Renderer
16 *
17 * Handles rendering of all form field types for the invoice form.
18 *
19 * @since 1.0.0
20 */
21 class FieldRenderer {
22
23 /**
24 * Render a field based on its type
25 *
26 * @since 1.0.0
27 * @param array $field Field configuration
28 * @param mixed $invoice Invoice object or null
29 * @return string Rendered HTML
30 */
31 public function renderField(array $field, $invoice = null): string {
32 $type = $field['type'] ?? 'text';
33 if ('attachments' === $type) {
34 return \EasyInvoice\Services\DocumentAttachments::renderField($field, $invoice);
35 }
36
37 // Convert field type to camelCase for method name generation
38 $method_name = 'render' . easy_invoice_str_replace('_', '', ucwords($type, '_')) . 'Field';
39
40 if (method_exists($this, $method_name)) {
41 return $this->$method_name($field, $invoice);
42 }
43
44 // Fallback to text field
45 return $this->renderTextField($field, $invoice);
46 }
47
48 /**
49 * Render a text field
50 *
51 * @since 1.0.0
52 * @param array $field Field configuration
53 * @param mixed $invoice Invoice object or null
54 * @return string Rendered HTML
55 */
56 private function renderTextField(array $field, $invoice = null): string {
57 $id = $field['id'] ?? $field['name'] ?? '';
58 $name = $field['name'] ?? '';
59 $label = $field['label'] ?? '';
60 $placeholder = $field['placeholder'] ?? '';
61 $required = $field['required'] ?? false;
62 $readonly = $field['readonly'] ?? false;
63 $description = $field['description'] ?? '';
64 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
65 $value = $this->getFieldValue($field, $invoice);
66
67 $required_attr = $required ? 'required' : '';
68 $required_class = $required ? 'required' : '';
69 $readonly_attr = $readonly ? 'readonly' : '';
70 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
71
72 return sprintf(
73 '<div class="%s">
74 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
75 <div class="mt-1">
76 <input type="text"
77 id="%s"
78 name="%s"
79 value="%s"
80 placeholder="%s"
81 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
82 %s %s>
83 %s
84 </div>
85 </div>',
86 esc_attr($grid_cols),
87 esc_attr($id),
88 esc_attr($required_class),
89 esc_html($label),
90 esc_attr($id),
91 esc_attr($name),
92 esc_attr($value),
93 esc_attr($placeholder),
94 esc_attr($required_class),
95 $required_attr,
96 $readonly_attr,
97 $description_html
98 );
99 }
100
101 /**
102 * Render a hidden field
103 *
104 * @since 1.0.0
105 * @param array $field Field configuration
106 * @param mixed $invoice Invoice object or null
107 * @return string Rendered HTML
108 */
109 private function renderHiddenField(array $field, $invoice = null): string {
110 $id = $field['id'] ?? '';
111 $name = $field['name'] ?? '';
112 $value = $this->getFieldValue($field, $invoice);
113
114 return sprintf(
115 '<input type="hidden" id="%s" name="%s" value="%s">',
116 esc_attr($id),
117 esc_attr($name),
118 esc_attr($value)
119 );
120 }
121
122 /**
123 * Render an email field
124 *
125 * @since 1.0.0
126 * @param array $field Field configuration
127 * @param mixed $invoice Invoice object or null
128 * @return string Rendered HTML
129 */
130 private function renderEmailField(array $field, $invoice = null): string {
131 $id = $field['id'] ?? '';
132 $name = $field['name'] ?? '';
133 $label = $field['label'] ?? '';
134 $placeholder = $field['placeholder'] ?? '';
135 $required = $field['required'] ?? false;
136 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
137 $value = $this->getFieldValue($field, $invoice);
138 $description = $field['description'] ?? '';
139
140 $required_attr = $required ? 'required' : '';
141 $required_class = $required ? 'required' : '';
142 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
143
144 return sprintf(
145 '<div class="%s">
146 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
147 <div class="mt-1">
148 <input type="email"
149 id="%s"
150 name="%s"
151 value="%s"
152 placeholder="%s"
153 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
154 %s>
155 %s
156 </div>
157 </div>',
158 esc_attr($grid_cols),
159 esc_attr($id),
160 esc_attr($required_class),
161 esc_html($label),
162 esc_attr($id),
163 esc_attr($name),
164 esc_attr($value),
165 esc_attr($placeholder),
166 esc_attr($required_class),
167 $required_attr,
168 $description_html
169 );
170 }
171
172 /**
173 * Render a telephone field
174 *
175 * @since 1.0.0
176 * @param array $field Field configuration
177 * @param mixed $invoice Invoice object or null
178 * @return string Rendered HTML
179 */
180 private function renderTelField(array $field, $invoice = null): string {
181 $id = $field['id'] ?? '';
182 $name = $field['name'] ?? '';
183 $label = $field['label'] ?? '';
184 $placeholder = $field['placeholder'] ?? '';
185 $required = $field['required'] ?? false;
186 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
187 $value = $this->getFieldValue($field, $invoice);
188 $description = $field['description'] ?? '';
189
190 $required_attr = $required ? 'required' : '';
191 $required_class = $required ? 'required' : '';
192 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
193
194 return sprintf(
195 '<div class="%s">
196 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
197 <div class="mt-1">
198 <input type="tel"
199 id="%s"
200 name="%s"
201 value="%s"
202 placeholder="%s"
203 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
204 %s>
205 %s
206 </div>
207 </div>',
208 esc_attr($grid_cols),
209 esc_attr($id),
210 esc_attr($required_class),
211 esc_html($label),
212 esc_attr($id),
213 esc_attr($name),
214 esc_attr($value),
215 esc_attr($placeholder),
216 esc_attr($required_class),
217 $required_attr,
218 $description_html
219 );
220 }
221
222 /**
223 * Render a URL field
224 *
225 * @since 1.0.0
226 * @param array $field Field configuration
227 * @param mixed $invoice Invoice object or null
228 * @return string Rendered HTML
229 */
230 private function renderUrlField(array $field, $invoice = null): string {
231 $id = $field['id'] ?? '';
232 $name = $field['name'] ?? '';
233 $label = $field['label'] ?? '';
234 $placeholder = $field['placeholder'] ?? '';
235 $required = $field['required'] ?? false;
236 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
237 $value = $this->getFieldValue($field, $invoice);
238 $description = $field['description'] ?? '';
239
240 $required_attr = $required ? 'required' : '';
241 $required_class = $required ? 'required' : '';
242 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
243
244 return sprintf(
245 '<div class="%s">
246 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
247 <div class="mt-1">
248 <input type="url"
249 id="%s"
250 name="%s"
251 value="%s"
252 placeholder="%s"
253 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
254 %s>
255 %s
256 </div>
257 </div>',
258 esc_attr($grid_cols),
259 esc_attr($id),
260 esc_attr($required_class),
261 esc_html($label),
262 esc_attr($id),
263 esc_attr($name),
264 esc_attr($value),
265 esc_attr($placeholder),
266 esc_attr($required_class),
267 $required_attr,
268 $description_html
269 );
270 }
271
272 /**
273 * Render a date field
274 *
275 * @since 1.0.0
276 * @param array $field Field configuration
277 * @param mixed $invoice Invoice object or null
278 * @return string Rendered HTML
279 */
280 private function renderDateField(array $field, $invoice = null): string {
281 $id = $field['id'] ?? '';
282 $name = $field['name'] ?? '';
283 $label = $field['label'] ?? '';
284 $placeholder = $field['placeholder'] ?? '';
285 $required = $field['required'] ?? false;
286 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
287 $value = $this->getFieldValue($field, $invoice);
288 $description = $field['description'] ?? '';
289
290 $required_attr = $required ? 'required' : '';
291 $required_class = $required ? 'required' : '';
292 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
293
294 return sprintf(
295 '<div class="%s">
296 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
297 <div class="mt-1">
298 <input type="date"
299 id="%s"
300 name="%s"
301 value="%s"
302 placeholder="%s"
303 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
304 %s>
305 %s
306 </div>
307 </div>',
308 esc_attr($grid_cols),
309 esc_attr($id),
310 esc_attr($required_class),
311 esc_html($label),
312 esc_attr($id),
313 esc_attr($name),
314 esc_attr($value),
315 esc_attr($placeholder),
316 esc_attr($required_class),
317 $required_attr,
318 $description_html
319 );
320 }
321
322 /**
323 * Render a number field
324 *
325 * @since 1.0.0
326 * @param array $field Field configuration
327 * @param mixed $invoice Invoice object or null
328 * @return string Rendered HTML
329 */
330 private function renderNumberField(array $field, $invoice = null): string {
331 $id = $field['id'] ?? '';
332 $name = $field['name'] ?? '';
333 $label = $field['label'] ?? '';
334 $placeholder = $field['placeholder'] ?? '';
335 $required = $field['required'] ?? false;
336 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
337 $value = $this->getFieldValue($field, $invoice);
338 $min = $field['min'] ?? '';
339 $max = $field['max'] ?? '';
340 $step = $field['step'] ?? '';
341 $description = $field['description'] ?? '';
342
343 $required_attr = $required ? 'required' : '';
344 $required_class = $required ? 'required' : '';
345 $min_attr = $min !== '' ? 'min="' . esc_attr($min) . '"' : '';
346 $max_attr = $max !== '' ? 'max="' . esc_attr($max) . '"' : '';
347 $step_attr = $step !== '' ? 'step="' . esc_attr($step) . '"' : '';
348 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
349
350 return sprintf(
351 '<div class="%s">
352 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
353 <div class="mt-1">
354 <input type="number"
355 id="%s"
356 name="%s"
357 value="%s"
358 placeholder="%s"
359 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
360 %s %s %s %s>
361 %s
362 </div>
363 </div>',
364 esc_attr($grid_cols),
365 esc_attr($id),
366 esc_attr($required_class),
367 esc_html($label),
368 esc_attr($id),
369 esc_attr($name),
370 esc_attr($value),
371 esc_attr($placeholder),
372 esc_attr($required_class),
373 $required_attr,
374 $min_attr,
375 $max_attr,
376 $step_attr,
377 $description_html
378 );
379 }
380
381 /**
382 * Render a select field
383 *
384 * @since 1.0.0
385 * @param array $field Field configuration
386 * @param mixed $invoice Invoice object or null
387 * @return string Rendered HTML
388 */
389 private function renderSelectField(array $field, $invoice = null): string {
390 $id = $field['id'] ?? $field['name'] ?? '';
391 $name = $field['name'] ?? '';
392 $label = $field['label'] ?? '';
393 $required = $field['required'] ?? false;
394 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
395 $value = $this->getFieldValue($field, $invoice);
396 $options = $field['options'] ?? [];
397 $description = $field['description'] ?? '';
398
399 $required_attr = $required ? 'required' : '';
400 $required_class = $required ? 'required' : '';
401 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
402
403 $options_html = '';
404 foreach ($options as $option_value => $option_label) {
405 $selected = ($value == $option_value) ? 'selected' : '';
406 $options_html .= sprintf(
407 '<option value="%s" %s>%s</option>',
408 esc_attr($option_value),
409 $selected,
410 esc_html($option_label)
411 );
412 }
413
414 return sprintf(
415 '<div class="%s">
416 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
417 <div class="mt-1">
418 <select id="%s"
419 name="%s"
420 class="mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md %s"
421 %s>
422 %s
423 </select>
424 %s
425 </div>
426 </div>',
427 esc_attr($grid_cols),
428 esc_attr($id),
429 esc_attr($required_class),
430 esc_html($label),
431 esc_attr($id),
432 esc_attr($name),
433 esc_attr($required_class),
434 $required_attr,
435 $options_html,
436 $description_html
437 );
438 }
439
440 /**
441 * Render a textarea field
442 *
443 * @since 1.0.0
444 * @param array $field Field configuration
445 * @param mixed $invoice Invoice object or null
446 * @return string Rendered HTML
447 */
448 private function renderTextareaField(array $field, $invoice = null): string {
449 $id = $field['id'] ?? '';
450 $name = $field['name'] ?? '';
451 $label = $field['label'] ?? '';
452 $placeholder = $field['placeholder'] ?? '';
453 $required = $field['required'] ?? false;
454 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
455 $value = $this->getFieldValue($field, $invoice);
456 $rows = $field['rows'] ?? 3;
457 $description = $field['description'] ?? '';
458 $required_attr = $required ? 'required' : '';
459 $required_class = $required ? 'required' : '';
460 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
461
462 return sprintf(
463 '<div class="%s">
464 <label for="%s" class="block text-sm font-medium text-gray-700 %s">%s</label>
465 <div class="mt-1">
466 <textarea id="%s"
467 name="%s"
468 rows="%s"
469 placeholder="%s"
470 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm %s"
471 %s>%s</textarea>
472 %s
473 </div>
474 </div>',
475 esc_attr($grid_cols),
476 esc_attr($id),
477 esc_attr($required_class),
478 esc_html($label),
479 esc_attr($id),
480 esc_attr($name),
481 esc_attr($rows),
482 esc_attr($placeholder),
483 esc_attr($required_class),
484 $required_attr,
485 wp_kses_post($value),
486 $description_html
487 );
488 }
489
490 /**
491 * Render a checkbox field
492 *
493 * @since 1.0.0
494 * @param array $field Field configuration
495 * @param mixed $invoice Invoice object or null
496 * @return string Rendered HTML
497 */
498 private function renderCheckboxField(array $field, $invoice = null): string {
499 $id = $field['id'] ?? $field['name'] ?? '';
500 $name = $field['name'] ?? '';
501 $label = $field['label'] ?? '';
502 $description = $field['description'] ?? '';
503 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
504 $value = $this->getFieldValue($field, $invoice);
505 $checked = $value ? 'checked' : '';
506
507 $description_html = $description ? '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>' : '';
508
509 return sprintf(
510 '<div class="%s">
511 <div class="flex items-start">
512 <div class="flex items-center h-5">
513 <input type="checkbox"
514 id="%s"
515 name="%s"
516 value="1"
517 class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
518 %s>
519 </div>
520 <div class="ml-3 text-sm">
521 <label for="%s" class="font-medium text-gray-700">%s</label>
522 %s
523 </div>
524 </div>
525 </div>',
526 esc_attr($grid_cols),
527 esc_attr($id),
528 esc_attr($name),
529 $checked,
530 esc_attr($id),
531 esc_html($label),
532 $description_html
533 );
534 }
535
536 /**
537 * Render a notice field
538 *
539 * @since 1.0.0
540 * @param array $field Field configuration
541 * @param mixed $invoice Invoice object or null
542 * @return string Rendered HTML
543 */
544 private function renderNoticeField(array $field, $invoice = null): string {
545 $label = $field['label'] ?? '';
546 $description = $field['description'] ?? '';
547 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-6';
548 $notice_type = $field['notice_type'] ?? 'info';
549
550 // Set notice colors based on type
551 $notice_classes = [
552 'info' => 'bg-blue-50 border-blue-200 text-blue-700',
553 'warning' => 'bg-yellow-50 border-yellow-200 text-yellow-700',
554 'error' => 'bg-red-50 border-red-200 text-red-700',
555 'success' => 'bg-green-50 border-green-200 text-green-700'
556 ];
557
558 $notice_class = $notice_classes[$notice_type] ?? $notice_classes['info'];
559
560 return sprintf(
561 '<div class="%s">
562 <div class="p-4 border rounded-md %s">
563 <div class="flex">
564 <div class="flex-shrink-0">
565 <i class="fas fa-exclamation-triangle text-yellow-400"></i>
566 </div>
567 <div class="ml-3">
568 <h3 class="text-sm font-medium">%s</h3>
569 <div class="mt-2 text-sm">
570 <p>%s</p>
571 </div>
572 </div>
573 </div>
574 </div>
575 </div>',
576 esc_attr($grid_cols),
577 esc_attr($notice_class),
578 esc_html($label),
579 esc_html($description)
580 );
581 }
582
583 /**
584 * Render a payment gateways field
585 *
586 * @since 1.0.0
587 * @param array $field Field configuration
588 * @param mixed $invoice Invoice object or null
589 * @return string Rendered HTML
590 */
591 private function renderPaymentGatewaysField(array $field, $invoice = null): string {
592 $id = $field['id'] ?? '';
593 $name = $field['name'] ?? '';
594 $label = $field['label'] ?? '';
595 $description = $field['description'] ?? '';
596 $grid_cols = $field['grid_cols'] ?? 'sm:col-span-12';
597 $value = $this->getFieldValue($field, $invoice);
598
599 // Get enabled payment gateways from the main plugin instance
600 $payment_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
601 $enabled_gateways = $payment_manager->getEnabledGateways();
602
603 // Convert saved value to array if it's a string
604 $selected_gateways = [];
605 $has_custom_selection = false;
606 if (!empty($value)) {
607 if (is_string($value)) {
608 $selected_gateways = explode(',', $value);
609 } elseif (is_array($value)) {
610 $selected_gateways = $value;
611 }
612 $has_custom_selection = !empty($selected_gateways);
613 }
614
615 // Check if toggle state is enabled (either from selected gateways or toggle state field)
616 $toggle_enabled = $has_custom_selection;
617 if ($invoice && is_callable([$invoice, 'getPaymentGatewaysToggleState'])) {
618 $toggle_state = $invoice->getPaymentGatewaysToggleState();
619 if ($toggle_state === '1') {
620 $toggle_enabled = true;
621 }
622 }
623
624 // Section heading with icon (fixed size) - will be shown/hidden via JavaScript
625 $section_heading = '<div id="' . esc_attr($id) . '_heading" class="section-heading hidden"><svg class="gateway-icon text-indigo-500" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="10" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><path d="M2 11h20" stroke="currentColor" stroke-width="2"/></svg><span class="text-lg font-semibold text-gray-800">' . esc_html($label) . '</span></div>';
626
627 $description_html = '';
628 if ($description) {
629 $description_html = '<p class="text-gray-500 mt-1 text-xs">' . esc_html($description) . '</p>';
630 }
631
632 $gateways_html = '';
633 if (empty($enabled_gateways)) {
634 $gateways_html = '<div class="no-gateways-notice">' . __('No payment gateways are currently enabled. Please enable gateways in the plugin settings.', 'easy-invoice') . '</div>';
635 } else {
636 // Show current status only when toggle is on and gateways are selected
637 if ($has_custom_selection) {
638 $gateway_names = array_map(function($gateway) {
639 return $gateway->getTitle();
640 }, $enabled_gateways);
641 $gateways_html .= '<div class="status-box">';
642 $gateways_html .= '<svg class="gateway-icon text-yellow-500" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 17v.01"/><path d="M12 13v-4"/><circle cx="12" cy="12" r="10"/></svg>';
643 $selected_names = array_map(function($gateway) use ($enabled_gateways) {
644 return $enabled_gateways[$gateway]->getTitle();
645 }, $selected_gateways);
646 $gateways_html .= '<div class="status-content"><span class="main"><strong>' . __('Restricted to:', 'easy-invoice') . '</strong> ' . esc_html(implode(', ', $selected_names)) . '</span></div>';
647 $gateways_html .= '</div>';
648 }
649
650 // Switch-style toggle for custom selection - improved visibility and design
651 $gateways_html .= '<div class="toggle-container">';
652 $gateways_html .= '<label class="toggle-row">';
653 $gateways_html .= '<span class="toggle-label">' . __('Choose specific payment methods', 'easy-invoice') . '</span>';
654 $gateways_html .= '<div class="toggle-switch">';
655 $gateways_html .= '<input type="checkbox" id="payment_gateways_toggle" class="toggle-checkbox" ' . ($toggle_enabled ? 'checked' : '') . '/>';
656 $gateways_html .= '<span class="toggle-slider"></span>';
657 $gateways_html .= '</div>';
658 $gateways_html .= '<span class="toggle-help" title="' . esc_attr(__('Enable to select which payment methods to offer for this invoice', 'easy-invoice')) . '">?</span>';
659 $gateways_html .= '</label>';
660 $gateways_html .= '<div class="toggle-description">' . __('By default, all payment methods are available. Enable this option to select specific methods - unchecked items will not appear during payment.', 'easy-invoice') . '</div>';
661 $gateways_html .= '</div>';
662
663 // Hidden field for selected gateways
664 $initial_gateways_value = $has_custom_selection ? implode(',', $selected_gateways) : '';
665 $gateways_html .= '<input type="hidden" name="payment_gateways_hidden" value="' . esc_attr($initial_gateways_value) . '" id="payment_gateways_hidden">';
666
667 // Gateway cards (no icon, minimal, modern) - only show when toggle is enabled
668 $gateways_html .= '<div id="' . esc_attr($id) . '_selection" class="gateway-cards hidden">';
669 foreach ($enabled_gateways as $gateway_id => $gateway) {
670 $checked = in_array($gateway_id, $selected_gateways) ? 'checked' : '';
671 $is_checked = in_array($gateway_id, $selected_gateways);
672 $gateway_title = $gateway->getTitle();
673 $gateway_description = $gateway->getDescription();
674 $is_available = $gateway->isAvailable();
675 $availability_badge = $is_available ? '' : '<span class="badge">' . __('Configuration required', 'easy-invoice') . '</span>';
676 $card_classes = 'gateway-card';
677 if ($is_checked) {
678 $card_classes .= ' checked';
679 }
680 if (!$is_available) {
681 $card_classes .= ' opacity-60';
682 }
683 $gateways_html .= sprintf(
684 '<label class="%s" onclick="toggleGatewayCard(this)">
685 <input type="checkbox" id="%s_%s" name="%s[]" value="%s" %s>
686 <span class="custom-checkbox">'
687 . '<svg viewBox="0 0 20 20"><polyline points="4 11 8 15 16 6" fill="none" stroke="currentColor" stroke-width="2"/></svg>'
688 . '</span>' .
689 '<div class="gateway-card-content">'
690 . '<span class="gateway-title">%s</span>'
691 . '%s'
692 . '<span class="gateway-desc">%s</span>'
693 . '</div>' .
694 '</label>',
695 esc_attr($card_classes),
696 esc_attr($id),
697 esc_attr($gateway_id),
698 esc_attr($name),
699 esc_attr($gateway_id),
700 $checked,
701 esc_html($gateway_title),
702 $availability_badge,
703 esc_html($gateway_description)
704 );
705 }
706 $gateways_html .= '</div>';
707
708 // Add JavaScript to handle toggle and card clicks
709 $gateways_html .= '<script>
710 document.addEventListener("DOMContentLoaded", function() {
711 const toggle = document.getElementById("payment_gateways_toggle");
712 const selection = document.getElementById("' . esc_js($id) . '_selection");
713 const heading = document.getElementById("' . esc_js($id) . '_heading");
714 const toggleStateField = document.getElementById("payment_gateways_toggle_state");
715 const gatewaysHiddenField = document.getElementById("payment_gateways_hidden");
716 const checkboxes = selection.querySelectorAll("input[type=checkbox]");
717
718 function updateGatewaysHidden() {
719 const selectedValues = [];
720 checkboxes.forEach(function(checkbox) {
721 if (checkbox.checked) {
722 selectedValues.push(checkbox.value);
723 }
724 });
725 gatewaysHiddenField.value = selectedValues.join(",");
726 }
727
728 // Toggle functionality
729 toggle.addEventListener("change", function() {
730 if (this.checked) {
731 heading.classList.remove("hidden");
732 selection.classList.remove("hidden");
733 checkboxes.forEach(function(checkbox) {
734 checkbox.checked = true;
735 checkbox.disabled = false;
736 checkbox.closest(".gateway-card").classList.add("checked");
737 });
738 toggleStateField.value = "1";
739 } else {
740 heading.classList.add("hidden");
741 selection.classList.add("hidden");
742 checkboxes.forEach(function(checkbox) {
743 checkbox.checked = false;
744 checkbox.disabled = true;
745 checkbox.closest(".gateway-card").classList.remove("checked");
746 });
747 toggleStateField.value = "0";
748 }
749 updateGatewaysHidden();
750 });
751
752 // On load, always disable checkboxes initially
753 checkboxes.forEach(function(checkbox) {
754 checkbox.disabled = true;
755 });
756
757 // If toggle is already checked (from saved data), enable the cards and heading
758 if (toggle.checked) {
759 heading.classList.remove("hidden");
760 selection.classList.remove("hidden");
761 checkboxes.forEach(function(checkbox) {
762 checkbox.disabled = false;
763 });
764 // Restore saved gateway selections instead of checking all
765 const savedGateways = gatewaysHiddenField.value;
766 if (savedGateways) {
767 const savedGatewayArray = savedGateways.split(",");
768 checkboxes.forEach(function(checkbox) {
769 if (savedGatewayArray.includes(checkbox.value)) {
770 checkbox.checked = true;
771 checkbox.closest(".gateway-card").classList.add("checked");
772 } else {
773 checkbox.checked = false;
774 checkbox.closest(".gateway-card").classList.remove("checked");
775 }
776 });
777 }
778 }
779 updateGatewaysHidden();
780
781 // Handle card clicks
782 window.toggleGatewayCard = function(cardElement) {
783 const checkbox = cardElement.querySelector("input[type=checkbox]");
784 if (checkbox && !checkbox.disabled) {
785 checkbox.checked = !checkbox.checked;
786 if (checkbox.checked) {
787 cardElement.classList.add("checked");
788 } else {
789 cardElement.classList.remove("checked");
790 }
791 updateGatewaysHidden();
792 }
793 };
794
795 // Handle form submission to ensure proper data
796 const form = toggle.closest("form");
797 if (form) {
798 form.addEventListener("submit", function() {
799 if (!toggle.checked) {
800 checkboxes.forEach(function(checkbox) {
801 checkbox.checked = false;
802 checkbox.disabled = true;
803 });
804 toggleStateField.value = "0";
805 } else {
806 checkboxes.forEach(function(checkbox) {
807 checkbox.disabled = false;
808 });
809 toggleStateField.value = "1";
810 }
811 updateGatewaysHidden();
812 });
813 }
814 });
815 </script>';
816 }
817
818 return sprintf(
819 '<div class="%s">
820 %s
821 <div class="mt-3">
822 %s
823 %s
824 </div>
825 </div>',
826 esc_attr($grid_cols),
827 $section_heading,
828 $description_html,
829 $gateways_html
830 );
831 }
832
833 /**
834 * Get field value from invoice or default
835 *
836 * @since 1.0.0
837 * @param array $field Field configuration
838 * @param mixed $invoice Invoice object or null
839 * @return string Field value
840 */
841 private function getFieldValue(array $field, $invoice = null): string {
842 $name = $field['name'] ?? '';
843 $default_value = $field['default_value'] ?? $field['value'] ?? '';
844
845 // Handle callable default values
846 if (is_callable($default_value)) {
847 $default_value = $default_value();
848 }
849
850 // Allow Pro plugins to populate field values from meta data
851 $field = apply_filters('easy_invoice_populate_field_values', $field, $invoice);
852
853 if (is_array($invoice)) {
854 // Handle different array structures
855 if (isset($invoice[$name])) {
856 $value = $invoice[$name];
857 } elseif (isset($invoice['default_values'][$name])) {
858 $value = $invoice['default_values'][$name];
859 } else {
860 $value = $default_value;
861 }
862 } else {
863 if ($invoice) {
864 // First check if the field has a populated value from the hook
865 if (isset($field['value']) && $field['value'] !== null && $field['value'] !== '') {
866 $value = $field['value'];
867 } else {
868 // Use dynamic property access (magic __get method)
869 if (method_exists($invoice, '__get') || property_exists($invoice, $name)) {
870 $value = $invoice->$name;
871 } else {
872 // Fallback to dynamic getter method
873 $getter_method = 'get' . easy_invoice_str_replace(' ', '', ucwords(easy_invoice_str_replace(['-', '_'], ' ', $name)));
874 if (method_exists($invoice, $getter_method)) {
875 $value = $invoice->$getter_method();
876 } else {
877 $value = $default_value;
878 }
879 }
880
881 // Special handling for currency fields - use raw values for form display
882 if ($name === 'currency_code') {
883 if (method_exists($invoice, 'getRawCurrencyCode')) {
884 return $invoice->getRawCurrencyCode();
885 }
886 return $value ?? 'global';
887 }
888
889 if ($name === 'currency_position') {
890 if (method_exists($invoice, 'getRawCurrencyPosition')) {
891 return $invoice->getRawCurrencyPosition();
892 }
893 return $value ?? 'global';
894 }
895
896 // Special handling for payment_gateways field
897 if ($name === 'payment_gateways') {
898 if (is_array($value)) {
899 return implode(',', $value);
900 }
901 return (string) $value;
902 }
903
904 if (empty($value) && $value !== '0') {
905 $value = $default_value;
906 }
907 }
908 } else {
909 $value = $default_value;
910 }
911 }
912
913 return (string) ($value ?? '');
914 }
915 }