PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.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 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.3.2, at includes/Forms/FieldRenderer.php

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