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/settings-page.php +374 -165 2.1.21 → 2.4.4 View file →
@@ -11,8 +11,98 @@
11 11 exit;
12 12 }
13 13
14 14 /**
15 + * Single source of truth for per-section icons.
16 + *
17 + * Used by BOTH the left-side section nav (small icons, h-5 w-5) AND the
18 + * right-side content-panel header (larger icon, h-6 w-6 inside an
19 + * indigo-100 background tile). The nav previously had its own inline
20 + * SVGs and the header used Font Awesome — different glyphs for the
21 + * same section. This helper unifies them so changing the icon for a
22 + * section is a one-line edit here.
23 + *
24 + * @param string $section_id Section slug (company / invoice / quote / etc.)
25 + * @param string $color_class Tailwind classes to apply to the SVG (color + hover).
26 + * @param string $size_class Tailwind size classes (h-5 w-5 for nav, h-6 w-6 for header).
27 + * @param string $extra_class Any extra wrapper classes (e.g. mr-3 for nav alignment).
28 + */
29 +if (!function_exists('easy_invoice_render_settings_section_icon')) {
30 + function easy_invoice_render_settings_section_icon(string $section_id, string $color_class, string $size_class, string $extra_class = ''): void {
31 + $klass = trim($color_class . ' ' . $size_class . ' ' . $extra_class);
32 + $open = '<svg class="' . esc_attr($klass) . '" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">';
33 + $close = '</svg>';
34 +
35 + $paths = '';
36 + switch ($section_id) {
37 + case 'company':
38 + // House icon — represents the business / company.
39 + $paths = '<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
40 + . '<polyline points="9,22 9,12 15,12 15,22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
41 + break;
42 + case 'invoice':
43 + // Document with $ inside — distinct from the quote icon.
44 + $paths = '<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
45 + . '<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
46 + . '<path d="M12 11V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
47 + . '<path d="M14.5 13H10.75a1.75 1.75 0 0 0 0 3.5h2.5a1.75 1.75 0 0 1 0 3.5H9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
48 + break;
49 + case 'quote':
50 + // Document with checkmark — proposal / awaiting approval.
51 + $paths = '<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
52 + . '<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
53 + . '<path d="M8.5 15.5L11 18L16 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
54 + break;
55 + case 'currency':
56 + // Dollar-sign-in-circle.
57 + $paths = '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
58 + . '<path d="M12 1V23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
59 + . '<path d="M17 5H9.5C8.57174 5 7.6815 5.36875 7.02513 6.02513C6.36875 6.6815 6 7.57174 6 8.5C6 9.42826 6.36875 10.3185 7.02513 10.9749C7.6815 11.6313 8.57174 12 9.5 12H14.5C15.4283 12 16.3185 12.3687 16.9749 13.0251C17.6313 13.6815 18 14.5717 18 15.5C18 16.4283 17.6313 17.3185 16.9749 17.9749C16.3185 18.6313 15.4283 19 14.5 19H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
60 + break;
61 + case 'tax':
62 + // Percent symbol — clean tax glyph.
63 + $paths = '<line x1="19" y1="5" x2="5" y2="19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
64 + . '<circle cx="6.5" cy="6.5" r="2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
65 + . '<circle cx="17.5" cy="17.5" r="2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
66 + break;
67 + case 'payment':
68 + // Card with horizontal stripe.
69 + $paths = '<rect x="1" y="4" width="22" height="16" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
70 + . '<line x1="1" y1="10" x2="23" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
71 + break;
72 + case 'email':
73 + // Envelope with flap.
74 + $paths = '<path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
75 + . '<polyline points="22,6 12,13 2,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
76 + break;
77 + case 'text_settings':
78 + // Document with text lines + edit-arrow indicator.
79 + $paths = '<path d="M4 7V4C4 3.44772 4.44772 3 5 3H19C19.5523 3 20 3.44772 20 4V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
80 + . '<path d="M4 7V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
81 + . '<path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
82 + . '<path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
83 + . '<path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
84 + . '<path d="M12 11L14 13L12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
85 + break;
86 + case 'account':
87 + // User in circle — distinct from the gear used elsewhere.
88 + $paths = '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
89 + . '<circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
90 + . '<path d="M6.34 19a6 6 0 0 1 11.32 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
91 + break;
92 + case 'advanced':
93 + default:
94 + // Gear icon — used by Advanced + as the fallback for any
95 + // section that didn't ship a case here.
96 + $paths = '<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
97 + . '<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
98 + break;
99 + }
100 + echo $open . $paths . $close; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- inline SVG icon markup assembled from literal path data above.
101 + }
102 +}
103 +
104 +/**
15 105 * Helper function to render a single settings field.
16 106 *
17 107 * @param string $option_key The option key.
18 108 * @param array $field_config The field configuration.
@@ -30,18 +120,18 @@
30 120 $placeholder = isset($field_config['placeholder']) ? esc_attr($field_config['placeholder']) : '';
31 121 $required = !empty($field_config['required']) ? 'required' : '';
32 122 $aria_describedby = !empty($description_text) ? 'aria-describedby="' . $field_id . '-description"' : '';
33 123
34 - echo '<div class=" ' . $col_span_class . '">'; // Added 'form-group' class for JS show/hide functionality
124 + echo '<div class=" ' . esc_html($col_span_class) . '">'; // Added 'form-group' class for JS show/hide functionality
35 125
36 126 // Special layout for checkboxes with descriptions (e.g., Invoice Numbering type)
37 127 if ($type === 'checkbox' && !empty($description_text) && ($option_key === 'easy_invoice_invoice_numbering')) {
38 128 echo '<div class="flex items-start space-x-3 p-4 bg-gray-50 rounded-lg border border-gray-200">';
39 129 echo ' <div class="flex items-center h-5 mt-0.5">';
40 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" ' . $required . '>';
130 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" ' . esc_html($required) . '>';
41 131 echo ' </div>';
42 132 echo ' <div class="flex-1">';
43 - echo ' <label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-1">' . $label . '</label>';
133 + echo ' <label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-1">' . esc_html($label) . '</label>';
44 134 echo ' <p id="' . $field_id . '-description" class="text-sm text-gray-400 leading-relaxed">' . wp_kses($description_text, array(
45 135 'a' => array(
46 136 'href' => array(),
47 137 'target' => array(),
@@ -52,33 +142,32 @@
52 142 echo '</div>';
53 143 } else {
54 144 // Standard label for other types, or checkboxes without the special description structure
55 145 if ($type !== 'checkbox') {
56 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-2">' . $label . '</label>';
146 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-2">' . esc_html($label) . '</label>';
57 147 }
58 148
59 149 $input_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';
60 150 $select_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';
61 151
62 -
63 152 switch ($type) {
64 153 case 'text':
65 154 case 'email':
66 155 case 'url':
67 156 case 'tel':
68 - echo '<input type="' . esc_attr($type) . '" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
157 + echo '<input type="' . esc_attr($type) . '" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
69 158 break;
70 159 case 'number':
71 160 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
72 161 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
73 162 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
74 - echo '<input type="number" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
163 + echo '<input type="number" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
75 164 break;
76 165 case 'textarea':
77 - echo '<textarea id="' . $field_id . '" name="' . $field_name . '" rows="3" class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>' . wp_kses_post($value) . '</textarea>';
166 + echo '<textarea id="' . $field_id . '" name="' . esc_html($field_name) . '" rows="3" class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>' . wp_kses_post($value) . '</textarea>';
78 167 break;
79 168 case 'select':
80 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
169 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
81 170 if (!empty($field_config['options']) && is_array($field_config['options'])) {
82 171 foreach ($field_config['options'] as $opt_val => $opt_label) {
83 172 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
84 173 }
@@ -85,25 +174,25 @@
85 174 }
86 175 echo '</select>';
87 176 break;
88 177 case 'multiselect':
89 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($label) . '">';
178 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($label) . '">';
90 179 if (!empty($field_config['options']) && is_array($field_config['options'])) {
91 180 foreach ($field_config['options'] as $opt_val => $opt_label) {
92 181 $selected = is_array($value) && in_array($opt_val, $value) ? 'selected="selected"' : '';
93 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
182 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
94 183 }
95 184 }
96 185 echo '</select>';
97 186 break;
98 187 case 'readonly':
99 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . $input_class . ' bg-gray-50" readonly ' . $aria_describedby . '>';
100 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($value) . '">';
188 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly ' . $aria_describedby . '>';
189 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($value) . '">';
101 190 break;
102 191 case 'checkbox': // Checkboxes that don't use the special description layout
103 192 echo '<div class="flex items-center">'; // Original 'tax_enabled' was just this simple structure
104 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" ' . $required . ' ' . $aria_describedby . '>';
105 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $label . '</label>';
193 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" ' . esc_html($required) . ' ' . $aria_describedby . '>';
194 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($label) . '</label>';
106 195 echo '</div>';
107 196 if (!empty($description_text)) { // Show description below the field
108 197 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . wp_kses($description_text, array(
109 198 'a' => array(
@@ -116,14 +205,14 @@
116 205 break;
117 206 case 'image':
118 207 echo '<div class="mt-1 flex items-center">';
119 208 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
120 - echo ' <img id="' . $field_id . '-preview" src="' . esc_url($value) . '" alt="' . esc_attr($label) . '" class="h-full w-full object-cover' . (empty($value) ? ' hidden' : '') . '">';
209 + echo ' <img id="' . $field_id . '-preview"' . (empty($value) ? '' : ' src="' . esc_url($value) . '"') . ' alt="' . esc_attr($label) . '" class="h-full w-full object-cover' . (empty($value) ? ' hidden' : '') . '">';
121 210 echo ' </span>';
122 211 echo ' <button type="button" id="upload_image_button_' . $field_id . '" class="upload-logo-button ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-uploader_title="' . esc_attr__('Choose Logo', 'easy-invoice') . '" data-uploader_button_text="' . esc_attr__('Select Logo', 'easy-invoice') . '">';
123 212 echo esc_html__('Change', 'easy-invoice');
124 213 echo '</button>';
125 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" ' . $required . ' ' . $aria_describedby . '>';
214 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
126 215 echo '</div>';
127 216 break;
128 217 case 'wp_editor':
129 218 // For wp_editor, the label is usually handled before calling it.
@@ -152,15 +241,15 @@
152 241 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
153 242 echo '</div>';
154 243 break;
155 244 case 'color':
156 - echo '<input type="color" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="h-10 w-20 border border-gray-300 rounded">';
245 + echo '<input type="color" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="h-10 w-20 border border-gray-300 rounded">';
157 246 break;
158 247 case 'range':
159 248 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
160 249 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
161 250 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
162 - echo '<input type="range" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="w-full">';
251 + echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="w-full">';
163 252 echo '<span class="text-sm text-gray-400">' . esc_html($value) . '%</span>';
164 253 break;
165 254 case 'button':
166 255 $button_text = $field_config['button_text'] ?? $label;
@@ -168,9 +257,9 @@
168 257 $button_class_name = 'regenerate-invoice-numbers-button';
169 258 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
170 259 $button_class_name = 'regenerate-quote-numbers-button';
171 260 }
172 - echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . $button_class_name . '">';
261 + echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
173 262 echo esc_html($button_text);
174 263 echo '</button>';
175 264 break;
176 265 case 'image_upload':
@@ -175,9 +264,9 @@
175 264 break;
176 265 case 'image_upload':
177 266 echo '<div class="image-upload-wrap">';
178 267 echo '<div class="flex items-center space-x-3">';
179 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '">';
268 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '">';
180 269 echo ' <button type="button" class="upload-image-button bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-target="' . $field_id . '">';
181 270 echo esc_html__('Upload Image', 'easy-invoice');
182 271 echo '</button>';
183 272 echo '</div>';
@@ -202,9 +291,9 @@
202 291 echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' regenerate-quote-numbers-button">';
203 292 echo esc_html($button_text);
204 293 echo '</button>';
205 294 } else {
206 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
295 + echo '<input type="text" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
207 296 }
208 297 break;
209 298 }
210 299 // General description for non-checkbox and non-wp_editor fields (if it exists and not handled by special layouts)
@@ -226,9 +315,9 @@
226 315 * Note: This filter is already applied in the controller, so this is redundant
227 316 */
228 317 // $settings_config = apply_filters('easy_invoice_settings_fields_config', $settings_config);
229 318
230 -$active_section = $_GET['section'] ?? key($settings_config);
319 +$active_section = isset($_GET['section']) ? sanitize_key(wp_unslash($_GET['section'])) : key($settings_config); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
231 320 if (!isset($settings_config[$active_section])) {
232 321 $active_section = key($settings_config);
233 322 }
234 323 ?>
@@ -233,9 +322,9 @@
233 322 }
234 323 ?>
235 324
236 325 <div class="p-8">
237 - <!-- Header with Actions -->
326 +
238 327 <div class="ei-app-page-header bg-white border-b border-gray-200 px-6 easy-invoice-admin-page-header">
239 328 <div class="flex items-center justify-between">
240 329 <div>
241 330 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Settings', 'easy-invoice'); ?></h1>
@@ -249,12 +338,12 @@
249 338 </div>
250 339 </div>
251 340 </div>
252 341
253 - <!-- Main Content -->
342 +
254 343 <div class="max-w-7xl mx-auto">
255 344 <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
256 - <!-- Left Sidebar - Navigation -->
345 +
257 346 <div class="lg:col-span-1">
258 347 <div class="bg-white shadow-lg rounded-lg sticky top-24 border border-gray-200">
259 348 <nav class="space-y-2 p-4" aria-label="<?php esc_attr_e('Settings Navigation', 'easy-invoice'); ?>">
260 349 <?php foreach ($settings_config as $section_id => $section_data) : ?>
@@ -259,80 +348,23 @@
259 348 <nav class="space-y-2 p-4" aria-label="<?php esc_attr_e('Settings Navigation', 'easy-invoice'); ?>">
260 349 <?php foreach ($settings_config as $section_id => $section_data) : ?>
261 350 <div class="space-y-1">
262 351 <a href="?page=easy-invoice-settings&section=<?php echo esc_attr($section_id); ?>"
263 - class="nav-link group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 <?php echo ($section_id === $active_section) ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"
352 + class="nav-link group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 <?php echo ($section_id === $active_section) ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'; ?>"
264 353 aria-selected="<?php echo $section_id === $active_section ? 'true' : 'false'; ?>"
265 354 role="tab">
266 355
267 - <?php if ($section_id === 'company'): ?>
268 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
269 - <path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
270 - <polyline points="9,22 9,12 15,12 15,22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
271 - </svg>
272 - <?php elseif ($section_id === 'invoice'): ?>
273 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
274 - <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
275 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
276 - <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
277 - <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
278 - <path d="M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
279 - </svg>
280 - <?php elseif ($section_id === 'quote'): ?>
281 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
282 - <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
283 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
284 - <path d="M9 9H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
285 - <path d="M9 13H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
286 - <path d="M9 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
287 - </svg>
288 - <?php elseif ($section_id === 'currency'): ?>
289 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
290 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
291 - <path d="M12 1V23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
292 - <path d="M17 5H9.5C8.57174 5 7.6815 5.36875 7.02513 6.02513C6.36875 6.6815 6 7.57174 6 8.5C6 9.42826 6.36875 10.3185 7.02513 10.9749C7.6815 11.6313 8.57174 12 9.5 12H14.5C15.4283 12 16.3185 12.3687 16.9749 13.0251C17.6313 13.6815 18 14.5717 18 15.5C18 16.4283 17.6313 17.3185 16.9749 17.9749C16.3185 18.6313 15.4283 19 14.5 19H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
293 - </svg>
294 - <?php elseif ($section_id === 'tax'): ?>
295 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
296 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
297 - <path d="M8 14L16 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
298 - <path d="M8 8L16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
299 - <path d="M12 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
300 - </svg>
301 - <?php elseif ($section_id === 'payment'): ?>
302 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
303 - <rect x="1" y="4" width="22" height="16" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
304 - <line x1="1" y1="10" x2="23" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
305 - </svg>
306 - <?php elseif ($section_id === 'email'): ?>
307 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
308 - <path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
309 - <polyline points="22,6 12,13 2,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
310 - </svg>
311 - <?php elseif ($section_id === 'advanced'): ?>
312 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
313 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
314 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
315 - </svg>
316 - <?php elseif ($section_id === 'text_settings'): ?>
317 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
318 - <path d="M4 7V4C4 3.44772 4.44772 3 5 3H19C19.5523 3 20 3.44772 20 4V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
319 - <path d="M4 7V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
320 - <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
321 - <path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
322 - <path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
323 - <path d="M12 11L14 13L12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
324 - </svg>
325 - <?php else: ?>
326 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
327 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
328 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
329 - </svg>
330 - <?php endif; ?>
356 + <?php
357 + // Section nav icon — same source as the content
358 + // header below. See easy_invoice_render_settings_section_icon().
359 + $is_active = ($section_id === $active_section);
360 + $icon_color = $is_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500';
361 + easy_invoice_render_settings_section_icon($section_id, $icon_color, 'h-5 w-5', 'mr-3 flex-shrink-0');
362 + ?>
331 363
332 364 <span class="flex-1"><?php echo esc_html($section_data['title']); ?></span>
333 365 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
334 - <svg class="flex-shrink-0 h-4 w-4 text-gray-400 group-hover:text-gray-500 transition-transform duration-200" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
366 + <svg class="flex-shrink-0 h-4 w-4 text-gray-400 group-hover:text-indigo-500 transition-transform duration-200" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
335 367 <polyline points="6,9 12,15 18,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
336 368 </svg>
337 369 <?php endif; ?>
338 370 </a>
@@ -343,11 +375,11 @@
343 375 // Only show active subsection if we're actually on an email settings page
344 376 $active_email_subsection = null;
345 377 if ($active_section === 'email') {
346 378 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
347 - $active_email_subsection = $_GET['subsection'];
379 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
348 380 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
349 - $active_email_subsection = $_GET['subsection'];
381 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
350 382 } else {
351 383 // Default to 'general' only if we're on email section but no specific subsection
352 384 $active_email_subsection = 'general';
353 385 }
@@ -358,9 +390,9 @@
358 390 <a href="?page=easy-invoice-settings&section=email&subsection=<?php echo esc_attr($subsection_id); ?>"
359 391 class="email-submenu-link group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 <?php echo $is_active_subsection ? 'text-indigo-700 bg-indigo-50 border-l-2 border-indigo-500' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'; ?>"
360 392 data-subsection="<?php echo esc_attr($subsection_id); ?>"
361 393 data-section="<?php echo esc_attr($section_id); ?>">
362 - <svg class="mr-2 flex-shrink-0 h-4 w-4 <?php echo $is_active_subsection ? 'text-indigo-500' : 'text-gray-400 group-hover:text-gray-500'; ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
394 + <svg class="mr-2 flex-shrink-0 h-4 w-4 <?php echo $is_active_subsection ? 'text-indigo-500' : 'text-gray-400 group-hover:text-indigo-500'; ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
363 395 <polyline points="9,18 15,12 9,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
364 396 </svg>
365 397 <?php
366 398 // Display shorter names for better UX
@@ -393,9 +425,9 @@
393 425 </nav>
394 426 </div>
395 427 </div>
396 428
397 - <!-- Right Content - Settings -->
429 +
398 430 <div class="lg:col-span-2">
399 431 <form id="settings-form" method="post" action="">
400 432 <?php wp_nonce_field('easy_invoice_settings', 'easy_invoice_settings_nonce'); ?>
401 433 <input type="hidden" name="action" value="easy_invoice_save_settings">
@@ -407,20 +439,15 @@
407 439 aria-labelledby="tab-<?php echo esc_attr($section_id); ?>">
408 440 <div class="px-6 py-5 border-b border-gray-200">
409 441 <div class="flex items-center">
410 442 <div class="flex-shrink-0 bg-indigo-100 rounded-lg p-3">
411 - <?php if (isset($section_data['icon']) && !empty($section_data['icon'])): ?>
412 - <?php if (strpos($section_data['icon'], '<svg') === 0): ?>
413 - <?php echo $section_data['icon']; ?>
414 - <?php else: ?>
415 - <i class="<?php echo esc_attr($section_data['icon']); ?> text-indigo-600 text-xl"></i>
416 - <?php endif; ?>
417 - <?php else: ?>
418 - <svg class="text-indigo-600 text-xl h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
419 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
420 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
421 - </svg>
422 - <?php endif; ?>
443 + <?php
444 + // Content-header icon — render via the same
445 + // helper the section nav uses. Same SVG glyph,
446 + // bigger size (h-6 w-6) and the brand colour
447 + // sits on top of the indigo-100 tile.
448 + easy_invoice_render_settings_section_icon($section_id, 'text-indigo-600', 'h-6 w-6');
449 + ?>
423 450 </div>
424 451 <div class="ml-4">
425 452 <h2 class="text-lg font-medium text-gray-900"><?php echo esc_html($section_data['title']); ?></h2>
426 453 <p class="mt-1 text-sm text-gray-400"><?php echo esc_html($section_data['description']); ?></p>
@@ -433,11 +460,11 @@
433 460 <?php
434 461 // Determine the active subsection from the hash or default to 'general'
435 462 $active_email_subsection = 'general';
436 463 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
437 - $active_email_subsection = $_GET['subsection'];
464 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
438 465 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
439 - $active_email_subsection = $_GET['subsection'];
466 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
440 467 }
441 468 if (!isset($section_data['subsections'][$active_email_subsection])) {
442 469 $active_email_subsection = array_key_first($section_data['subsections']);
443 470 }
@@ -447,9 +474,9 @@
447 474 // Render all email subsections so JS can show/hide them
448 475 foreach ($section_data['subsections'] as $subsection_key => $subsection_data) {
449 476 $is_active = ($subsection_key === $active_email_subsection) ? '' : 'hidden';
450 477 ?>
451 - <div id="email--<?php echo esc_attr($subsection_key); ?>-content" class="email-subsection-content <?php echo $is_active; ?>">
478 + <div id="email--<?php echo esc_attr($subsection_key); ?>-content" class="email-subsection-content <?php echo esc_attr($is_active); ?>">
452 479 <?php
453 480 // Display subsection title
454 481 $subsection_title = '';
455 482 switch ($subsection_key) {
@@ -475,8 +502,55 @@
475 502 <?php if (!empty($subsection_data['description'])): ?>
476 503 <p class="text-sm text-gray-400"><?php echo esc_html($subsection_data['description']); ?></p>
477 504 <?php endif; ?>
478 505 </div>
506 + <?php
507 + // Optional per-subsection notice. Subsection configs may set
508 + // 'notice' => [
509 + // 'type' => 'info' | 'warning' | 'error' | 'success',
510 + // 'title' => string,
511 + // 'message' => string (plain text, may contain links rendered via 'cta_url' + 'cta_label'),
512 + // 'cta_url' => optional URL,
513 + // 'cta_label' => optional CTA button label,
514 + // ]
515 + // Used by PaymentReminderEmail to flag when Smart Reminders
516 + // has taken over so the user understands why the fields below
517 + // are inactive. Renders inline above the fields, not as a
518 + // global admin_notice, so it's anchored to the right section.
519 + if (!empty($subsection_data['notice']) && is_array($subsection_data['notice'])):
520 + $notice = $subsection_data['notice'];
521 + $notice_type = $notice['type'] ?? 'info';
522 + $palette = [
523 + 'info' => ['bg' => 'bg-blue-50', 'border' => 'border-blue-200', 'title' => 'text-blue-900', 'body' => 'text-blue-800', 'icon' => '#1d4ed8'],
524 + 'warning' => ['bg' => 'bg-amber-50', 'border' => 'border-amber-200', 'title' => 'text-amber-900', 'body' => 'text-amber-800', 'icon' => '#b45309'],
525 + 'error' => ['bg' => 'bg-red-50', 'border' => 'border-red-200', 'title' => 'text-red-900', 'body' => 'text-red-800', 'icon' => '#b91c1c'],
526 + 'success' => ['bg' => 'bg-green-50', 'border' => 'border-green-200', 'title' => 'text-green-900', 'body' => 'text-green-800', 'icon' => '#15803d'],
527 + ];
528 + $c = $palette[$notice_type] ?? $palette['info'];
529 + ?>
530 + <div class="rounded-md <?php echo esc_attr($c['bg'] . ' border ' . $c['border']); ?> p-4 mb-6">
531 + <div class="flex">
532 + <div class="flex-shrink-0">
533 + <span class="dashicons dashicons-info-outline" style="color: <?php echo esc_attr($c['icon']); ?>;"></span>
534 + </div>
535 + <div class="ml-3 flex-1">
536 + <?php if (!empty($notice['title'])): ?>
537 + <h4 class="text-sm font-semibold <?php echo esc_attr($c['title']); ?>"><?php echo esc_html($notice['title']); ?></h4>
538 + <?php endif; ?>
539 + <?php if (!empty($notice['message'])): ?>
540 + <p class="mt-1 text-sm <?php echo esc_attr($c['body']); ?>"><?php echo wp_kses_post($notice['message']); ?></p>
541 + <?php endif; ?>
542 + <?php if (!empty($notice['cta_url']) && !empty($notice['cta_label'])): ?>
543 + <p class="mt-3">
544 + <a href="<?php echo esc_url($notice['cta_url']); ?>" class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700">
545 + <?php echo esc_html($notice['cta_label']); ?>
546 + </a>
547 + </p>
548 + <?php endif; ?>
549 + </div>
550 + </div>
551 + </div>
552 + <?php endif; ?>
479 553 <div class="space-y-6">
480 554 <?php if (!empty($subsection_data['fields'])) {
481 555 foreach ($subsection_data['fields'] as $option_key => $field_config) {
482 556 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
@@ -490,9 +564,9 @@
490 564 $select_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';
491 565 $col_span_class = esc_attr($field_config['col_span'] ?? 'sm:col-span-6');
492 566 echo '<div class="field-wrapper form-group">';
493 567 if ($field_type !== 'checkbox') {
494 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . $field_label . '</label>';
568 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
495 569 }
496 570
497 571 // Define required and aria_describedby variables
498 572 $required = !empty($field_config['required']) ? 'required' : '';
@@ -502,21 +576,21 @@
502 576 case 'text':
503 577 case 'email':
504 578 case 'url':
505 579 case 'tel':
506 - echo '<input type="' . esc_attr($field_type) . '" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
580 + echo '<input type="' . esc_attr($field_type) . '" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
507 581 break;
508 582 case 'number':
509 583 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
510 584 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
511 585 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
512 - echo '<input type="number" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
586 + echo '<input type="number" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
513 587 break;
514 588 case 'textarea':
515 - echo '<textarea id="' . $field_id . '" name="' . $field_name . '" rows="3" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
589 + echo '<textarea id="' . $field_id . '" name="' . esc_html($field_name) . '" rows="3" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
516 590 break;
517 591 case 'select':
518 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
592 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
519 593 if (!empty($field_config['options']) && is_array($field_config['options'])) {
520 594 foreach ($field_config['options'] as $opt_val => $opt_label) {
521 595 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
522 596 }
@@ -523,25 +597,25 @@
523 597 }
524 598 echo '</select>';
525 599 break;
526 600 case 'multiselect':
527 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
601 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
528 602 if (!empty($field_config['options']) && is_array($field_config['options'])) {
529 603 foreach ($field_config['options'] as $opt_val => $opt_label) {
530 604 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
531 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
605 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
532 606 }
533 607 }
534 608 echo '</select>';
535 609 break;
536 610 case 'readonly':
537 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . ' bg-gray-50" readonly>';
538 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($current_value) . '">';
611 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly>';
612 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($current_value) . '">';
539 613 break;
540 614 case 'checkbox':
541 615 echo '<div class="flex items-center">';
542 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
543 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $field_label . '</label>';
616 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
617 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($field_label) . '</label>';
544 618 echo '</div>';
545 619 if (!empty($field_description)) {
546 620 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
547 621 }
@@ -548,14 +622,14 @@
548 622 break;
549 623 case 'image':
550 624 echo '<div class="mt-1 flex items-center">';
551 625 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
552 - echo ' <img id="' . $field_id . '-preview" src="' . esc_url($current_value) . '" alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
626 + echo ' <img id="' . $field_id . '-preview"' . (empty($current_value) ? '' : ' src="' . esc_url($current_value) . '"') . ' alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
553 627 echo ' </span>';
554 628 echo ' <button type="button" id="upload_image_button_' . $field_id . '" class="upload-logo-button ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-uploader_title="' . esc_attr__('Choose Logo', 'easy-invoice') . '" data-uploader_button_text="' . esc_attr__('Select Logo', 'easy-invoice') . '">';
555 629 echo esc_html__('Change', 'easy-invoice');
556 630 echo '</button>';
557 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
631 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
558 632 echo '</div>';
559 633 break;
560 634 case 'wp_editor':
561 635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
@@ -560,9 +634,9 @@
560 634 case 'wp_editor':
561 635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
562 636 // Handle description for wp_editor specifically
563 637 if (!empty($field_description)) {
564 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
638 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
565 639 }
566 640 break;
567 641 case 'test_email':
568 642 echo '<div class="mt-1">';
@@ -581,10 +655,10 @@
581 655 $is_payment_test = ($field_id === 'easy_invoice_payment_test_template');
582 656 $width_class = $is_payment_test ? 'w-[200px]' : 'w-64';
583 657 echo '<div class="mt-1">';
584 658 echo ' <div class="flex items-center space-x-3">';
585 - echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block ' . $width_class . ' 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" value="' . esc_attr(get_option('admin_email')) . '">';
586 - echo ' <button type="button" id="' . $field_id . '_button" class="test-template-email-button inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[38px]" data-template-type="' . esc_attr($template_type) . '">';
659 + echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block ' . esc_html($width_class) . ' 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" value="' . esc_attr(get_option('admin_email')) . '">';
660 + echo ' <button type="button" id="' . $field_id . '_button" data-template-type="' . esc_attr($field_config['template_type'] ?? '') . '" class="test-template-email-button inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[38px]" data-template-type="' . esc_attr($template_type) . '">';
587 661 echo ' <i class="fas fa-envelope mr-2" aria-hidden="true"></i>';
588 662 echo ' ' . esc_html__('Test Template', 'easy-invoice');
589 663 echo ' </button>';
590 664 echo ' </div>';
@@ -603,13 +677,13 @@
603 677 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
604 678 echo '</div>';
605 679 break;
606 680 default:
607 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '">';
681 + echo '<input type="text" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '">';
608 682 break;
609 683 }
610 684 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
611 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
685 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
612 686 }
613 687 echo '</div>'; // Close the field wrapper div
614 688 }
615 689 }
@@ -622,9 +696,9 @@
622 696 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
623 697 <p class="text-sm font-medium text-blue-900 mb-3"><?php esc_html_e('Available Placeholders:', 'easy-invoice'); ?></p>
624 698 <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
625 699 <?php if (in_array($subsection_key, ['invoice_available', 'payment_received', 'payment_reminder'])): ?>
626 - <!-- Invoice-specific placeholders -->
700 +
627 701 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_number}}</div>
628 702 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{due_date}}</div>
629 703 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_url}}</div>
630 704 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_url}}</div>
@@ -630,15 +704,15 @@
630 704 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_url}}</div>
631 705 <?php endif; ?>
632 706
633 707 <?php if (in_array($subsection_key, ['quote_available'])): ?>
634 - <!-- Quote-specific placeholders -->
708 +
635 709 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_number}}</div>
636 710 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{expiry_date}}</div>
637 711 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_url}}</div>
638 712 <?php endif; ?>
639 713
640 - <!-- Common placeholders for all templates -->
714 +
641 715 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_name}}</div>
642 716 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_first_name}}</div>
643 717 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_last_name}}</div>
644 718 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_email}}</div>
@@ -644,8 +718,9 @@
644 718 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_email}}</div>
645 719 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_address}}</div>
646 720 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_name}}</div>
647 721 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{total_amount}}</div>
722 + <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{amount_due}}</div>
648 723 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{issue_date}}</div>
649 724 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_email}}</div>
650 725 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_phone}}</div>
651 726 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_terms}}</div>
@@ -663,19 +738,149 @@
663 738 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
664 739 <div class="flex items-center">
665 740 <i class="fas fa-info-circle text-blue-500 mr-2"></i>
666 741 <p class="text-sm text-blue-700">
667 - <?php _e('Drag and drop payment methods to reorder them. The order will be reflected on payment forms.', 'easy-invoice'); ?>
742 + <?php esc_html_e('Drag and drop payment methods to reorder them. The order will be reflected on payment forms.', 'easy-invoice'); ?>
668 743 </p>
669 744 </div>
670 745 </div>
671 746 <?php endif; ?>
672 747
748 + <?php
749 + /**
750 + * Pro-gateway teaser block (renders only when Pro is NOT active).
751 + *
752 + * Showing the names of Pro gateways here drives conversion — users
753 + * see exactly what they unlock without leaving the settings page.
754 + * Each row links to the pricing page in a new tab. Free gateways
755 + * (PayPal, Manual) remain enabled below this block as usual.
756 + */
757 + if (!easy_invoice_has_pro()) :
758 + $pro_gateway_teasers = [
759 + 'stripe' => [
760 + 'title' => __('Stripe', 'easy-invoice'),
761 + 'desc' => __('Cards, Apple Pay, Google Pay, Link. SCA / 3-D Secure built in.', 'easy-invoice'),
762 + ],
763 + 'square' => [
764 + 'title' => __('Square', 'easy-invoice'),
765 + 'desc' => __('US / CA / UK / AU / JP card processing with webhook reconciliation.', 'easy-invoice'),
766 + ],
767 + 'authorizenet' => [
768 + 'title' => __('Authorize.Net', 'easy-invoice'),
769 + 'desc' => __('Long-standing US gateway — credit cards + eChecks via AIM.', 'easy-invoice'),
770 + ],
771 + 'mollie' => [
772 + 'title' => __('Mollie', 'easy-invoice'),
773 + 'desc' => __('Europe-friendly: SEPA, iDEAL, Bancontact, Sofort, Klarna, card.', 'easy-invoice'),
774 + ],
775 + 'paystack' => [
776 + 'title' => __('Paystack', 'easy-invoice'),
777 + 'desc' => __('Africa-first (NGN / GHS / ZAR / KES / USD): card, bank, USSD, mobile money, QR.', 'easy-invoice'),
778 + ],
779 + 'moneris' => [
780 + 'title' => __('Moneris', 'easy-invoice'),
781 + 'desc' => __('Canada\'s largest card processor — Visa, Mastercard, AMEX, Interac.', 'easy-invoice'),
782 + ],
783 + 'bank_transfer'=> [
784 + 'title' => __('Bank Transfer', 'easy-invoice'),
785 + 'desc' => __('Show your IBAN / SWIFT on invoices; mark paid on wire arrival.', 'easy-invoice'),
786 + ],
787 + 'cheque' => [
788 + 'title' => __('Cheque', 'easy-invoice'),
789 + 'desc' => __('Display your mailing address; mark paid when the cheque clears.', 'easy-invoice'),
790 + ],
791 + 'cash' => [
792 + 'title' => __('Cash', 'easy-invoice'),
793 + 'desc' => __('In-person cash with custom collection instructions.', 'easy-invoice'),
794 + ],
795 + ];
796 +
797 + // Hide the ones we already register in Free / are already shown above.
798 + $already_registered = array_keys($all_gateways_sorted);
799 + foreach ($already_registered as $id) {
800 + unset($pro_gateway_teasers[$id]);
801 + }
802 + ?>
803 + <?php if (!empty($pro_gateway_teasers)): ?>
804 + <div class="ei-pro-gateways mt-2 mb-6 rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden">
805 +
806 + <div class="flex items-center justify-between px-5 py-4 border-b border-gray-100 bg-gradient-to-r from-indigo-50/60 to-transparent">
807 + <div class="flex items-center min-w-0">
808 + <span class="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 text-white shadow-sm mr-3 flex-shrink-0">
809 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
810 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
811 + </svg>
812 + </span>
813 + <div class="min-w-0">
814 + <h3 class="text-sm font-semibold text-gray-900 truncate">
815 + <?php
816 + printf(
817 + /* translators: %d is the number of additional gateways */
818 + esc_html__('Unlock %d more payment gateways with Easy Invoice Pro', 'easy-invoice'),
819 + count($pro_gateway_teasers)
820 + );
821 + ?>
822 + </h3>
823 + <p class="mt-0.5 text-xs text-gray-500"><?php esc_html_e('Stripe, Square, Mollie, Paystack and more — installed alongside PayPal and Manual.', 'easy-invoice'); ?></p>
824 + </div>
825 + </div>
826 + <a href="https://matrixaddons.com/plugins/easy-invoice/#pricing" target="_blank" rel="noopener"
827 + class="ml-4 shrink-0 inline-flex items-center px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-700 transition-colors">
828 + <?php esc_html_e('View pricing', 'easy-invoice'); ?>
829 + <svg class="w-3 h-3 ml-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
830 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/>
831 + </svg>
832 + </a>
833 + </div>
834 +
835 +
836 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 divide-y sm:divide-y-0 sm:divide-x divide-gray-100 lg:divide-x">
837 + <?php
838 + $teaser_idx = 0;
839 + $teaser_count = count($pro_gateway_teasers);
840 + foreach ($pro_gateway_teasers as $teaser_id => $teaser):
841 + $teaser_idx++;
842 + // Mark items that start a new row so the row-divider rules look correct on every grid breakpoint.
843 + $row_break_sm = ($teaser_idx > 2); // 2 cols
844 + $row_break_lg = ($teaser_idx > 3); // 3 cols
845 + $initial = strtoupper(mb_substr($teaser['title'], 0, 1));
846 + ?>
847 + <a href="https://matrixaddons.com/plugins/easy-invoice/#pricing" target="_blank" rel="noopener"
848 + class="ei-pro-gateway-card group relative flex items-center gap-3 px-4 py-3.5 hover:bg-indigo-50/40 transition-colors <?php echo $row_break_sm ? 'sm:border-t sm:border-gray-100' : ''; ?> <?php echo $row_break_lg ? 'lg:border-t lg:border-gray-100' : ''; ?>"
849 + data-gateway-id="<?php echo esc_attr($teaser_id); ?>">
850 +
851 + <span class="flex-shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-md bg-gradient-to-br from-gray-50 to-gray-100 border border-gray-200 text-sm font-bold text-gray-700 group-hover:from-indigo-50 group-hover:to-indigo-100 group-hover:border-indigo-200 group-hover:text-indigo-700 transition-all">
852 + <?php echo esc_html($initial); ?>
853 + </span>
854 + <div class="min-w-0 flex-1">
855 + <div class="text-sm font-semibold text-gray-900 truncate"><?php echo esc_html($teaser['title']); ?></div>
856 + <p class="mt-0.5 text-xs text-gray-500 leading-snug line-clamp-2"><?php echo esc_html($teaser['desc']); ?></p>
857 + </div>
858 + <svg class="w-4 h-4 text-gray-300 group-hover:text-indigo-500 group-hover:translate-x-0.5 flex-shrink-0 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
859 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
860 + </svg>
861 + </a>
862 + <?php endforeach; ?>
863 + </div>
864 + </div>
865 + <?php endif; ?>
866 + <?php endif; ?>
867 +
868 + <div id="ei-offline-add" class="mb-5 rounded-lg border border-dashed border-gray-300 bg-white p-4">
869 + <div class="flex flex-col sm:flex-row sm:items-end gap-3">
870 + <div class="flex-1">
871 + <label for="ei-offline-add-title" class="block text-sm font-medium text-gray-900"><?php esc_html_e('Add an offline payment method', 'easy-invoice'); ?></label>
872 + <p class="mt-0.5 text-xs text-gray-500"><?php esc_html_e('For money you receive outside a card gateway — ACH, wire transfer, mobile money, a second bank account. Each method gets its own instructions, an optional reference field and a receipt upload; clients pick it on the invoice and you confirm the payment on the Payments screen.', 'easy-invoice'); ?></p>
873 + <input type="text" id="ei-offline-add-title" maxlength="60" placeholder="<?php esc_attr_e('e.g. ACH, Wire transfer', 'easy-invoice'); ?>" class="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
874 + </div>
875 + <button type="button" id="ei-offline-add-btn" class="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"><?php esc_html_e('Add method', 'easy-invoice'); ?></button>
876 + </div>
877 + </div>
673 878 <div id="sortable-gateways" class="space-y-5">
674 879 <?php foreach ($all_gateways_sorted as $gateway_id => $gateway) : ?>
675 880 <div class="gateway-item bg-gray-50 p-5 rounded-lg border border-gray-200 cursor-move" data-gateway-id="<?php echo esc_attr($gateway_id); ?>">
676 881 <div class="flex items-center mb-3 space-x-3">
677 - <!-- Drag Handle -->
882 +
678 883 <div class="gateway-handle text-gray-400 hover:text-gray-600 cursor-move mr-2">
679 884 <i class="fas fa-grip-vertical"></i>
680 885 </div>
681 886
@@ -690,18 +895,18 @@
690 895 <label for="gateway-enable-<?php echo esc_attr($gateway_id); ?>" class="font-medium text-gray-900">
691 896 <?php echo esc_html(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : $gateway_id); ?>
692 897 </label>
693 898 <?php if (method_exists($gateway, 'getDescription') && $gateway->getDescription()): ?>
694 - <span class="block text-sm text-gray-400"><?php echo esc_html($gateway->getDescription()); ?></span>
899 + <span class="block text-sm text-gray-400"><?php echo wp_kses_post($gateway->getDescription()); ?></span>
695 900 <?php endif; ?>
696 901 </div>
697 902 </div>
698 903
699 - <!-- Custom Display Name Field -->
904 +
700 905 <div class="gateway-settings <?php echo empty($payment_methods_enabled) || !in_array($gateway_id, $payment_methods_enabled) ? 'hidden' : ''; ?>">
701 906 <div class="mb-4">
702 907 <label for="gateway-display-name-<?php echo esc_attr($gateway_id); ?>" class="block text-sm font-medium text-gray-700">
703 - <?php _e('Display Name', 'easy-invoice'); ?>
908 + <?php esc_html_e('Display Name', 'easy-invoice'); ?>
704 909 </label>
705 910 <input type="text"
706 911 id="gateway-display-name-<?php echo esc_attr($gateway_id); ?>"
707 912 name="settings[easy_invoice_gateway_display_name_<?php echo esc_attr($gateway_id); ?>]"
@@ -708,9 +913,9 @@
708 913 value="<?php echo esc_attr($settings['easy_invoice_gateway_display_name_' . $gateway_id] ?? (method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id)))); ?>"
709 914 placeholder="<?php echo esc_attr(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id))); ?>"
710 915 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">
711 916 <p class="mt-1 text-xs text-gray-500">
712 - <?php _e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
917 + <?php esc_html_e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
713 918 </p>
714 919 </div>
715 920
716 921 <?php
@@ -727,8 +932,11 @@
727 932 <?php endif; ?>
728 933 </div>
729 934
730 935 <input type="hidden" class="gateway-order-input" name="settings[easy_invoice_gateway_order][]" value="<?php echo esc_attr($gateway_id); ?>">
936 + <?php if ($gateway instanceof \EasyInvoice\Gateways\OfflineGateway && $gateway->isCustom()) : ?>
937 + <p class="mt-3 text-xs text-gray-500"><?php esc_html_e('Added by you.', 'easy-invoice'); ?> <button type="button" class="ei-offline-remove font-medium text-red-600 hover:text-red-800" data-method-id="<?php echo esc_attr($gateway_id); ?>" data-method-title="<?php echo esc_attr($gateway->getTitle()); ?>"><?php esc_html_e('Remove this method', 'easy-invoice'); ?></button> <span class="text-gray-400">· <?php esc_html_e('Payments already recorded keep it.', 'easy-invoice'); ?></span></p>
938 + <?php endif; ?>
731 939 </div>
732 940 <?php endforeach; ?>
733 941 </div>
734 942 </div>
@@ -792,18 +1000,18 @@
792 1000 case 'text':
793 1001 case 'email':
794 1002 case 'url':
795 1003 case 'tel':
796 - echo '<input type="' . esc_attr($field_config['type']) . '" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
1004 + echo '<input type="' . esc_attr($field_config['type']) . '" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
797 1005 break;
798 1006 case 'number':
799 1007 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
800 1008 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
801 1009 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
802 - echo '<input type="number" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
1010 + echo '<input type="number" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
803 1011 break;
804 1012 case 'select':
805 - echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . $select_class . '">';
1013 + echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . esc_html($select_class) . '">';
806 1014 if (!empty($field_config['options']) && is_array($field_config['options'])) {
807 1015 foreach ($field_config['options'] as $opt_val => $opt_label) {
808 1016 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
809 1017 }
@@ -821,12 +1029,13 @@
821 1029 // Allow additional fields to be added to the tax section
822 1030 $additional_tax_fields = apply_filters('easy_invoice_tax_section_additional_fields', [], $settings);
823 1031 if (!empty($additional_tax_fields)) {
824 1032 echo '<div class="mt-6 pt-6 border-t border-gray-200">';
825 - echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . __('Additional Tax Settings', 'easy-invoice-pro') . '</h4>';
1033 + echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . esc_html__('Additional Tax Settings', 'easy-invoice') . '</h4>';
826 1034 echo '<div class="grid grid-cols-1 md:grid-cols-2 gap-6">';
827 1035 foreach ($additional_tax_fields as $field_key => $field_config) {
828 - $current_value = $settings[$field_key] ?? ($field_config['default'] ?? '');
1036 + // Filter-only fields are not in the loaded settings array; read the option itself.
1037 + $current_value = array_key_exists($field_key, (array) $settings) ? $settings[$field_key] : get_option($field_key, $field_config['default'] ?? '');
829 1038 easy_invoice_render_field($field_key, $field_config, $current_value);
830 1039 }
831 1040 echo '</div>';
832 1041 echo '</div>';
@@ -875,11 +1084,11 @@
875 1084 $conditional_class .= ' depends-on-' . esc_attr($depends_key) . '-' . esc_attr($depends_value);
876 1085 }
877 1086 }
878 1087
879 - echo '<div class="form-group ' . $form_group_class . $conditional_class . '">';
1088 + echo '<div class="form-group ' . esc_html($form_group_class) . esc_html($conditional_class) . '">';
880 1089 if ($field_type !== 'checkbox') {
881 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . $field_label . '</label>';
1090 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
882 1091 }
883 1092
884 1093 // Define required and aria_describedby variables
885 1094 $required = !empty($field_config['required']) ? 'required' : '';
@@ -889,21 +1098,21 @@
889 1098 case 'text':
890 1099 case 'email':
891 1100 case 'url':
892 1101 case 'tel':
893 - echo '<input type="' . esc_attr($field_type) . '" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
1102 + echo '<input type="' . esc_attr($field_type) . '" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
894 1103 break;
895 1104 case 'number':
896 1105 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
897 1106 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
898 1107 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
899 - echo '<input type="number" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
1108 + echo '<input type="number" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
900 1109 break;
901 1110 case 'textarea':
902 - echo '<textarea id="' . $field_id . '" name="' . $field_name . '" rows="3" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
1111 + echo '<textarea id="' . $field_id . '" name="' . esc_html($field_name) . '" rows="3" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
903 1112 break;
904 1113 case 'select':
905 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
1114 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
906 1115 if (!empty($field_config['options']) && is_array($field_config['options'])) {
907 1116 foreach ($field_config['options'] as $opt_val => $opt_label) {
908 1117 echo '<option value="' . esc_attr($opt_val) . '" ' . selected(strtolower($current_value), strtolower($opt_val), false) . '>' . esc_html($opt_label) . '</option>';
909 1118 }
@@ -910,25 +1119,25 @@
910 1119 }
911 1120 echo '</select>';
912 1121 break;
913 1122 case 'multiselect':
914 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
1123 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
915 1124 if (!empty($field_config['options']) && is_array($field_config['options'])) {
916 1125 foreach ($field_config['options'] as $opt_val => $opt_label) {
917 1126 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
918 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
1127 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
919 1128 }
920 1129 }
921 1130 echo '</select>';
922 1131 break;
923 1132 case 'readonly':
924 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . ' bg-gray-50" readonly>';
925 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($current_value) . '">';
1133 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly>';
1134 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($current_value) . '">';
926 1135 break;
927 1136 case 'checkbox':
928 1137 echo '<div class="flex items-center">';
929 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
930 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $field_label . '</label>';
1138 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
1139 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($field_label) . '</label>';
931 1140 echo '</div>';
932 1141 if (!empty($field_description)) {
933 1142 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
934 1143 }
@@ -935,14 +1144,14 @@
935 1144 break;
936 1145 case 'image':
937 1146 echo '<div class="mt-1 flex items-center">';
938 1147 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
939 - echo ' <img id="' . $field_id . '-preview" src="' . esc_url($current_value) . '" alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
1148 + echo ' <img id="' . $field_id . '-preview"' . (empty($current_value) ? '' : ' src="' . esc_url($current_value) . '"') . ' alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
940 1149 echo ' </span>';
941 1150 echo ' <button type="button" id="upload_image_button_' . $field_id . '" class="upload-logo-button ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-uploader_title="' . esc_attr__('Choose Logo', 'easy-invoice') . '" data-uploader_button_text="' . esc_attr__('Select Logo', 'easy-invoice') . '">';
942 1151 echo esc_html__('Change', 'easy-invoice');
943 1152 echo '</button>';
944 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1153 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
945 1154 echo '</div>';
946 1155 break;
947 1156 case 'wp_editor':
948 1157 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
@@ -947,25 +1156,25 @@
947 1156 case 'wp_editor':
948 1157 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
949 1158 // Handle description for wp_editor specifically
950 1159 if (!empty($field_description)) {
951 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
1160 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
952 1161 }
953 1162 break;
954 1163 case 'color':
955 - echo '<input type="color" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="h-10 w-20 border border-gray-300 rounded">';
1164 + echo '<input type="color" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="h-10 w-20 border border-gray-300 rounded">';
956 1165 break;
957 1166 case 'range':
958 1167 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
959 1168 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
960 1169 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
961 - echo '<input type="range" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
1170 + echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
962 1171 echo '<span class="text-sm text-gray-400">' . esc_html($current_value) . '%</span>';
963 1172 break;
964 1173 case 'image_upload':
965 1174 echo '<div class="image-upload-wrap">';
966 1175 echo '<div class="flex items-center space-x-3">';
967 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1176 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
968 1177 echo ' <button type="button" class="upload-image-button bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-target="' . $field_id . '">';
969 1178 echo esc_html__('Upload Image', 'easy-invoice');
970 1179 echo '</button>';
971 1180 echo '</div>';
@@ -982,18 +1191,18 @@
982 1191 $button_class_name = 'regenerate-invoice-numbers-button';
983 1192 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
984 1193 $button_class_name = 'regenerate-quote-numbers-button';
985 1194 }
986 - echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . $button_class_name . '">';
1195 + echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
987 1196 echo esc_html($button_text);
988 1197 echo '</button>';
989 1198 break;
990 1199 default:
991 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '">';
1200 + echo '<input type="text" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '">';
992 1201 break;
993 1202 }
994 1203 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
995 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
1204 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
996 1205 }
997 1206 echo '</div>';
998 1207 }
999 1208 if ($is_grid) echo '</div>';
@@ -1009,9 +1218,9 @@
1009 1218 </div>
1010 1219 </div>
1011 1220 <?php endforeach; ?>
1012 1221
1013 - <!-- Bottom Save Button -->
1222 +
1014 1223 <div class="mt-8 pt-6 border-t border-gray-200">
1015 1224 <div class="flex items-center justify-start space-x-3">
1016 1225 <button type="button" id="save-settings-bottom" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200" aria-label="<?php esc_attr_e('Save Settings', 'easy-invoice'); ?>">
1017 1226 <i class="fas fa-save mr-2" aria-hidden="true"></i>