PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
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
easy-invoice / templates / settings-page.php

settings-page.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.2, at templates/settings-page.php

1,389 lines 125.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings page template for Easy Invoice
4 *
5 * @package Easy_Invoice
6 * @since 1.0.0
7 */
8
9 // Exit if accessed directly
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
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 /**
105 * Helper function to render a single settings field.
106 *
107 * @param string $option_key The option key.
108 * @param array $field_config The field configuration.
109 * @param mixed $current_value The current value of the field.
110 */
111 function easy_invoice_render_field($option_key, $field_config, $current_value) {
112 $label = esc_html($field_config['label'] ?? '');
113 $type = $field_config['type'] ?? 'text';
114 $default = $field_config['default'] ?? '';
115 $value = $current_value ?? $default;
116 $description_text = $field_config['description'] ?? '';
117 $field_id = esc_attr($option_key);
118 $field_name = 'settings[' . esc_attr($option_key) . ']';
119 $col_span_class = esc_attr($field_config['col_span'] ?? 'sm:col-span-6');
120 $placeholder = isset($field_config['placeholder']) ? esc_attr($field_config['placeholder']) : '';
121 $required = !empty($field_config['required']) ? 'required' : '';
122 $aria_describedby = !empty($description_text) ? 'aria-describedby="' . $field_id . '-description"' : '';
123
124 echo '<div class=" ' . esc_html($col_span_class) . '">'; // Added 'form-group' class for JS show/hide functionality
125
126 // Special layout for checkboxes with descriptions (e.g., Invoice Numbering type)
127 if ($type === 'checkbox' && !empty($description_text) && ($option_key === 'easy_invoice_invoice_numbering')) {
128 echo '<div class="flex items-start space-x-3 p-4 bg-gray-50 rounded-lg border border-gray-200">';
129 echo ' <div class="flex items-center h-5 mt-0.5">';
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) . '>';
131 echo ' </div>';
132 echo ' <div class="flex-1">';
133 echo ' <label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-1">' . esc_html($label) . '</label>';
134 echo ' <p id="' . $field_id . '-description" class="text-sm text-gray-400 leading-relaxed">' . wp_kses($description_text, array(
135 'a' => array(
136 'href' => array(),
137 'target' => array(),
138 'class' => array()
139 )
140 )) . '</p>';
141 echo ' </div>';
142 echo '</div>';
143 } else {
144 // Standard label for other types, or checkboxes without the special description structure
145 if ($type !== 'checkbox') {
146 echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-2">' . esc_html($label) . '</label>';
147 }
148
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';
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';
151
152 switch ($type) {
153 case 'text':
154 case 'email':
155 case 'url':
156 case 'tel':
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 . '>';
158 break;
159 case 'number':
160 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
161 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
162 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
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 . '>';
164 break;
165 case '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>';
167 break;
168 case 'select':
169 echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
170 if (!empty($field_config['options']) && is_array($field_config['options'])) {
171 foreach ($field_config['options'] as $opt_val => $opt_label) {
172 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
173 }
174 }
175 echo '</select>';
176 break;
177 case 'multiselect':
178 echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($label) . '">';
179 if (!empty($field_config['options']) && is_array($field_config['options'])) {
180 foreach ($field_config['options'] as $opt_val => $opt_label) {
181 $selected = is_array($value) && in_array($opt_val, $value) ? 'selected="selected"' : '';
182 echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
183 }
184 }
185 echo '</select>';
186 break;
187 case 'readonly':
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) . '">';
190 break;
191 case 'checkbox': // Checkboxes that don't use the special description layout
192 echo '<div class="flex items-center">'; // Original 'tax_enabled' was just this simple structure
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>';
195 echo '</div>';
196 if (!empty($description_text)) { // Show description below the field
197 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . wp_kses($description_text, array(
198 'a' => array(
199 'href' => array(),
200 'target' => array(),
201 'class' => array()
202 )
203 )) . '</p>';
204 }
205 break;
206 case 'image':
207 echo '<div class="mt-1 flex items-center">';
208 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
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' : '') . '">';
210 echo ' </span>';
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') . '">';
212 echo esc_html__('Change', 'easy-invoice');
213 echo '</button>';
214 echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
215 echo '</div>';
216 break;
217 case 'wp_editor':
218 // For wp_editor, the label is usually handled before calling it.
219 // The col_span div wraps this.
220 wp_editor( $value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
221 // Handle description for wp_editor specifically
222 if (!empty($description_text)) {
223 echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($description_text, array(
224 'a' => array(
225 'href' => array(),
226 'target' => array(),
227 'class' => array()
228 )
229 )) . '</p>';
230 }
231 break;
232 case 'test_email':
233 echo '<div class="mt-1">';
234 echo ' <div class="flex items-center space-x-3">';
235 echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block w-[200px] 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')) . '">';
236 echo ' <button type="button" id="' . $field_id . '_button" class="test-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-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 h-[38px]">';
237 echo ' <i class="fas fa-paper-plane mr-2" aria-hidden="true"></i>';
238 echo ' ' . esc_html__('Send Test Email', 'easy-invoice');
239 echo ' </button>';
240 echo ' </div>';
241 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
242 echo '</div>';
243 break;
244 case 'color':
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">';
246 break;
247 case 'range':
248 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
249 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
250 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
251 echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="w-full">';
252 echo '<span class="text-sm text-gray-400">' . esc_html($value) . '%</span>';
253 break;
254 case 'button':
255 $button_text = $field_config['button_text'] ?? $label;
256 $button_class = $field_config['button_class'] ?? 'button button-secondary';
257 $button_class_name = 'regenerate-invoice-numbers-button';
258 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
259 $button_class_name = 'regenerate-quote-numbers-button';
260 }
261 echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
262 echo esc_html($button_text);
263 echo '</button>';
264 break;
265 case 'image_upload':
266 echo '<div class="image-upload-wrap">';
267 echo '<div class="flex items-center space-x-3">';
268 echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '">';
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 . '">';
270 echo esc_html__('Upload Image', 'easy-invoice');
271 echo '</button>';
272 echo '</div>';
273 echo '<div class="image-preview mt-2' . (empty($value) ? ' hidden' : '') . '">';
274 if (!empty($value)) {
275 echo '<img src="' . esc_url($value) . '" alt="Preview" style="max-width: 200px; max-height: 100px;">';
276 }
277 echo '</div>';
278 echo '</div>';
279 break;
280 default:
281 // Special handling for regenerate invoice numbers button
282 if ($option_key === 'easy_invoice_regenerate_invoice_numbers') {
283 $button_text = $field_config['button_text'] ?? $label;
284 $button_class = $field_config['button_class'] ?? 'button button-secondary';
285 echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' regenerate-invoice-numbers-button">';
286 echo esc_html($button_text);
287 echo '</button>';
288 } elseif ($option_key === 'easy_invoice_regenerate_quote_numbers') {
289 $button_text = $field_config['button_text'] ?? $label;
290 $button_class = $field_config['button_class'] ?? 'button button-secondary';
291 echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' regenerate-quote-numbers-button">';
292 echo esc_html($button_text);
293 echo '</button>';
294 } else {
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 . '>';
296 }
297 break;
298 }
299 // General description for non-checkbox and non-wp_editor fields (if it exists and not handled by special layouts)
300 if ($type !== 'checkbox' && $type !== 'wp_editor' && !empty($description_text)) {
301 echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($description_text, array(
302 'a' => array(
303 'href' => array(),
304 'target' => array(),
305 'class' => array()
306 )
307 )) . '</p>';
308 }
309 }
310 echo '</div>'; // End col_span div
311 }
312
313 /**
314 * Filter settings config to allow extensions to modify it
315 * Note: This filter is already applied in the controller, so this is redundant
316 */
317 // $settings_config = apply_filters('easy_invoice_settings_fields_config', $settings_config);
318
319 $active_section = isset($_GET['section']) ? sanitize_key(wp_unslash($_GET['section'])) : key($settings_config); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
320 if (!isset($settings_config[$active_section])) {
321 $active_section = key($settings_config);
322 }
323 ?>
324
325 <div class="p-8">
326
327 <div class="ei-app-page-header bg-white border-b border-gray-200 px-6 easy-invoice-admin-page-header">
328 <div class="flex items-center justify-between">
329 <div>
330 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Settings', 'easy-invoice'); ?></h1>
331 <p class="mt-1 text-sm text-gray-400"><?php esc_html_e('Configure your invoice management system', 'easy-invoice'); ?></p>
332 </div>
333 <div class="flex items-center space-x-3">
334 <button type="button" id="save-settings" 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" aria-label="<?php esc_attr_e('Save Settings', 'easy-invoice'); ?>">
335 <i class="fas fa-save mr-2" aria-hidden="true"></i>
336 <?php esc_html_e('Save Changes', 'easy-invoice'); ?>
337 </button>
338 </div>
339 </div>
340 </div>
341
342
343 <div class="max-w-7xl mx-auto">
344 <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
345
346 <div class="lg:col-span-1">
347 <div class="bg-white shadow-lg rounded-lg sticky top-24 border border-gray-200">
348 <nav class="space-y-2 p-4" aria-label="<?php esc_attr_e('Settings Navigation', 'easy-invoice'); ?>">
349 <?php foreach ($settings_config as $section_id => $section_data) : ?>
350 <div class="space-y-1">
351 <a href="?page=easy-invoice-settings&section=<?php echo esc_attr($section_id); ?>"
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'; ?>"
353 aria-selected="<?php echo $section_id === $active_section ? 'true' : 'false'; ?>"
354 role="tab">
355
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 ?>
363
364 <span class="flex-1"><?php echo esc_html($section_data['title']); ?></span>
365 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
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">
367 <polyline points="6,9 12,15 18,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
368 </svg>
369 <?php endif; ?>
370 </a>
371
372 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
373 <div class="ml-6 space-y-1 email-settings-submenu" style="display: none;">
374 <?php
375 // Only show active subsection if we're actually on an email settings page
376 $active_email_subsection = null;
377 if ($active_section === 'email') {
378 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
379 $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
380 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
381 $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
382 } else {
383 // Default to 'general' only if we're on email section but no specific subsection
384 $active_email_subsection = 'general';
385 }
386 }
387 ?>
388 <?php foreach ($section_data['subsections'] as $subsection_id => $subsection_data): ?>
389 <?php $is_active_subsection = ($active_email_subsection && $subsection_id === $active_email_subsection); ?>
390 <a href="?page=easy-invoice-settings&section=email&subsection=<?php echo esc_attr($subsection_id); ?>"
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'; ?>"
392 data-subsection="<?php echo esc_attr($subsection_id); ?>"
393 data-section="<?php echo esc_attr($section_id); ?>">
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">
395 <polyline points="9,18 15,12 9,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
396 </svg>
397 <?php
398 // Display shorter names for better UX
399 $display_name = '';
400 switch ($subsection_id) {
401 case 'general':
402 $display_name = 'General';
403 break;
404 case 'invoice_available':
405 $display_name = 'Invoice Available';
406 break;
407 case 'quote_available':
408 $display_name = 'Quote Available';
409 break;
410 case 'payment_received':
411 $display_name = 'Payment Received';
412 break;
413 default:
414 $display_name = $subsection_data['title'];
415 break;
416 }
417 echo esc_html($display_name);
418 ?>
419 </a>
420 <?php endforeach; ?>
421 </div>
422 <?php endif; ?>
423 </div>
424 <?php endforeach; ?>
425 </nav>
426 </div>
427 </div>
428
429
430 <div class="lg:col-span-2">
431 <form id="settings-form" method="post" action="">
432 <?php wp_nonce_field('easy_invoice_settings', 'easy_invoice_settings_nonce'); ?>
433 <input type="hidden" name="action" value="easy_invoice_save_settings">
434
435 <?php foreach ($settings_config as $section_id => $section_data) : ?>
436 <div id="<?php echo esc_attr($section_id); ?>"
437 class="settings-section bg-white shadow rounded-lg <?php echo ($section_id !== $active_section) ? 'hidden' : ''; ?>"
438 role="tabpanel"
439 aria-labelledby="tab-<?php echo esc_attr($section_id); ?>">
440 <div class="px-6 py-5 border-b border-gray-200">
441 <div class="flex items-center">
442 <div class="flex-shrink-0 bg-indigo-100 rounded-lg p-3">
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 ?>
450 </div>
451 <div class="ml-4">
452 <h2 class="text-lg font-medium text-gray-900"><?php echo esc_html($section_data['title']); ?></h2>
453 <p class="mt-1 text-sm text-gray-400"><?php echo esc_html($section_data['description']); ?></p>
454 </div>
455 </div>
456 </div>
457 <div class="px-6 py-5">
458 <div class="email-settings">
459 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
460 <?php
461 // Determine the active subsection from the hash or default to 'general'
462 $active_email_subsection = 'general';
463 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
464 $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
465 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
466 $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
467 }
468 if (!isset($section_data['subsections'][$active_email_subsection])) {
469 $active_email_subsection = array_key_first($section_data['subsections']);
470 }
471 $subsection_data = $section_data['subsections'][$active_email_subsection];
472 ?>
473 <?php
474 // Render all email subsections so JS can show/hide them
475 foreach ($section_data['subsections'] as $subsection_key => $subsection_data) {
476 $is_active = ($subsection_key === $active_email_subsection) ? '' : 'hidden';
477 ?>
478 <div id="email--<?php echo esc_attr($subsection_key); ?>-content" class="email-subsection-content <?php echo esc_attr($is_active); ?>">
479 <?php
480 // Display subsection title
481 $subsection_title = '';
482 switch ($subsection_key) {
483 case 'general':
484 $subsection_title = 'General Email Settings';
485 break;
486 case 'invoice_available':
487 $subsection_title = 'Invoice Available Email Settings';
488 break;
489 case 'quote_available':
490 $subsection_title = 'Quote Available Email Settings';
491 break;
492 case 'payment_received':
493 $subsection_title = 'Payment Received Email Settings';
494 break;
495 default:
496 $subsection_title = $subsection_data['title'];
497 break;
498 }
499 ?>
500 <div class="mb-6">
501 <h3 class="text-lg font-medium text-gray-900 mb-2"><?php echo esc_html($subsection_title); ?></h3>
502 <?php if (!empty($subsection_data['description'])): ?>
503 <p class="text-sm text-gray-400"><?php echo esc_html($subsection_data['description']); ?></p>
504 <?php endif; ?>
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; ?>
553 <div class="space-y-6">
554 <?php if (!empty($subsection_data['fields'])) {
555 foreach ($subsection_data['fields'] as $option_key => $field_config) {
556 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
557 $field_id = esc_attr($option_key);
558 $field_type = $field_config['type'] ?? 'text';
559 $field_label = esc_html($field_config['label'] ?? '');
560 $field_placeholder = isset($field_config['placeholder']) ? esc_attr($field_config['placeholder']) : '';
561 $field_description = $field_config['description'] ?? '';
562 $field_name = 'settings[' . esc_attr($option_key) . ']';
563 $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';
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';
565 $col_span_class = esc_attr($field_config['col_span'] ?? 'sm:col-span-6');
566 echo '<div class="field-wrapper form-group">';
567 if ($field_type !== 'checkbox') {
568 echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
569 }
570
571 // Define required and aria_describedby variables
572 $required = !empty($field_config['required']) ? 'required' : '';
573 $aria_describedby = !empty($field_description) ? 'aria-describedby="' . $field_id . '-description"' : '';
574
575 switch ($field_type) {
576 case 'text':
577 case 'email':
578 case 'url':
579 case 'tel':
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 . '>';
581 break;
582 case 'number':
583 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
584 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
585 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
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 . '>';
587 break;
588 case '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>';
590 break;
591 case 'select':
592 echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
593 if (!empty($field_config['options']) && is_array($field_config['options'])) {
594 foreach ($field_config['options'] as $opt_val => $opt_label) {
595 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
596 }
597 }
598 echo '</select>';
599 break;
600 case 'multiselect':
601 echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
602 if (!empty($field_config['options']) && is_array($field_config['options'])) {
603 foreach ($field_config['options'] as $opt_val => $opt_label) {
604 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
605 echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
606 }
607 }
608 echo '</select>';
609 break;
610 case 'readonly':
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) . '">';
613 break;
614 case 'checkbox':
615 echo '<div class="flex items-center">';
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>';
618 echo '</div>';
619 if (!empty($field_description)) {
620 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
621 }
622 break;
623 case 'image':
624 echo '<div class="mt-1 flex items-center">';
625 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
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' : '') . '">';
627 echo ' </span>';
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') . '">';
629 echo esc_html__('Change', 'easy-invoice');
630 echo '</button>';
631 echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
632 echo '</div>';
633 break;
634 case 'wp_editor':
635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
636 // Handle description for wp_editor specifically
637 if (!empty($field_description)) {
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>';
639 }
640 break;
641 case 'test_email':
642 echo '<div class="mt-1">';
643 echo ' <div class="flex items-center space-x-3">';
644 echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block w-[200px] 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')) . '">';
645 echo ' <button type="button" id="' . $field_id . '_button" class="test-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-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 h-[38px]">';
646 echo ' <i class="fas fa-paper-plane mr-2" aria-hidden="true"></i>';
647 echo ' ' . esc_html__('Send Test Email', 'easy-invoice');
648 echo ' </button>';
649 echo ' </div>';
650 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
651 echo '</div>';
652 break;
653 case 'test_template_email':
654 $template_type = $field_config['template_type'] ?? '';
655 $is_payment_test = ($field_id === 'easy_invoice_payment_test_template');
656 $width_class = $is_payment_test ? 'w-[200px]' : 'w-64';
657 echo '<div class="mt-1">';
658 echo ' <div class="flex items-center space-x-3">';
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) . '">';
661 echo ' <i class="fas fa-envelope mr-2" aria-hidden="true"></i>';
662 echo ' ' . esc_html__('Test Template', 'easy-invoice');
663 echo ' </button>';
664 echo ' </div>';
665 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
666 echo '</div>';
667 break;
668 case 'test_payment_reminder_email':
669 echo '<div class="mt-1">';
670 echo ' <div class="flex items-center space-x-3">';
671 echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block w-64 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')) . '">';
672 echo ' <button type="button" id="' . $field_id . '_button" class="test-payment-reminder-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-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 h-[38px]">';
673 echo ' <i class="fas fa-bell mr-2" aria-hidden="true"></i>';
674 echo ' ' . esc_html__('Test Reminder', 'easy-invoice');
675 echo ' </button>';
676 echo ' </div>';
677 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
678 echo '</div>';
679 break;
680 default:
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) . '">';
682 break;
683 }
684 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
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>';
686 }
687 echo '</div>'; // Close the field wrapper div
688 }
689 }
690 ?>
691
692 <?php
693 // Show placeholders only for specific email subsections (not General)
694 if (in_array($subsection_key, ['invoice_available', 'quote_available', 'payment_received', 'payment_reminder'])): ?>
695 <div class="mt-6">
696 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
697 <p class="text-sm font-medium text-blue-900 mb-3"><?php esc_html_e('Available Placeholders:', 'easy-invoice'); ?></p>
698 <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
699 <?php if (in_array($subsection_key, ['invoice_available', 'payment_received', 'payment_reminder'])): ?>
700
701 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_number}}</div>
702 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{due_date}}</div>
703 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_url}}</div>
704 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_url}}</div>
705 <?php endif; ?>
706
707 <?php if (in_array($subsection_key, ['quote_available'])): ?>
708
709 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_number}}</div>
710 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{expiry_date}}</div>
711 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_url}}</div>
712 <?php endif; ?>
713
714
715 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_name}}</div>
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>
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>
718 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_email}}</div>
719 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_address}}</div>
720 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_name}}</div>
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>
723 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{issue_date}}</div>
724 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_email}}</div>
725 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_phone}}</div>
726 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_terms}}</div>
727 <?php do_action('easy_invoice_settings_email_placeholders', $settings); ?>
728 </div>
729 </div>
730 </div>
731 <?php endif; ?>
732 </div>
733 </div>
734 <?php } ?>
735 <?php elseif ($section_id === 'payment' && !empty($all_gateways_sorted)): ?>
736 <div class="space-y-5">
737 <?php if (count($all_gateways_sorted) > 1): ?>
738 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
739 <div class="flex items-center">
740 <i class="fas fa-info-circle text-blue-500 mr-2"></i>
741 <p class="text-sm text-blue-700">
742 <?php esc_html_e('Drag and drop payment methods to reorder them. The order will be reflected on payment forms.', 'easy-invoice'); ?>
743 </p>
744 </div>
745 </div>
746 <?php endif; ?>
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>
878 <div id="sortable-gateways" class="space-y-5">
879 <?php foreach ($all_gateways_sorted as $gateway_id => $gateway) : ?>
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); ?>">
881 <div class="flex items-center mb-3 space-x-3">
882
883 <div class="gateway-handle text-gray-400 hover:text-gray-600 cursor-move mr-2">
884 <i class="fas fa-grip-vertical"></i>
885 </div>
886
887 <input type="checkbox"
888 class="gateway-enable-checkbox h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
889 name="settings[easy_invoice_payment_methods][]"
890 id="gateway-enable-<?php echo esc_attr($gateway_id); ?>"
891 value="<?php echo esc_attr($gateway_id); ?>"
892 <?php checked(!empty($payment_methods_enabled) && in_array($gateway_id, $payment_methods_enabled)); ?>>
893
894 <div class="flex-1">
895 <label for="gateway-enable-<?php echo esc_attr($gateway_id); ?>" class="font-medium text-gray-900">
896 <?php echo esc_html(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : $gateway_id); ?>
897 </label>
898 <?php if (method_exists($gateway, 'getDescription') && $gateway->getDescription()): ?>
899 <span class="block text-sm text-gray-400"><?php echo wp_kses_post($gateway->getDescription()); ?></span>
900 <?php endif; ?>
901 </div>
902 </div>
903
904
905 <div class="gateway-settings <?php echo empty($payment_methods_enabled) || !in_array($gateway_id, $payment_methods_enabled) ? 'hidden' : ''; ?>">
906 <div class="mb-4">
907 <label for="gateway-display-name-<?php echo esc_attr($gateway_id); ?>" class="block text-sm font-medium text-gray-700">
908 <?php esc_html_e('Display Name', 'easy-invoice'); ?>
909 </label>
910 <input type="text"
911 id="gateway-display-name-<?php echo esc_attr($gateway_id); ?>"
912 name="settings[easy_invoice_gateway_display_name_<?php echo esc_attr($gateway_id); ?>]"
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)))); ?>"
914 placeholder="<?php echo esc_attr(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id))); ?>"
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">
916 <p class="mt-1 text-xs text-gray-500">
917 <?php esc_html_e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
918 </p>
919 </div>
920
921 <?php
922 $gateway_config = method_exists($gateway, 'getSettingsConfig') ? $gateway->getSettingsConfig() : [];
923 if (!empty($gateway_config['fields'])): ?>
924 <div class="grid grid-cols-1 md:grid-cols-6 gap-4">
925 <?php foreach ($gateway_config['fields'] as $option_key => $field_config): ?>
926 <?php
927 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
928 easy_invoice_render_field($option_key, $field_config, $current_value);
929 ?>
930 <?php endforeach; ?>
931 </div>
932 <?php endif; ?>
933 </div>
934
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; ?>
939 </div>
940 <?php endforeach; ?>
941 </div>
942 </div>
943 <?php elseif (!empty($section_data['fields'])): ?>
944 <?php
945 // Check if this section has subsections
946 if (!empty($section_data['subsections'])) {
947 // Handle sections with subsections (like invoice, quote, etc.)
948 foreach ($section_data['subsections'] as $subsection_id => $subsection_data) {
949 $subsection_title = $subsection_data['title'] ?? '';
950 $subsection_description = $subsection_data['description'] ?? '';
951 ?>
952 <div class="subsection-content mb-8">
953 <div class="mb-4">
954 <h3 class="text-lg font-medium text-gray-900"><?php echo esc_html($subsection_title); ?></h3>
955 <?php if (!empty($subsection_description)): ?>
956 <p class="text-sm text-gray-400"><?php echo esc_html($subsection_description); ?></p>
957 <?php endif; ?>
958 </div>
959 <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
960 <?php if (!empty($subsection_data['fields'])) {
961 foreach ($subsection_data['fields'] as $option_key => $field_config) {
962 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
963 easy_invoice_render_field($option_key, $field_config, $current_value);
964 }
965 }
966 ?>
967 </div>
968 </div>
969 <?php
970 }
971 } else {
972 // Handle regular sections without subsections
973 if ($section_id === 'tax') {
974 // Render Enable Tax checkbox full width
975 $enable_tax_key = 'easy_invoice_tax_enabled';
976 $enable_tax_field = $section_data['fields'][$enable_tax_key];
977 $current_value = $settings[$enable_tax_key] ?? ($enable_tax_field['default'] ?? '');
978 echo '<div class="mb-4">';
979 echo '<div class="flex items-center">';
980 echo ' <input type="checkbox" name="settings[' . esc_attr($enable_tax_key) . ']" id="' . esc_attr($enable_tax_key) . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
981 echo ' <label for="' . esc_attr($enable_tax_key) . '" class="ml-2 block text-sm text-gray-900">' . esc_html($enable_tax_field['label']) . '</label>';
982 echo '</div>';
983 if (!empty($enable_tax_field['description'])) {
984 echo '<p id="' . $field_id . '-description" class="text-gray-400 mt-1 text-xs">' . esc_html($enable_tax_field['description']) . '</p>';
985 }
986 echo '</div>';
987 // Render the other three fields in a 3-column grid
988 echo '<div class="grid grid-cols-1 md:grid-cols-3 gap-6">';
989 $tax_keys = ['easy_invoice_tax_entry_method', 'easy_invoice_tax_rate', 'easy_invoice_tax_name'];
990 $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';
991 $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';
992 foreach ($tax_keys as $tax_key) {
993 $field_config = $section_data['fields'][$tax_key];
994 $current_value = $settings[$tax_key] ?? ($field_config['default'] ?? '');
995 echo '<div>';
996 if ($field_config['type'] !== 'checkbox') {
997 echo '<label for="' . esc_attr($tax_key) . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_config['label']) . '</label>';
998 }
999 switch ($field_config['type']) {
1000 case 'text':
1001 case 'email':
1002 case 'url':
1003 case 'tel':
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'] ?? '') . '">';
1005 break;
1006 case 'number':
1007 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
1008 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
1009 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
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'] ?? '') . '">';
1011 break;
1012 case 'select':
1013 echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . esc_html($select_class) . '">';
1014 if (!empty($field_config['options']) && is_array($field_config['options'])) {
1015 foreach ($field_config['options'] as $opt_val => $opt_label) {
1016 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
1017 }
1018 }
1019 echo '</select>';
1020 break;
1021 }
1022 if (!empty($field_config['description'])) {
1023 echo '<p class="mt-1 text-xs text-gray-400">' . esc_html($field_config['description']) . '</p>';
1024 }
1025 echo '</div>';
1026 }
1027 echo '</div>';
1028
1029 // Allow additional fields to be added to the tax section
1030 $additional_tax_fields = apply_filters('easy_invoice_tax_section_additional_fields', [], $settings);
1031 if (!empty($additional_tax_fields)) {
1032 echo '<div class="mt-6 pt-6 border-t border-gray-200">';
1033 echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . esc_html__('Additional Tax Settings', 'easy-invoice') . '</h4>';
1034 echo '<div class="grid grid-cols-1 md:grid-cols-2 gap-6">';
1035 foreach ($additional_tax_fields as $field_key => $field_config) {
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'] ?? '');
1038 easy_invoice_render_field($field_key, $field_config, $current_value);
1039 }
1040 echo '</div>';
1041 echo '</div>';
1042 }
1043 } else {
1044 // Use a two-column grid for currency and company settings
1045 $is_grid = ($section_id === 'currency' || $section_id === 'company');
1046 // Use a 6-column grid for invoice and quote settings to allow specific field pairing
1047 $is_invoice_grid = ($section_id === 'invoice');
1048 $is_quote_grid = ($section_id === 'quote');
1049 // Use a specific grid for advanced settings to improve spacing
1050 $is_advanced_grid = ($section_id === 'advanced');
1051 // Use a 3-column grid for text settings
1052 $is_text_settings_grid = ($section_id === 'text_settings');
1053 if ($is_grid) echo '<div class="grid grid-cols-1 md:grid-cols-2 gap-6">';
1054 if ($is_invoice_grid) echo '<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">';
1055 if ($is_quote_grid) echo '<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">';
1056 if ($is_advanced_grid) echo '<div class="grid grid-cols-1 gap-6">';
1057 if ($is_text_settings_grid) echo '<div class="grid grid-cols-1 md:grid-cols-3 gap-6">';
1058 foreach ($section_data['fields'] as $option_key => $field_config) {
1059 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
1060 $field_id = esc_attr($option_key);
1061 $field_type = $field_config['type'] ?? 'text';
1062 $field_label = esc_html($field_config['label'] ?? '');
1063 $field_placeholder = isset($field_config['placeholder']) ? esc_attr($field_config['placeholder']) : '';
1064 $field_description = $field_config['description'] ?? '';
1065 $field_name = 'settings[' . esc_attr($option_key) . ']';
1066 $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';
1067 $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';
1068 $col_span_class = esc_attr($field_config['col_span'] ?? 'sm:col-span-6');
1069 $form_group_class = '';
1070 if ($is_grid) {
1071 $form_group_class = '';
1072 } elseif ($is_invoice_grid || $is_quote_grid || $is_advanced_grid || $is_text_settings_grid) {
1073 $form_group_class = $col_span_class;
1074 } else {
1075 $form_group_class = $col_span_class;
1076 }
1077
1078 // Handle conditional display
1079 $conditional_class = '';
1080 if (isset($field_config['depends_on']) && is_array($field_config['depends_on'])) {
1081 $conditional_class = ' conditional-field';
1082 foreach ($field_config['depends_on'] as $depends_key => $depends_value) {
1083 $depends_field_name = 'settings[' . esc_attr($depends_key) . ']';
1084 $conditional_class .= ' depends-on-' . esc_attr($depends_key) . '-' . esc_attr($depends_value);
1085 }
1086 }
1087
1088 echo '<div class="form-group ' . esc_html($form_group_class) . esc_html($conditional_class) . '">';
1089 if ($field_type !== 'checkbox') {
1090 echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
1091 }
1092
1093 // Define required and aria_describedby variables
1094 $required = !empty($field_config['required']) ? 'required' : '';
1095 $aria_describedby = !empty($field_description) ? 'aria-describedby="' . $field_id . '-description"' : '';
1096
1097 switch ($field_type) {
1098 case 'text':
1099 case 'email':
1100 case 'url':
1101 case 'tel':
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 . '>';
1103 break;
1104 case 'number':
1105 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
1106 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
1107 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
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 . '>';
1109 break;
1110 case '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>';
1112 break;
1113 case 'select':
1114 echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
1115 if (!empty($field_config['options']) && is_array($field_config['options'])) {
1116 foreach ($field_config['options'] as $opt_val => $opt_label) {
1117 echo '<option value="' . esc_attr($opt_val) . '" ' . selected(strtolower($current_value), strtolower($opt_val), false) . '>' . esc_html($opt_label) . '</option>';
1118 }
1119 }
1120 echo '</select>';
1121 break;
1122 case 'multiselect':
1123 echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
1124 if (!empty($field_config['options']) && is_array($field_config['options'])) {
1125 foreach ($field_config['options'] as $opt_val => $opt_label) {
1126 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
1127 echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
1128 }
1129 }
1130 echo '</select>';
1131 break;
1132 case 'readonly':
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) . '">';
1135 break;
1136 case 'checkbox':
1137 echo '<div class="flex items-center">';
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>';
1140 echo '</div>';
1141 if (!empty($field_description)) {
1142 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
1143 }
1144 break;
1145 case 'image':
1146 echo '<div class="mt-1 flex items-center">';
1147 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
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' : '') . '">';
1149 echo ' </span>';
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') . '">';
1151 echo esc_html__('Change', 'easy-invoice');
1152 echo '</button>';
1153 echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1154 echo '</div>';
1155 break;
1156 case 'wp_editor':
1157 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
1158 // Handle description for wp_editor specifically
1159 if (!empty($field_description)) {
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>';
1161 }
1162 break;
1163 case 'color':
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">';
1165 break;
1166 case 'range':
1167 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
1168 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
1169 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
1170 echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
1171 echo '<span class="text-sm text-gray-400">' . esc_html($current_value) . '%</span>';
1172 break;
1173 case 'image_upload':
1174 echo '<div class="image-upload-wrap">';
1175 echo '<div class="flex items-center space-x-3">';
1176 echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
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 . '">';
1178 echo esc_html__('Upload Image', 'easy-invoice');
1179 echo '</button>';
1180 echo '</div>';
1181 echo '<div class="image-preview mt-2' . (empty($current_value) ? ' hidden' : '') . '">';
1182 if (!empty($current_value)) {
1183 echo '<img src="' . esc_url($current_value) . '" alt="Preview" style="max-width: 200px; max-height: 100px;">';
1184 }
1185 echo '</div>';
1186 echo '</div>';
1187 break;
1188 case 'button':
1189 $button_text = $field_config['button_text'] ?? $field_label;
1190 $button_class = $field_config['button_class'] ?? 'button button-secondary';
1191 $button_class_name = 'regenerate-invoice-numbers-button';
1192 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
1193 $button_class_name = 'regenerate-quote-numbers-button';
1194 }
1195 echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
1196 echo esc_html($button_text);
1197 echo '</button>';
1198 break;
1199 default:
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) . '">';
1201 break;
1202 }
1203 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
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>';
1205 }
1206 echo '</div>';
1207 }
1208 if ($is_grid) echo '</div>';
1209 if ($is_invoice_grid) echo '</div>';
1210 if ($is_quote_grid) echo '</div>';
1211 if ($is_advanced_grid) echo '</div>';
1212 if ($is_text_settings_grid) echo '</div>';
1213 }
1214 }
1215 ?>
1216 <?php endif; ?>
1217 </div>
1218 </div>
1219 </div>
1220 <?php endforeach; ?>
1221
1222
1223 <div class="mt-8 pt-6 border-t border-gray-200">
1224 <div class="flex items-center justify-start space-x-3">
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'); ?>">
1226 <i class="fas fa-save mr-2" aria-hidden="true"></i>
1227 <?php esc_html_e('Save Changes', 'easy-invoice'); ?>
1228 </button>
1229 </div>
1230 </div>
1231
1232 </form>
1233 </div>
1234 </div>
1235 </div>
1236 </div>
1237
1238 <script>
1239 document.addEventListener('DOMContentLoaded', function() {
1240 // Enter key functionality to save settings
1241 const settingsForm = document.getElementById('settings-form');
1242 const saveButton = document.getElementById('save-settings-bottom');
1243
1244 if (settingsForm && saveButton) {
1245 // Get both save buttons
1246 const topSaveButton = document.getElementById('save-settings');
1247 const bottomSaveButton = document.getElementById('save-settings-bottom');
1248
1249 // Function to trigger save
1250 function triggerSave() {
1251 // Try bottom button first, then top button
1252 if (bottomSaveButton) {
1253 bottomSaveButton.click();
1254 } else if (topSaveButton) {
1255 topSaveButton.click();
1256 }
1257 }
1258
1259 // Listen for Enter key on form inputs
1260 settingsForm.addEventListener('keydown', function(e) {
1261 // Check if Enter key is pressed and not in a textarea or select
1262 if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') {
1263 e.preventDefault();
1264 triggerSave();
1265 }
1266 });
1267
1268 // Also listen for Enter key on the entire form
1269 settingsForm.addEventListener('keypress', function(e) {
1270 if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') {
1271 e.preventDefault();
1272 triggerSave();
1273 }
1274 });
1275 }
1276
1277 // Email Settings submenu hover behavior for settings page
1278 const emailNavLink = document.querySelector('.nav-link[href*="section=email"]');
1279 const emailSubmenu = document.querySelector('.email-settings-submenu');
1280
1281 if (emailNavLink && emailSubmenu) {
1282 // Check if any Email Settings subsection is active
1283 const activeSubmenuLink = document.querySelector('.email-submenu-link.text-indigo-700');
1284 const isEmailSectionActive = emailNavLink.classList.contains('text-indigo-700');
1285 const isAnySubsectionActive = activeSubmenuLink !== null;
1286
1287 // If any subsection is active, show submenu permanently and disable hover effects
1288 if (isAnySubsectionActive || isEmailSectionActive) {
1289 emailSubmenu.style.display = 'block';
1290 emailSubmenu.style.opacity = '1';
1291 emailSubmenu.style.visibility = 'visible';
1292 emailSubmenu.style.maxHeight = '200px';
1293 emailSubmenu.classList.add('show');
1294 return; // Exit early, no hover effects needed
1295 }
1296
1297 // Only add hover effects if no subsection is active
1298 // Show submenu on hover
1299 emailNavLink.addEventListener('mouseenter', function() {
1300 emailSubmenu.style.display = 'block';
1301 emailSubmenu.classList.add('show');
1302 setTimeout(() => {
1303 emailSubmenu.style.opacity = '1';
1304 emailSubmenu.style.visibility = 'visible';
1305 emailSubmenu.style.maxHeight = '200px';
1306 }, 10);
1307 });
1308
1309 // Hide submenu when mouse leaves
1310 emailNavLink.addEventListener('mouseleave', function() {
1311 // Only hide if not hovering over submenu
1312 setTimeout(() => {
1313 if (!emailSubmenu.matches(':hover')) {
1314 emailSubmenu.style.opacity = '0';
1315 emailSubmenu.style.visibility = 'hidden';
1316 emailSubmenu.style.maxHeight = '0';
1317 emailSubmenu.classList.remove('show');
1318 setTimeout(() => {
1319 emailSubmenu.style.display = 'none';
1320 }, 200);
1321 }
1322 }, 100);
1323 });
1324
1325 // Keep submenu open when hovering over submenu items
1326 emailSubmenu.addEventListener('mouseenter', function() {
1327 emailSubmenu.style.opacity = '1';
1328 emailSubmenu.style.visibility = 'visible';
1329 emailSubmenu.style.maxHeight = '200px';
1330 emailSubmenu.classList.add('show');
1331 });
1332
1333 emailSubmenu.addEventListener('mouseleave', function() {
1334 emailSubmenu.style.opacity = '0';
1335 emailSubmenu.style.visibility = 'hidden';
1336 emailSubmenu.style.maxHeight = '0';
1337 emailSubmenu.classList.remove('show');
1338 setTimeout(() => {
1339 emailSubmenu.style.display = 'none';
1340 }, 200);
1341 });
1342 }
1343 });
1344 </script>
1345
1346 <style>
1347 #screen-meta-links{
1348 display:none;
1349 }
1350 /* Email Settings Submenu Styles for Settings Page */
1351 .email-settings-submenu {
1352 transition: all 0.3s ease;
1353 overflow: hidden;
1354 }
1355
1356 .email-settings-submenu.show {
1357 display: block !important;
1358 }
1359
1360 .email-settings-submenu.show ~ .nav-link .fas.fa-chevron-down,
1361 .email-settings-submenu:hover ~ .nav-link .fas.fa-chevron-down {
1362 transform: rotate(180deg);
1363 }
1364
1365 /* Custom color for settings navigation icons */
1366 .nav-link:hover svg {
1367 color: rgb(79 70 229) !important;
1368 }
1369
1370 /* Active state icon color */
1371 .bg-indigo-50 svg {
1372 color: rgb(79 70 229) !important;
1373 }
1374
1375 /* Hover state for non-active items */
1376 .nav-link:hover:not(.bg-indigo-50) svg {
1377 color: rgb(79 70 229) !important;
1378 }
1379
1380 /* Email submenu icon colors */
1381 .email-submenu-link:hover svg {
1382 color: rgb(79 70 229) !important;
1383 }
1384
1385 .email-submenu-link.bg-indigo-50 svg {
1386 color: rgb(79 70 229) !important;
1387 }
1388 </style>
1389