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

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

2,124 lines 104.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings Controller Class
4 *
5 * Handles configuration, display and storage of all plugin settings
6 *
7 * @package Easy_Invoice
8 * @subpackage Controllers
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Controllers;
13
14 use EasyInvoice\Constants\PagesSlugs;
15 use EasyInvoice\Helpers\CurrencyHelper;
16
17 /**
18 * SettingsController handles all settings-related functionality
19 */
20 class SettingsController extends BaseController {
21 /**
22 * Settings cache
23 *
24 * @var array
25 */
26 private $settings_cache = null;
27
28 /**
29 * Settings resolved once per request. Every money formatter used to
30 * construct its own controller and rebuild the whole settings array (the
31 * config tree with every translated label) — around a millisecond per
32 * formatted amount, ten seconds on a 4,500-line statement.
33 *
34 * @var array|null
35 */
36 private static $shared_settings_cache = null;
37
38 /**
39 * Drop the per-request settings cache (any easy_invoice_* option changed).
40 */
41 public static function flushSharedCache(): void {
42 self::$shared_settings_cache = null;
43 }
44
45 /**
46 * Settings configuration cache
47 *
48 * @var array
49 */
50 private $config_cache = null;
51
52 /**
53 * Clear settings cache
54 *
55 * @return void
56 */
57 public function clearCache(): void {
58 $this->config_cache = null;
59 // Also clear any WordPress object cache
60 wp_cache_delete('easy_invoice_settings_config', 'easy_invoice');
61 // Force clear any other caches
62 if (function_exists('wp_cache_flush')) {
63 wp_cache_flush();
64 }
65 }
66
67 /**
68 * Initialize the controller
69 *
70 * @return void
71 */
72 public function init() {
73 // Add AJAX handlers
74 add_action('wp_ajax_easy_invoice_save_settings', [$this, 'saveSettings']);
75 add_action('wp_ajax_easy_invoice_test_email', [$this, 'testEmail']);
76 add_action('wp_ajax_easy_invoice_test_template_email', [$this, 'testTemplateEmail']);
77 add_action('wp_ajax_easy_invoice_test_payment_reminder_email', [$this, 'testPaymentReminderEmail']);
78 add_action('wp_ajax_regenerate_invoice_numbers', [$this, 'ajaxRegenerateInvoiceNumbers']);
79 add_action('wp_ajax_regenerate_quote_numbers', [$this, 'ajaxRegenerateQuoteNumbers']);
80
81 // Clear cache on admin init to ensure new fields are recognized
82 add_action('admin_init', [$this, 'clearCache']);
83
84 // Enqueue admin scripts
85 add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
86
87 // Initialize settings
88 $this->initializeSettings();
89 }
90
91
92
93 /**
94 * Enqueue admin scripts and styles for the settings page.
95 *
96 * @param string $hook_suffix The current admin page.
97 * @return void
98 */
99 public function enqueueAdminScripts($hook_suffix) {
100 // Check if we are on the Easy Invoice settings page.
101 if (empty($hook_suffix) || strpos($hook_suffix, PagesSlugs::SETTINGS) === false) {
102 return;
103 }
104
105 wp_enqueue_script('jquery-ui-sortable');
106 wp_enqueue_media(); // For the logo uploader
107
108 // Enqueue Select2 for enhancing multiselect fields
109 wp_enqueue_style(
110 'select2',
111 EASY_INVOICE_URL . 'assets/lib/select2/select2.min.css',
112 [],
113 '4.0.13'
114 );
115
116
117 wp_enqueue_script(
118 'select2',
119 EASY_INVOICE_URL . 'assets/lib/select2/select2.min.js',
120 ['jquery'],
121 '4.0.13',
122 true
123 );
124
125 // Enqueue confirmation modal for premium features
126 wp_enqueue_script(
127 'easy-invoice-confirmation-modal',
128 EASY_INVOICE_URL . 'assets/js/confirmation-modal.js',
129 ['jquery'],
130 EASY_INVOICE_VERSION,
131 true
132 );
133
134 // Enqueue toast notification system
135 wp_enqueue_script(
136 'easy-invoice-toast',
137 EASY_INVOICE_URL . 'assets/js/easy-invoice-toast.js',
138 ['jquery'],
139 EASY_INVOICE_VERSION,
140 true
141 );
142
143 $script_handle = 'easy-invoice-settings';
144 wp_enqueue_script(
145 $script_handle,
146 EASY_INVOICE_URL . 'assets/js/settings.js',
147 ['jquery', 'jquery-ui-sortable', 'wp-util', 'select2', 'easy-invoice-confirmation-modal', 'media-views'],
148 EASY_INVOICE_VERSION,
149 true
150 );
151
152 // Enqueue the new settings.css only on the settings page
153 wp_enqueue_style(
154 'easy-invoice-settings',
155 EASY_INVOICE_URL . 'assets/css/settings.css',
156 [],
157 EASY_INVOICE_VERSION
158 );
159
160 $localize_data = apply_filters('easy_invoice_settings_js_data', [
161 'ajaxurl' => admin_url('admin-ajax.php'),
162 'nonce' => wp_create_nonce('easy_invoice_settings'),
163 'saveSuccess' => __('Settings saved successfully', 'easy-invoice'),
164 'saveError' => __('Error saving settings', 'easy-invoice'),
165 ]);
166
167 wp_localize_script($script_handle, 'easyInvoiceSettings', $localize_data);
168
169 do_action('easy_invoice_after_admin_scripts', $hook_suffix);
170 }
171
172 /**
173 * Check if Easy Invoice Pro is active
174 *
175 * @return bool
176 */
177 private function isProActive(): bool {
178 return easy_invoice_has_pro();
179 }
180
181 /**
182 * Get settings fields configuration
183 *
184 * @return array
185 */
186 public function get_settings_fields_config() {
187 // Use cached config if available
188 if (is_array($this->config_cache)) {
189 return $this->config_cache;
190 }
191
192 $config = [
193 'company' => [
194 'title' => __('Company Information', 'easy-invoice'),
195 'description' => __('Set up your company details', 'easy-invoice'),
196 'icon' => 'fas fa-building',
197 'fields' => [
198 'easy_invoice_company_name' => ['label' => __('Company Name', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6' ],
199 'easy_invoice_company_email' => ['label' => __('Email', 'easy-invoice'), 'type' => 'email', 'default' => '', 'col_span' => 'sm:col-span-6' ],
200 'easy_invoice_company_phone' => ['label' => __('Phone Number', 'easy-invoice'), 'type' => 'tel', 'default' => '', 'col_span' => 'sm:col-span-6' ],
201 'easy_invoice_company_website' => ['label' => __('Website', 'easy-invoice'), 'type' => 'url', 'default' => '', 'col_span' => 'sm:col-span-6' ],
202 // Both are required by EN 16931 and are what makes an intra-EU
203 // reverse charge determinable. See Services\TaxTreatment.
204 'easy_invoice_company_vat_number' => ['label' => __('VAT number', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-3', 'description' => __('Your VAT identifier, including the country prefix — for example DE123456789.', 'easy-invoice') ],
205 'easy_invoice_company_country' => ['label' => __('Country', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-3', 'description' => __('Two-letter country code, for example DE, FR or GB.', 'easy-invoice') ],
206 'easy_invoice_company_address' => ['label' => __('Address', 'easy-invoice'), 'type' => 'textarea', 'default' => '', 'col_span' => 'sm:col-span-6' ],
207 'easy_invoice_tax_number' => ['label' => __('Tax ID / VAT Number', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6' ],
208 'easy_invoice_company_logo' => ['label' => __('Company Logo', 'easy-invoice'), 'type' => 'image', 'default' => '', 'col_span' => 'sm:col-span-6' ],
209 ]
210 ],
211 'invoice' => [
212 'title' => __('Invoice Settings', 'easy-invoice'),
213 'description' => __('Configure invoice-specific settings and preferences', 'easy-invoice'),
214 'icon' => 'fas fa-file-invoice',
215 'fields' => [
216 'easy_invoice_invoice_prefix' => [
217 'label' => __('Invoice Prefix', 'easy-invoice'),
218 'type' => 'text',
219 'default' => 'EIIN_',
220 'description' => __('Prefix for invoice numbers (e.g., EIIN_, INV-)', 'easy-invoice'),
221 'col_span' => 'sm:col-span-2'
222 ],
223 'easy_invoice_next_invoice_number' => [
224 'label' => __('Next Invoice Number', 'easy-invoice'),
225 'type' => 'number',
226 'default' => '1',
227 'description' => __('Set the next invoice number to be generated. This will be the number of the next invoice created.', 'easy-invoice'),
228 'col_span' => 'sm:col-span-2',
229 ],
230 'easy_invoice_regenerate_invoice_numbers' => [
231 'label' => __('Regenerate Invoice Numbers', 'easy-invoice'),
232 'type' => 'button',
233 'button_text' => __('Regenerate All Invoice Numbers', 'easy-invoice'),
234 'button_class' => 'button button-secondary',
235 'description' => __('Click to regenerate all invoice numbers starting from the Last Invoice Number. This will update all existing invoices with new sequential numbers.', 'easy-invoice'),
236 'col_span' => 'sm:col-span-2',
237 'ajax_action' => 'regenerate_invoice_numbers'
238 ],
239 'easy_invoice_invoice_show_adjust_field' => [
240 'label' => __('Show/Hide Adjust Field', 'easy-invoice'),
241 'type' => 'checkbox',
242 'default' => 'yes',
243 'description' => __('Enable/Disable Adjust field. Tick this to show adjust field', 'easy-invoice'),
244 'col_span' => 'sm:col-span-6'
245 ],
246 'easy_invoice_invoice_terms_conditions' => [
247 'label' => __('Terms & Conditions', 'easy-invoice'),
248 'type' => 'wp_editor',
249 'default' => __('Payment is due within 30 days from date of invoice', 'easy-invoice'),
250 'description' => __('Terms and conditions that will be displayed on your invoice', 'easy-invoice'),
251 'col_span' => 'sm:col-span-6'
252 ],
253 'easy_invoice_invoice_footer_text' => [
254 'label' => __('Footer Text', 'easy-invoice'),
255 'type' => 'wp_editor',
256 'default' => '',
257 'description' => __('You can modify your invoice footer text from here. HTML tags supports: a, br, em, strong, hr, p, h1 to h4', 'easy-invoice'),
258 'col_span' => 'sm:col-span-6'
259 ],
260 ],
261 ],
262 'quote' => [
263 'title' => __('Quote Settings', 'easy-invoice'),
264 'description' => __('Configure quote-specific settings and preferences', 'easy-invoice'),
265 'icon' => 'fas fa-file-contract',
266 'fields' => [
267 'easy_invoice_quote_prefix' => [
268 'label' => __('Quote Prefix', 'easy-invoice'),
269 'type' => 'text',
270 'default' => 'EIQN_',
271 'description' => __('Prefix for quote numbers (e.g., EIQN_, QT-)', 'easy-invoice'),
272 'col_span' => 'sm:col-span-2'
273 ],
274 'easy_invoice_next_quote_number' => [
275 'label' => __('Next Quote Number', 'easy-invoice'),
276 'type' => 'number',
277 'default' => '1',
278 'description' => __('Set the next quote number to be generated. This will be the number of the next quote created.', 'easy-invoice'),
279 'col_span' => 'sm:col-span-2',
280 ],
281 'easy_invoice_regenerate_quote_numbers' => [
282 'label' => __('Regenerate Quote Numbers', 'easy-invoice'),
283 'type' => 'button',
284 'button_text' => __('Regenerate All Quote Numbers', 'easy-invoice'),
285 'button_class' => 'button button-secondary',
286 'description' => __('Click to regenerate all quote numbers starting from the Next Quote Number. This will update all existing quotes with new sequential numbers.', 'easy-invoice'),
287 'col_span' => 'sm:col-span-2',
288 'ajax_action' => 'regenerate_quote_numbers'
289 ],
290 'easy_invoice_quote_show_adjust_field' => [
291 'label' => __('Show/Hide Adjust Field', 'easy-invoice'),
292 'type' => 'checkbox',
293 'default' => 'yes',
294 'description' => __('Enable/Disable Adjust field. Tick this to show adjust field', 'easy-invoice'),
295 'col_span' => 'sm:col-span-6'
296 ],
297 'easy_invoice_quote_terms_conditions' => [
298 'label' => __('Terms & Conditions', 'easy-invoice'),
299 'type' => 'wp_editor',
300 'default' => __('This quote has a fixed price. Upon acceptance, we kindly ask for a 25% deposit prior to initiating the work.', 'easy-invoice'),
301 'description' => __('Terms and conditions that will be displayed on your quote!', 'easy-invoice'),
302 'col_span' => 'sm:col-span-6'
303 ],
304 'easy_invoice_quote_footer_text' => [
305 'label' => __('Footer Text', 'easy-invoice'),
306 'type' => 'wp_editor',
307 'default' => __('Thank you for your business.', 'easy-invoice'),
308 'description' => __('You can modify your quote footer text from here. HTML tags supports: a, br, em, strong, hr, p, h1 to h4', 'easy-invoice'),
309 'col_span' => 'sm:col-span-6'
310 ],
311 'easy_invoice_quote_accept_button' => [
312 'label' => __('Accept quote button', 'easy-invoice'),
313 'type' => 'checkbox',
314 'default' => 'yes',
315 'description' => __('Show/hide accept quote button on quotes.', 'easy-invoice'),
316 'col_span' => 'sm:col-span-6'
317 ],
318 'easy_invoice_quote_accept_action' => [
319 'label' => __('Accept quote button action', 'easy-invoice'),
320 'type' => 'select',
321 'default' => 'convert',
322 'options' => [
323 'convert' => __('Convert quote to invoice - Draft', 'easy-invoice'),
324 'convert_available' => __('Convert quote to invoice - Available', 'easy-invoice'),
325 'convert_send' => __('Convert quote to invoice and send to client - Available', 'easy-invoice'),
326 'duplicate' => __('Create new invoice, keep quote as-is - Draft', 'easy-invoice'),
327 'duplicate_send' => __('Create new invoice and send to client, keep quote as-is - Available', 'easy-invoice'),
328 'do_nothing' => __('Do nothing', 'easy-invoice'),
329 ],
330 'description' => __('Upon the client clicking the "accept quote" button, the subsequent action will be activated.', 'easy-invoice'),
331 'col_span' => 'sm:col-span-6'
332 ],
333 'easy_invoice_quote_accept_text' => [
334 'label' => __('Accept Quote Text', 'easy-invoice'),
335 'type' => 'wp_editor',
336 'default' => __('Important: When you accept this Quote, an Invoice will be created automatically. This will form a legally binding contract.', 'easy-invoice'),
337 'description' => __('This information tells your client what happens once they accept the Quote', 'easy-invoice'),
338 'col_span' => 'sm:col-span-6'
339 ],
340 'easy_invoice_quote_accepted_message' => [
341 'label' => __('Accepted Quote Message', 'easy-invoice'),
342 'type' => 'wp_editor',
343 'default' => __('You\'ve confirmed the Quote.<br>We\'ll get in touch with you shortly.', 'easy-invoice'),
344 'description' => __('If the client accepts the Quote, display this message.', 'easy-invoice'),
345 'col_span' => 'sm:col-span-6'
346 ],
347 'easy_invoice_quote_decline_reason_required' => [
348 'label' => __('Decline Reason Required', 'easy-invoice'),
349 'type' => 'checkbox',
350 'default' => 'no',
351 'description' => __('Make the \'Reason for declining\' field mandatory when rejecting.', 'easy-invoice'),
352 'col_span' => 'sm:col-span-6'
353 ],
354 'easy_invoice_quote_declined_message' => [
355 'label' => __('Declined Quote Message', 'easy-invoice'),
356 'type' => 'wp_editor',
357 'default' => '',
358 'description' => __('Message to display if client declines the Quote', 'easy-invoice'),
359 'col_span' => 'sm:col-span-6'
360 ],
361 ],
362 ],
363 'currency' => [
364 'title' => __('Currency Settings', 'easy-invoice'),
365 'description' => __('Configure currency display preferences', 'easy-invoice'),
366 'icon' => 'fas fa-dollar-sign',
367 'fields' => [
368 'easy_invoice_currency_code' => [
369 'label' => __('Currency & Symbol', 'easy-invoice'),
370 'type' => 'select',
371 'default' => 'USD',
372 'options' => CurrencyHelper::getCurrencyOptions(),
373 'col_span' => 'sm:col-span-6',
374 'description' => __('Select your preferred currency. The symbol will be automatically set.', 'easy-invoice')
375 ],
376 'easy_invoice_currency_position' => [
377 'label' => __('Symbol Position', 'easy-invoice'),
378 'type' => 'select',
379 'default' => 'left',
380 'options' => [
381 'left' => __('Left', 'easy-invoice'),
382 'right' => __('Right', 'easy-invoice'),
383 'left_space' => __('Left with space', 'easy-invoice'),
384 'right_space' => __('Right with space', 'easy-invoice'),
385 ],
386 'col_span' => 'sm:col-span-3'
387 ],
388 'easy_invoice_currency_symbol_type' => [
389 'label' => __('Currency Symbol Type', 'easy-invoice'),
390 'type' => 'select',
391 'default' => 'symbol',
392 'options' => [
393 'code' => __('Currency Code', 'easy-invoice'),
394 'symbol' => __('Currency Symbol', 'easy-invoice'),
395 ],
396 'description' => __('Choose whether to display currency code (USD) or currency symbol ($)', 'easy-invoice'),
397 'col_span' => 'sm:col-span-3'
398 ],
399 'easy_invoice_thousands_separator' => ['label' => __('Thousands Separator', 'easy-invoice'), 'type' => 'text', 'default' => ',', 'col_span' => 'sm:col-span-3' ],
400 'easy_invoice_decimal_separator' => ['label' => __('Decimal Separator', 'easy-invoice'), 'type' => 'text', 'default' => '.', 'col_span' => 'sm:col-span-3' ],
401 'easy_invoice_decimal_precision' => ['label' => __('Decimal Places', 'easy-invoice'), 'type' => 'number', 'default' => '2', 'min' => 0, 'max' => 4, 'col_span' => 'sm:col-span-3' ],
402 ]
403 ],
404 'tax' => [
405 'title' => __('Tax Settings', 'easy-invoice'),
406 'description' => __('Configure tax rates and preferences', 'easy-invoice'),
407 'icon' => 'fas fa-percent',
408 'fields' => [
409 'easy_invoice_tax_enabled' => ['label' => __('Enable Tax', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6' ],
410 'easy_invoice_tax_entry_method' => [
411 'label' => __('How do you enter tax?', 'easy-invoice'),
412 'type' => 'select',
413 'default' => 'exclusive',
414 'options' => [
415 'inclusive' => __('I will enter price inclusive of tax', 'easy-invoice'),
416 'exclusive' => __('I will enter price exclusive of tax', 'easy-invoice'),
417 ],
418 'description' => __('Choose how you want to enter prices - with or without tax included', 'easy-invoice'),
419 'col_span' => 'sm:col-span-6'
420 ],
421 'easy_invoice_tax_rate' => ['label' => __('Default Tax Rate (%)', 'easy-invoice'), 'type' => 'number', 'default' => '0', 'step' => '0.01', 'min' => '0', 'max' => '100', 'col_span' => 'sm:col-span-3' ],
422 'easy_invoice_tax_name' => ['label' => __('Tax Name', 'easy-invoice'), 'type' => 'text', 'default' => __('Tax', 'easy-invoice'), 'col_span' => 'sm:col-span-3' ],
423 ]
424 ],
425 'payment' => [
426 'title' => __('Payment Methods', 'easy-invoice'),
427 'description' => __('Configure and order available payment methods.', 'easy-invoice'),
428 'icon' => 'fas fa-credit-card',
429 'is_special_section' => true,
430 'gateways' => $this->getGatewaySettingsConfigs(),
431 ],
432 'email' => [
433 'title' => __('Email Settings', 'easy-invoice'),
434 'description' => __('Configure email sending options and templates', 'easy-invoice'),
435 'icon' => 'fas fa-envelope',
436 'subsections' => [
437 'general' => [
438 'title' => __('General Email Settings', 'easy-invoice'),
439 'description' => __('Configure general email sending options', 'easy-invoice'),
440 'fields' => [
441 'easy_invoice_email_from_name' => ['label' => __('From Name', 'easy-invoice'), 'type' => 'text', 'default' => get_bloginfo('name') ?: 'Easy Invoice', 'col_span' => 'sm:col-span-3'],
442 'easy_invoice_email_from_address' => ['label' => __('From Email Address', 'easy-invoice'), 'type' => 'email', 'default' => get_bloginfo('admin_email') ?: get_option('admin_email', ''), 'col_span' => 'sm:col-span-3'],
443 'easy_invoice_email_reply_to' => ['label' => __('Reply-To Email', 'easy-invoice'), 'type' => 'email', 'default' => '', 'col_span' => 'sm:col-span-3'],
444 'easy_invoice_email_reply_to_name' => ['label' => __('Reply-To Name', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-3'],
445 'easy_invoice_enable_email_styling' => ['label' => __('Enable HTML Emails', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
446 'easy_invoice_bcc_admin' => ['label' => __('BCC Admin on All Emails', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6'],
447 // Off by default: attaching a document changes what every
448 // customer receives and makes messages much larger, which
449 // some SMTP relays reject. See EmailManager::shouldAttachInvoicePdf().
450 'easy_invoice_attach_pdf_to_email' => ['label' => __('Attach a PDF copy to invoice and quote emails', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6', 'description' => __('Generates the document on the server and attaches it. The PDF contains real, selectable text rather than an image.', 'easy-invoice')],
451 'easy_invoice_pdf_paper_size' => ['label' => __('PDF paper size', 'easy-invoice'), 'type' => 'select', 'default' => 'a4', 'options' => ['a4' => 'A4', 'letter' => 'Letter', 'legal' => 'Legal'], 'col_span' => 'sm:col-span-3'],
452 'easy_invoice_pdf_download_method' => ['label' => __('"Download as PDF" button', 'easy-invoice'), 'type' => 'select', 'default' => 'browser', 'options' => ['browser' => __('Exactly as shown on screen (rendered in the browser)', 'easy-invoice'), 'server' => __('Generated on the server (real text, smaller file)', 'easy-invoice')], 'col_span' => 'sm:col-span-3', 'description' => __('The browser capture reproduces the page pixel for pixel — every design, custom template and watermark. The server file is selectable text and a fraction of the size. Emails, the API and e-invoicing always use the server.', 'easy-invoice')],
453 'easy_invoice_pdf_layout' => ['label' => __('Server-generated PDF layout', 'easy-invoice'), 'type' => 'select', 'default' => 'design', 'options' => ['design' => __('Match the selected invoice / quote design', 'easy-invoice'), 'plain' => __('Plain print layout', 'easy-invoice')], 'col_span' => 'sm:col-span-3', 'description' => __('Applies to PDFs the server produces: email attachments, the API, e-invoicing, and the download button when set to "server". The design is the template chosen on the document; the plain layout is a neutral table-based page.', 'easy-invoice')],
454 'easy_invoice_admin_email' => ['label' => __('Admin Email for BCC', 'easy-invoice'), 'type' => 'email', 'default' => get_option('admin_email', ''), 'col_span' => 'sm:col-span-6'],
455 'easy_invoice_email_logo' => ['label' => __('Email Logo URL', 'easy-invoice'), 'type' => 'url', 'default' => '', 'col_span' => 'sm:col-span-6'],
456 'easy_invoice_email_footer_text' => ['label' => __('Email Footer Text', 'easy-invoice'), 'type' => 'textarea', 'default' => '', 'col_span' => 'sm:col-span-6'],
457 'easy_invoice_test_email' => [
458 'label' => __('Test Email', 'easy-invoice'),
459 'type' => 'test_email',
460 'default' => '',
461 'col_span' => 'sm:col-span-6',
462 'description' => __('Send a test email to verify your email configuration is working correctly.', 'easy-invoice')
463 ],
464 ]
465 ],
466 'invoice_available' => [
467 'title' => __('Invoice Available Email', 'easy-invoice'),
468 'description' => __('Configure email sent when an invoice is available for the client', 'easy-invoice'),
469 'fields' => [
470 'easy_invoice_invoice_email_enabled' => ['label' => __('Enable Invoice Available Email', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
471 'easy_invoice_invoice_email_subject' => ['label' => __('Subject', 'easy-invoice'), 'type' => 'text', 'default' => __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'), 'col_span' => 'sm:col-span-6'],
472 'easy_invoice_invoice_email_body' => ['label' => __('Email Body', 'easy-invoice'), 'type' => 'wp_editor', 'default' => \EasyInvoice\Services\EmailManager::defaultTemplate('invoice'), 'col_span' => 'sm:col-span-6'],
473 'easy_invoice_invoice_test_template' => [
474 'label' => __('Test Invoice Email', 'easy-invoice'),
475 'type' => 'test_template_email',
476 'default' => '',
477 'col_span' => 'sm:col-span-6',
478 'description' => __('Send a test invoice email to verify the template and configuration.', 'easy-invoice'),
479 'template_type' => 'invoice_available'
480 ],
481 ]
482 ],
483 'quote_available' => [
484 'title' => __('Quote Available Email', 'easy-invoice'),
485 'description' => __('Configure email sent when a quote is available for the client', 'easy-invoice'),
486 'fields' => [
487 'easy_invoice_quote_email_enabled' => ['label' => __('Enable Quote Available Email', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
488 'easy_invoice_quote_email_subject' => ['label' => __('Subject', 'easy-invoice'), 'type' => 'text', 'default' => __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'), 'col_span' => 'sm:col-span-6'],
489 'easy_invoice_quote_email_body' => ['label' => __('Email Body', 'easy-invoice'), 'type' => 'wp_editor', 'default' => \EasyInvoice\Services\EmailManager::defaultTemplate('quote'), 'col_span' => 'sm:col-span-6'],
490 'easy_invoice_quote_test_template' => [
491 'label' => __('Test Quote Email', 'easy-invoice'),
492 'type' => 'test_template_email',
493 'default' => '',
494 'col_span' => 'sm:col-span-6',
495 'description' => __('Send a test quote email to verify the template and configuration.', 'easy-invoice'),
496 'template_type' => 'quote_available'
497 ],
498 ]
499 ],
500 'payment_received' => [
501 'title' => __('Payment Received Email', 'easy-invoice'),
502 'description' => __('Configure email sent when a payment is received', 'easy-invoice'),
503 'fields' => [
504 'easy_invoice_payment_email_enabled' => ['label' => __('Enable Payment Received Email', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
505 'easy_invoice_payment_email_subject' => ['label' => __('Subject', 'easy-invoice'), 'type' => 'text', 'default' => __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'), 'col_span' => 'sm:col-span-6'],
506 'easy_invoice_payment_email_body' => ['label' => __('Email Body', 'easy-invoice'), 'type' => 'wp_editor', 'default' => \EasyInvoice\Services\EmailManager::defaultTemplate('payment'), 'col_span' => 'sm:col-span-6'],
507 'easy_invoice_payment_test_template' => [
508 'label' => __('Test Payment Email', 'easy-invoice'),
509 'type' => 'test_template_email',
510 'default' => '',
511 'col_span' => 'sm:col-span-6',
512 'description' => __('Send a test payment email to verify the template and configuration.', 'easy-invoice'),
513 'template_type' => 'payment_received'
514 ],
515 ]
516 ],
517 ]
518 ],
519 'text_settings' => [
520 'title' => __('Text Settings', 'easy-invoice'),
521 'description' => __('Customize text labels for invoices and quotes. Perfect for multi-language support.', 'easy-invoice'),
522 'icon' => '<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">
523 <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"/>
524 <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
525 <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
526 <path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
527 <path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
528 </svg>',
529 'fields' => [
530 // Invoice Text Settings
531 'easy_invoice_text_invoice' => ['label' => __('Invoice', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoice', 'easy-invoice'), 'col_span' => '', 'description' => __('Label for single invoice', 'easy-invoice')],
532 'easy_invoice_text_invoices' => ['label' => __('Invoices', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoices', 'easy-invoice'), 'col_span' => '', 'description' => __('Label for multiple invoices', 'easy-invoice')],
533 'easy_invoice_text_to' => ['label' => __('To', 'easy-invoice'), 'type' => 'text', 'default' => __('To', 'easy-invoice'), 'col_span' => '', 'description' => __('To label on invoice/quote', 'easy-invoice')],
534 'easy_invoice_text_invoice_number' => ['label' => __('Invoice Number', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoice Number', 'easy-invoice'), 'col_span' => '', 'description' => __('Invoice number label', 'easy-invoice')],
535 'easy_invoice_text_invoice_date' => ['label' => __('Invoice Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoice Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Invoice date label', 'easy-invoice')],
536 'easy_invoice_text_due_date' => ['label' => __('Due Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Due Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Due date label', 'easy-invoice')],
537 'easy_invoice_text_total_due' => ['label' => __('Total Due', 'easy-invoice'), 'type' => 'text', 'default' => __('Total Due', 'easy-invoice'), 'col_span' => '', 'description' => __('Total due label', 'easy-invoice')],
538 'easy_invoice_text_qty' => ['label' => __('Qty', 'easy-invoice'), 'type' => 'text', 'default' => __('Qty', 'easy-invoice'), 'col_span' => '', 'description' => __('Quantity label', 'easy-invoice')],
539 'easy_invoice_text_service' => ['label' => __('Service', 'easy-invoice'), 'type' => 'text', 'default' => __('Service', 'easy-invoice'), 'col_span' => '', 'description' => __('Service label', 'easy-invoice')],
540 'easy_invoice_text_rate_price' => ['label' => __('Rate', 'easy-invoice'), 'type' => 'text', 'default' => __('Rate', 'easy-invoice'), 'col_span' => '', 'description' => __('Rate or price label', 'easy-invoice')],
541 'easy_invoice_text_adjust' => ['label' => __('Adjust', 'easy-invoice'), 'type' => 'text', 'default' => __('Adjust', 'easy-invoice'), 'col_span' => '', 'description' => __('Adjust label', 'easy-invoice')],
542 'easy_invoice_text_sub_total' => ['label' => __('Sub Total', 'easy-invoice'), 'type' => 'text', 'default' => __('Sub Total', 'easy-invoice'), 'col_span' => '', 'description' => __('Sub total label', 'easy-invoice')],
543 'easy_invoice_text_total' => ['label' => __('Total', 'easy-invoice'), 'type' => 'text', 'default' => __('Total', 'easy-invoice'), 'col_span' => '', 'description' => __('Total label', 'easy-invoice')],
544 'easy_invoice_text_line_total' => ['label' => __('Line total', 'easy-invoice'), 'type' => 'text', 'default' => __('Total', 'easy-invoice'), 'col_span' => '', 'description' => __('Heading of the line-item amount column', 'easy-invoice')],
545 'easy_invoice_text_tax' => ['label' => __('Tax', 'easy-invoice'), 'type' => 'text', 'default' => __('Tax', 'easy-invoice'), 'col_span' => '', 'description' => __('Tax label', 'easy-invoice')],
546 'easy_invoice_text_discount' => ['label' => __('Discount', 'easy-invoice'), 'type' => 'text', 'default' => __('Discount', 'easy-invoice'), 'col_span' => '', 'description' => __('Discount label', 'easy-invoice')],
547 'easy_invoice_text_print' => ['label' => __('Print', 'easy-invoice'), 'type' => 'text', 'default' => __('Print', 'easy-invoice'), 'col_span' => '', 'description' => __('Print button text', 'easy-invoice')],
548 'easy_invoice_text_download_pdf' => ['label' => __('Download as PDF', 'easy-invoice'), 'type' => 'text', 'default' => __('Download as PDF', 'easy-invoice'), 'col_span' => '', 'description' => __('Download PDF button text', 'easy-invoice')],
549 'easy_invoice_text_send_email' => ['label' => __('Send Email', 'easy-invoice'), 'type' => 'text', 'default' => __('Send Email', 'easy-invoice'), 'col_span' => '', 'description' => __('Send email button text', 'easy-invoice')],
550 'easy_invoice_text_pay_now' => ['label' => __('Pay Now', 'easy-invoice'), 'type' => 'text', 'default' => __('Pay Now', 'easy-invoice'), 'col_span' => '', 'description' => __('Pay now button text', 'easy-invoice')],
551
552 // Quote Text Settings
553 'easy_invoice_text_quote' => ['label' => __('Quote', 'easy-invoice'), 'type' => 'text', 'default' => __('Quote', 'easy-invoice'), 'col_span' => '', 'description' => __('Label for single quote', 'easy-invoice')],
554 'easy_invoice_text_quote_number' => ['label' => __('Quote Number', 'easy-invoice'), 'type' => 'text', 'default' => __('Quote Number', 'easy-invoice'), 'col_span' => '', 'description' => __('Quote number label', 'easy-invoice')],
555 'easy_invoice_text_accept_quote' => ['label' => __('Accept Quote', 'easy-invoice'), 'type' => 'text', 'default' => __('Accept Quote', 'easy-invoice'), 'col_span' => '', 'description' => __('Accept quote button text', 'easy-invoice')],
556 'easy_invoice_text_decline_quote' => ['label' => __('Decline Quote', 'easy-invoice'), 'type' => 'text', 'default' => __('Decline Quote', 'easy-invoice'), 'col_span' => '', 'description' => __('Decline quote button text', 'easy-invoice')],
557 'easy_invoice_text_decline_reason' => ['label' => __('Reason for declining', 'easy-invoice'), 'type' => 'text', 'default' => __('Reason for declining', 'easy-invoice'), 'col_span' => '', 'description' => __('Decline reason label', 'easy-invoice')],
558 'easy_invoice_text_valid_until' => ['label' => __('Valid Until Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Valid Until Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Valid until date label', 'easy-invoice')],
559 'easy_invoice_text_quote_date' => ['label' => __('Quote Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Quote Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Quote date label', 'easy-invoice')],
560 ]
561 ],
562 'advanced' => [
563 'title' => __('Advanced Settings', 'easy-invoice'),
564 'description' => __('Configure system preferences', 'easy-invoice'),
565 'icon' => 'fas fa-cog',
566 'fields' => [
567 'easy_invoice_date_format' => [
568 'label' => __('Date Format', 'easy-invoice'),
569 'type' => 'select',
570 'default' => 'us',
571 'options' => [
572 'us' => __('MM/DD/YYYY (US) - 01/15/2024', 'easy-invoice'),
573 'uk' => __('DD/MM/YYYY (UK) - 15/01/2024', 'easy-invoice'),
574 'iso' => __('YYYY-MM-DD (ISO) - 2024-01-15', 'easy-invoice'),
575 ],
576 'col_span' => 'sm:col-span-3'
577 ],
578
579 'easy_invoice_invoice_numbering' => ['label' => __('Auto-increment invoice numbers', 'easy-invoice'), 'type' => 'checkbox', 'description' => __('Automatically increment invoice numbers for new invoices', 'easy-invoice'), 'default' => 'yes', 'col_span' => 'sm:col-span-6' ],
580 'easy_invoice_payment_reminder_days' => ['label' => __('Payment Reminder Days', 'easy-invoice'), 'type' => 'number', 'default' => 3, 'col_span' => 'sm:col-span-3'],
581 'easy_invoice_remove_data_on_uninstall' => ['label' => __('Remove all data when the plugin is deleted', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6', 'description' => __('Off, deleting the plugin from the Plugins screen keeps every invoice, quote, payment, client record and setting, so reinstalling picks up where you left off. On, all of it is removed — invoices are tax records, so leave this off unless you are certain.', 'easy-invoice')],
582 ]
583 ],
584 ];
585
586 // Cache and filter the config
587 $this->config_cache = apply_filters('easy_invoice_settings_fields_config', $config);
588
589 return $this->config_cache;
590 }
591
592 /**
593 * Get all gateway settings configurations
594 *
595 * @return array Gateway settings configs
596 */
597 private function getGatewaySettingsConfigs(): array {
598 $gateway_configs = [];
599
600 try {
601 // Get gateway manager from the main plugin instance
602 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
603 $gateways = $gateway_manager->getGateways();
604
605 // Collect settings from each gateway
606 foreach ($gateways as $gateway_id => $gateway) {
607 try {
608 $gateway_configs[$gateway_id] = $gateway->getSettingsConfig();
609 } catch (\Exception $e) {
610 // Log the error but continue with other gateways
611 $gateway_configs[$gateway_id] = [];
612 }
613 }
614 } catch (\Exception $e) {
615 }
616
617 return $gateway_configs;
618 }
619
620 /**
621 * Prepare settings for display with proper filtering
622 *
623 * @param array $settings The settings array
624 * @return array The filtered settings
625 */
626 public function prepareSettingsForDisplay($settings) {
627 return apply_filters('easy_invoice_settings_for_display', $settings);
628 }
629
630 /**
631 * Display method implementation
632 *
633 * @param array $args Display arguments
634 * @return void
635 */
636 public function display(array $args = []) {
637 $page = isset($args['page']) ? $args['page'] : '';
638 $subsection = isset($args['subsection']) ? $args['subsection'] : '';
639
640 // Clear cache to ensure new fields are recognized
641 $this->clearCache();
642
643 switch ($page) {
644 case PagesSlugs::SETTINGS:
645 if (!empty($subsection)) {
646 // Handle email settings subsections
647 $this->displayEmailSettingsPage($subsection);
648 } else {
649 $this->displaySettingsPage();
650 }
651 break;
652
653 default:
654 $this->displaySettingsPage();
655 break;
656 }
657 }
658
659 /**
660 * Display email settings page for specific subsection
661 *
662 * @param string $subsection The subsection to display
663 * @return void
664 */
665 protected function displayEmailSettingsPage($subsection) {
666 // Get settings configuration
667 $settings_config = $this->get_settings_fields_config();
668 $settings = $this->getSettings();
669
670 // Filter to show only email settings
671 $email_config = $settings_config['email'] ?? [];
672
673 // Map subsection to the appropriate subsection in email config
674 $subsection_mapping = [
675 PagesSlugs::EMAIL_SETTINGS_GENERAL => 'general',
676 PagesSlugs::EMAIL_SETTINGS_INVOICE => 'invoice_available',
677 PagesSlugs::EMAIL_SETTINGS_QUOTE => 'quote_available',
678 PagesSlugs::EMAIL_SETTINGS_PAYMENT => 'payment_received',
679 PagesSlugs::EMAIL_SETTINGS_PAYMENT_REMINDER => 'payment_reminder',
680 ];
681
682 $target_subsection = $subsection_mapping[$subsection] ?? 'general';
683
684 // Create a modified config with only the target subsection
685 $filtered_config = [
686 'email' => [
687 'title' => __('Email Settings', 'easy-invoice'),
688 'description' => __('Configure email sending options and templates', 'easy-invoice'),
689 'icon' => 'fas fa-envelope',
690 'subsections' => [
691 $target_subsection => $email_config['subsections'][$target_subsection] ?? []
692 ]
693 ]
694 ];
695
696 // Include the settings page template with filtered config
697 include EASY_INVOICE_PLUGIN_DIR . 'templates/settings-page.php';
698 }
699
700 /**
701 * Display settings page
702 *
703 * @return void
704 */
705 protected function displaySettingsPage() {
706 $settings = $this->getSettings();
707
708 // Allow developers to modify settings before display
709 $settings = apply_filters('easy_invoice_before_display_settings', $settings);
710
711 // Ensure payment methods are set (this is critical for gateway checkboxes)
712 if (!isset($settings['easy_invoice_payment_methods']) || !is_array($settings['easy_invoice_payment_methods'])) {
713 $settings['easy_invoice_payment_methods'] = get_option('easy_invoice_payment_methods', []);
714 }
715
716 // Ensure gateway order is set
717 if (!isset($settings['easy_invoice_payment_gateway_order']) || !is_array($settings['easy_invoice_payment_gateway_order'])) {
718 $settings['easy_invoice_payment_gateway_order'] = get_option('easy_invoice_payment_gateway_order', []);
719 }
720
721 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
722 $all_gateways_unsorted = $gateway_manager->getGateways();
723 $gateway_order_option = $settings['easy_invoice_payment_gateway_order'];
724 $payment_methods_enabled = $settings['easy_invoice_payment_methods'];
725
726 $all_gateways_sorted = [];
727 if (!empty($gateway_order_option)) {
728 foreach ($gateway_order_option as $gateway_id) {
729 if (isset($all_gateways_unsorted[$gateway_id])) {
730 $all_gateways_sorted[$gateway_id] = $all_gateways_unsorted[$gateway_id];
731 unset($all_gateways_unsorted[$gateway_id]);
732 }
733 }
734 }
735 $all_gateways_sorted = array_merge($all_gateways_sorted, $all_gateways_unsorted);
736
737 $settings_config = $this->get_settings_fields_config();
738
739 // Prepare template variables
740 $template_args = apply_filters('easy_invoice_settings_template_args', [
741 'settings' => $settings,
742 'settings_config' => $settings_config,
743 'all_gateways_sorted' => $all_gateways_sorted,
744 'payment_methods_enabled' => $payment_methods_enabled
745 ]);
746
747 // Display the template with template args
748 $this->displayTemplate(
749 apply_filters('easy_invoice_settings_template_path', EASY_INVOICE_PLUGIN_DIR . 'templates/settings-page.php'),
750 $template_args
751 );
752
753 // Action for plugins to add their own content after the settings page
754 do_action('easy_invoice_after_settings_page');
755 }
756
757 /**
758 * Save payment settings
759 *
760 * @param array $posted_settings Posted settings
761 * @param array $response Response array
762 * @return void
763 */
764 private function savePaymentSettings($posted_settings, &$response) {
765 // Handle payment methods
766 if (isset($posted_settings['easy_invoice_payment_methods']) && is_array($posted_settings['easy_invoice_payment_methods'])) {
767 $sanitized_methods = array_map('sanitize_key', $posted_settings['easy_invoice_payment_methods']);
768 update_option('easy_invoice_payment_methods', $sanitized_methods);
769 $response['saved_options']['easy_invoice_payment_methods'] = $sanitized_methods;
770 } else {
771 update_option('easy_invoice_payment_methods', []);
772 $response['saved_options']['easy_invoice_payment_methods'] = [];
773 }
774
775 // Handle gateway order
776 if (isset($posted_settings['easy_invoice_gateway_order']) && is_array($posted_settings['easy_invoice_gateway_order'])) {
777 $gateway_order = array_map('sanitize_key', $posted_settings['easy_invoice_gateway_order']);
778 update_option('easy_invoice_payment_gateway_order', $gateway_order);
779 $response['saved_options']['easy_invoice_gateway_order'] = $gateway_order;
780 } else {
781 update_option('easy_invoice_payment_gateway_order', []);
782 $response['saved_options']['easy_invoice_gateway_order'] = [];
783 }
784
785 // Handle gateway display names
786 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
787 $all_gateways = $gateway_manager->getGateways();
788
789 foreach ($all_gateways as $gateway_id => $gateway) {
790 $display_name_key = 'easy_invoice_gateway_display_name_' . $gateway_id;
791 if (isset($posted_settings[$display_name_key])) {
792 $display_name = wp_strip_all_tags(wp_unslash($posted_settings[$display_name_key]));
793 update_option($display_name_key, $display_name);
794 $response['saved_options'][$display_name_key] = $display_name;
795 }
796 }
797
798 // Handle gateway-specific settings
799 try {
800 $gateway_configs = $this->getGatewaySettingsConfigs();
801 foreach ($gateway_configs as $gateway_id => $gateway_config) {
802 if (isset($gateway_config['fields']) && is_array($gateway_config['fields'])) {
803 foreach ($gateway_config['fields'] as $option_key => $field_config) {
804 if (isset($posted_settings[$option_key])) {
805 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
806 } elseif ($field_config['type'] === 'checkbox') {
807 // Checkboxes not in POST are considered unchecked
808 update_option($option_key, 'no');
809 $response['saved_options'][$option_key] = 'no';
810 }
811 }
812 }
813 }
814 } catch (\Exception $e) {
815 throw $e; // Re-throw to be caught by the main try-catch
816 }
817 }
818
819 /**
820 * Save settings
821 *
822 * @return void
823 */
824 public function saveSettings() {
825 // Verify nonce. $_POST['easy_invoice_settings_nonce'] was read unguarded, so a
826 // request without it raised "Undefined array key" before the check could run.
827 $nonce = isset($_POST['easy_invoice_settings_nonce'])
828 ? sanitize_text_field(wp_unslash($_POST['easy_invoice_settings_nonce']))
829 : '';
830 if (!wp_verify_nonce($nonce, 'easy_invoice_settings')) {
831 wp_die(esc_html__('Security check failed', 'easy-invoice'));
832 }
833
834
835 // Check permissions
836 if (!current_user_can('manage_options')) {
837 wp_die(esc_html__('You do not have permission to perform this action', 'easy-invoice'));
838 }
839
840 $posted_settings = $_POST['settings'] ?? [];
841
842 $settings_config = $this->get_settings_fields_config();
843 $response = [
844 'success' => true,
845 'message' => __('Settings saved successfully', 'easy-invoice'),
846 'saved_options' => []
847 ];
848
849 try {
850 // Process each section
851 foreach ($settings_config as $section_id => $section_data) {
852 if (isset($section_data['fields']) && is_array($section_data['fields'])) {
853 // Handle regular fields
854 foreach ($section_data['fields'] as $option_key => $field_config) {
855 if (isset($posted_settings[$option_key])) {
856 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
857 } elseif ($field_config['type'] === 'checkbox') {
858 // Checkboxes not in POST are considered unchecked
859 update_option($option_key, 'no');
860 $response['saved_options'][$option_key] = 'no';
861 }
862 }
863 } else if (isset($section_data['subsections']) && is_array($section_data['subsections'])) {
864 // Handle sections with subsections (like invoice, quote, email, etc.)
865 foreach ($section_data['subsections'] as $subsection_id => $subsection_data) {
866 if (isset($subsection_data['fields']) && is_array($subsection_data['fields'])) {
867 foreach ($subsection_data['fields'] as $option_key => $field_config) {
868 if (isset($posted_settings[$option_key])) {
869 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
870 } elseif ($field_config['type'] === 'checkbox') {
871 // Checkboxes not in POST are considered unchecked
872 update_option($option_key, 'no');
873 $response['saved_options'][$option_key] = 'no';
874 }
875 }
876 }
877 }
878 }
879 }
880
881 // Fields other code adds to the Tax section for display only
882 // (`easy_invoice_tax_section_additional_fields`) are saved with the
883 // same rules — the reverse-charge switch lived only there and could
884 // never be turned on from the screen.
885 $extra_tax_fields = apply_filters('easy_invoice_tax_section_additional_fields', [], []);
886 $known = [];
887 foreach ($settings_config as $section_data) {
888 foreach ((array) ($section_data['fields'] ?? []) as $k => $unused) { $known[$k] = true; }
889 foreach ((array) ($section_data['subsections'] ?? []) as $sub) { foreach ((array) ($sub['fields'] ?? []) as $k => $unused) { $known[$k] = true; } }
890 }
891 foreach ((array) $extra_tax_fields as $option_key => $field_config) {
892 if (!is_string($option_key) || isset($known[$option_key]) || !is_array($field_config)) {
893 continue;
894 }
895 if (isset($posted_settings[$option_key])) {
896 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
897 } elseif (($field_config['type'] ?? '') === 'checkbox') {
898 update_option($option_key, 'no');
899 $response['saved_options'][$option_key] = 'no';
900 }
901 }
902
903 // Handle special sections like payment methods
904 if (isset($settings_config['payment']) && isset($settings_config['payment']['is_special_section'])) {
905 $this->savePaymentSettings($posted_settings, $response);
906 }
907
908 // Clear any caches
909 wp_cache_flush();
910
911 // Clear settings cache
912 $this->settings_cache = null;
913 self::$shared_settings_cache = null;
914
915 // Log the save action
916 $this->log('Settings saved successfully by user: ' . get_current_user_id());
917
918 } catch (\Exception $e) {
919 $response['success'] = false;
920 $response['message'] = __('Error saving settings: ', 'easy-invoice') . $e->getMessage();
921 $this->log('Error saving settings: ' . $e->getMessage());
922
923 }
924
925 // Clear settings cache to ensure new fields are recognized
926 $this->clearCache();
927
928 // Send JSON response
929 wp_send_json($response);
930 }
931
932 /**
933 * Sanitize and save a single setting
934 *
935 * @param string $option_key The option key
936 * @param array $field_config The field configuration
937 * @param mixed $value The value to sanitize and save
938 * @param array &$response The response array to update
939 * @return void
940 */
941 private function sanitizeAndSaveSetting($option_key, $field_config, $value, &$response) {
942 if (in_array($option_key, [
943 'easy_invoice_quote_accept_text',
944 'easy_invoice_quote_accepted_message',
945 'easy_invoice_quote_declined_message'
946 ])) {
947 }
948
949 // Unslash the value to prevent double-escaping
950 $value = wp_unslash($value);
951
952 // Allow pre-sanitization filters
953 $value = apply_filters('easy_invoice_pre_sanitize_option', $value, $option_key, $field_config);
954
955 // Sanitize based on field type
956 switch ($field_config['type']) {
957 case 'email':
958 $value = sanitize_email($value);
959 break;
960 case 'url':
961 $value = esc_url_raw($value);
962 break;
963 case 'textarea':
964 // Use wp_kses_post for textarea to allow safe HTML while preventing double-escaping
965 $value = wp_kses_post($value);
966 break;
967 case 'wp_editor':
968 // For wp_editor content, use WordPress's built-in sanitization
969 if (is_string($value)) {
970 // Use wp_kses_post which expects unslashed data
971 $value = wp_kses_post($value);
972 }
973 break;
974 case 'number':
975 if (isset($field_config['step']) && strpos((string)$field_config['step'], '.') !== false) {
976 $value = floatval(wp_strip_all_tags(easy_invoice_str_replace(',', '.', $value)));
977 } elseif (isset($field_config['min']) && intval($field_config['min']) < 0) {
978 $value = intval($value);
979 } else {
980 $value = absint($value);
981 }
982 break;
983 case 'checkbox':
984 // For checkboxes, we expect 'yes' or 'no' values
985 $value = ($value === 'yes' || $value === '1' || $value === true) ? 'yes' : 'no';
986 break;
987 case 'select':
988 $value = sanitize_key($value);
989 // Ensure currency codes are always uppercase
990 if ($option_key === 'easy_invoice_currency_code') {
991 $value = strtoupper($value);
992 }
993 break;
994 case 'multiselect':
995 // For multiselect, ensure it's an array and sanitize each value
996 if (!is_array($value)) {
997 $value = [];
998 } else {
999 $value = array_map('sanitize_key', $value);
1000 }
1001 break;
1002 default:
1003 // For regular text fields, use wp_strip_all_tags to prevent double-escaping
1004 $value = wp_strip_all_tags($value);
1005 break;
1006 }
1007
1008 // Allow post-sanitization value modification
1009 $value = apply_filters("easy_invoice_sanitize_option_{$option_key}", $value, $field_config);
1010 $value = apply_filters('easy_invoice_sanitize_option', $value, $option_key, $field_config);
1011
1012 // Save the setting normally
1013 update_option($option_key, $value);
1014 $response['saved_options'][$option_key] = $value;
1015 }
1016
1017 /**
1018 * Get all settings for the plugin
1019 *
1020 * @param bool $use_cache Whether to use cached settings
1021 * @return array The settings array
1022 */
1023 public function getSettings($use_cache = true): array {
1024 // Return cached settings if available
1025 if ($use_cache && is_array($this->settings_cache)) {
1026 return $this->settings_cache;
1027 }
1028 if ($use_cache && is_array(self::$shared_settings_cache)) {
1029 return $this->settings_cache = self::$shared_settings_cache;
1030 }
1031
1032 $settings = [];
1033 $config = $this->get_settings_fields_config();
1034
1035 foreach ($config as $section_id => $section_data) {
1036 if ($section_id === 'payment' && isset($section_data['gateways'])) {
1037 // Handle gateway fields specifically
1038 foreach ($section_data['gateways'] as $gateway_id => $gateway_config) {
1039 // The per-gateway display name is saved by savePaymentSettings()
1040 // but was never loaded back, so the field always showed the default.
1041 $display_key = 'easy_invoice_gateway_display_name_' . $gateway_id;
1042 $display_val = get_option($display_key, '');
1043 if ('' !== $display_val) {
1044 $settings[$display_key] = $display_val;
1045 }
1046 if (isset($gateway_config['fields']) && is_array($gateway_config['fields'])) {
1047 foreach ($gateway_config['fields'] as $option_key => $field_data) {
1048 $default_value = $field_data['default'] ?? '';
1049 $settings[$option_key] = get_option($option_key, $default_value);
1050 }
1051 }
1052 }
1053 }
1054 else if (isset($section_data['subsections']) && is_array($section_data['subsections'])) {
1055 // Handle sections with subsections (like email, invoice, quote)
1056 foreach ($section_data['subsections'] as $subsection_id => $subsection_data) {
1057 if (isset($subsection_data['fields']) && is_array($subsection_data['fields'])) {
1058 foreach ($subsection_data['fields'] as $option_key => $field_data) {
1059 $default_value = $field_data['default'] ?? '';
1060
1061 // Handle special defaults
1062 if ($option_key === 'easy_invoice_email_from_name' && $default_value === get_bloginfo('name')) {
1063 $default_value = get_bloginfo('name');
1064 }
1065 if ($option_key === 'easy_invoice_email_from_address' && $default_value === get_bloginfo('admin_email')) {
1066 $default_value = get_bloginfo('admin_email');
1067 }
1068
1069 // Check if field has a value_callback function
1070 if (isset($field_data['value_callback']) && is_callable($field_data['value_callback'])) {
1071 $settings[$option_key] = $field_data['value_callback']();
1072 } else {
1073 // Get option value or default
1074 $settings[$option_key] = get_option($option_key, $default_value);
1075 }
1076 }
1077 }
1078 }
1079 }
1080 else if (isset($section_data['fields']) && is_array($section_data['fields'])) {
1081 foreach ($section_data['fields'] as $option_key => $field_data) {
1082 $default_value = $field_data['default'] ?? '';
1083
1084 // Handle special defaults
1085 if ($option_key === 'easy_invoice_email_from_name' && $default_value === get_bloginfo('name')) {
1086 $default_value = get_bloginfo('name');
1087 }
1088 if ($option_key === 'easy_invoice_email_from_address' && $default_value === get_bloginfo('admin_email')) {
1089 $default_value = get_bloginfo('admin_email');
1090 }
1091
1092 // Check if field has a value_callback function
1093 if (isset($field_data['value_callback']) && is_callable($field_data['value_callback'])) {
1094 $settings[$option_key] = $field_data['value_callback']();
1095 } else {
1096 // Get option value or default
1097 $settings[$option_key] = get_option($option_key, $default_value);
1098 }
1099 }
1100 }
1101 }
1102
1103 // Add special array settings
1104 $settings['easy_invoice_payment_methods'] = get_option('easy_invoice_payment_methods', []);
1105 $settings['easy_invoice_payment_gateway_order'] = get_option('easy_invoice_payment_gateway_order', []);
1106
1107 // Cache settings
1108 $this->settings_cache = $settings;
1109 self::$shared_settings_cache = $settings;
1110
1111 // Allow third party to add/modify settings
1112 return apply_filters('easy_invoice_settings', $settings);
1113 }
1114
1115 /**
1116 * Log debug information
1117 *
1118 * @param string $message The message to log
1119 * @return void
1120 */
1121 protected function log(string $message): void {
1122
1123 }
1124
1125 /**
1126 * Initialize default settings
1127 */
1128 public function initializeSettings() {
1129 // One-time migration (2.1.22): strip the leftover
1130 // "<strong>Shortcode:</strong> <code>[easy_(invoice|quote)_url …]</code>"
1131 // reference line from saved email bodies. Originally intended as admin-side
1132 // documentation, but it lived inside the email template and got emailed to
1133 // clients verbatim. Guarded by a flag option so each install runs at most once.
1134 //
1135 // The regex tolerates the optional surrounding <p>...</p> because WP's
1136 // wp_kses_post may normalise the wrapping when the option is saved.
1137 if (!get_option('easy_invoice_email_shortcode_hint_removed_v2')) {
1138 foreach (['easy_invoice_invoice_email_body', 'easy_invoice_quote_email_body'] as $opt_key) {
1139 $body = get_option($opt_key, '');
1140 if (!is_string($body) || $body === '') {
1141 continue;
1142 }
1143 $cleaned = preg_replace(
1144 '#\s*(?:<p>\s*)?<strong>\s*Shortcode\s*:\s*</strong>\s*<code>\s*\[easy_(?:invoice|quote)_url[^\]]*\]\s*</code>(?:\s*</p>)?\s*#i',
1145 '',
1146 $body
1147 );
1148 if ($cleaned !== null && $cleaned !== $body) {
1149 update_option($opt_key, $cleaned);
1150 }
1151 }
1152 update_option('easy_invoice_email_shortcode_hint_removed_v2', 1);
1153 }
1154
1155 // Initialize invoice number settings if not already set
1156 if (!get_option('easy_invoice_invoice_prefix')) {
1157 update_option('easy_invoice_invoice_prefix', 'INV-');
1158 }
1159
1160 if (!get_option('easy_invoice_next_invoice_number')) {
1161 update_option('easy_invoice_next_invoice_number', 1);
1162 }
1163
1164 // Initialize other default settings
1165 if (!get_option('easy_invoice_invoice_due_days')) {
1166 update_option('easy_invoice_invoice_due_days', 30);
1167 }
1168
1169 if (!get_option('easy_invoice_currency_code')) {
1170 update_option('easy_invoice_currency_code', 'USD');
1171 }
1172
1173 if (!get_option('easy_invoice_currency_symbol')) {
1174 update_option('easy_invoice_currency_symbol', '$');
1175 }
1176
1177 if (!get_option('easy_invoice_currency_position')) {
1178 update_option('easy_invoice_currency_position', 'left');
1179 }
1180
1181 if (!get_option('easy_invoice_currency_symbol_type')) {
1182 update_option('easy_invoice_currency_symbol_type', 'symbol');
1183 }
1184
1185 if (!get_option('easy_invoice_decimal_separator')) {
1186 update_option('easy_invoice_decimal_separator', '.');
1187 }
1188
1189 if (!get_option('easy_invoice_thousands_separator')) {
1190 update_option('easy_invoice_thousands_separator', ',');
1191 }
1192
1193 if (!get_option('easy_invoice_decimal_precision')) {
1194 update_option('easy_invoice_decimal_precision', 2);
1195 }
1196
1197 // Initialize date format setting
1198 if (!get_option('easy_invoice_date_format')) {
1199 update_option('easy_invoice_date_format', 'us');
1200 }
1201
1202 // Fix legacy date format values
1203 $current_date_format = get_option('easy_invoice_date_format');
1204 if ($current_date_format === 'mdy' || $current_date_format === 'm/d/Y') {
1205 update_option('easy_invoice_date_format', 'us');
1206 } elseif ($current_date_format === 'd/m/Y') {
1207 update_option('easy_invoice_date_format', 'uk');
1208 } elseif ($current_date_format === 'Y-m-d') {
1209 update_option('easy_invoice_date_format', 'iso');
1210 }
1211
1212 // Initialize email settings
1213 if (!get_option('easy_invoice_email_from_name')) {
1214 update_option('easy_invoice_email_from_name', get_bloginfo('name'));
1215 }
1216
1217 if (!get_option('easy_invoice_email_from_address')) {
1218 update_option('easy_invoice_email_from_address', get_bloginfo('admin_email'));
1219 }
1220
1221 if (!get_option('easy_invoice_enable_email_styling')) {
1222 update_option('easy_invoice_enable_email_styling', 'yes');
1223 }
1224
1225 if (!get_option('easy_invoice_bcc_admin')) {
1226 update_option('easy_invoice_bcc_admin', 'no');
1227 }
1228
1229 if (!get_option('easy_invoice_admin_email')) {
1230 update_option('easy_invoice_admin_email', get_option('admin_email'));
1231 }
1232
1233 // Initialize Text Settings
1234 $text_settings = [
1235 'easy_invoice_text_invoice' => __('Invoice', 'easy-invoice'),
1236 'easy_invoice_text_invoices' => __('Invoices', 'easy-invoice'),
1237 'easy_invoice_text_from' => __('From', 'easy-invoice'),
1238 'easy_invoice_text_to' => __('To', 'easy-invoice'),
1239 'easy_invoice_text_invoice_number' => __('Invoice Number', 'easy-invoice'),
1240 'easy_invoice_text_order_number' => __('Order Number', 'easy-invoice'),
1241 'easy_invoice_text_invoice_date' => __('Invoice Date', 'easy-invoice'),
1242 'easy_invoice_text_due_date' => __('Due Date', 'easy-invoice'),
1243 'easy_invoice_text_total_due' => __('Total Due', 'easy-invoice'),
1244 'easy_invoice_text_qty' => __('Qty', 'easy-invoice'),
1245 'easy_invoice_text_service' => __('Service', 'easy-invoice'),
1246 'easy_invoice_text_rate_price' => __('Rate', 'easy-invoice'),
1247 'easy_invoice_text_adjust' => __('Adjust', 'easy-invoice'),
1248 'easy_invoice_text_sub_total' => __('Sub Total', 'easy-invoice'),
1249 'easy_invoice_text_total' => __('Total', 'easy-invoice'),
1250 'easy_invoice_text_line_total' => __('Total', 'easy-invoice'),
1251 'easy_invoice_text_tax' => __('Tax', 'easy-invoice'),
1252 'easy_invoice_text_discount' => __('Discount', 'easy-invoice'),
1253 'easy_invoice_text_page' => __('Page', 'easy-invoice'),
1254 'easy_invoice_text_print' => __('Print', 'easy-invoice'),
1255 'easy_invoice_text_download_pdf' => __('Download as PDF', 'easy-invoice'),
1256 'easy_invoice_text_send_email' => __('Send Email', 'easy-invoice'),
1257 'easy_invoice_text_pay_now' => __('Pay Now', 'easy-invoice'),
1258 'easy_invoice_text_proceed_payment' => __('Proceed to payment', 'easy-invoice'),
1259 'easy_invoice_text_payment_gateway' => __('Invoice Payment Gateway', 'easy-invoice'),
1260 'easy_invoice_text_quote' => __('Quote', 'easy-invoice'),
1261 'easy_invoice_text_quotes' => __('Quotes', 'easy-invoice'),
1262 'easy_invoice_text_quote_number' => __('Quote Number', 'easy-invoice'),
1263 'easy_invoice_text_accept_quote' => __('Accept Quote', 'easy-invoice'),
1264 'easy_invoice_text_decline_quote' => __('Decline Quote', 'easy-invoice'),
1265 'easy_invoice_text_decline_reason' => __('Reason for declining', 'easy-invoice'),
1266 'easy_invoice_text_quote_amount' => __('Quote Amount', 'easy-invoice'),
1267 'easy_invoice_text_valid_until' => __('Valid Until Date', 'easy-invoice'),
1268 'easy_invoice_text_quote_date' => __('Quote Date', 'easy-invoice'),
1269 'easy_invoice_text_available' => __('Available', 'easy-invoice'),
1270 'easy_invoice_text_draft' => __('Draft', 'easy-invoice'),
1271 'easy_invoice_text_overdue' => __('Overdue', 'easy-invoice'),
1272 'easy_invoice_text_paid' => __('Paid', 'easy-invoice'),
1273 'easy_invoice_text_unpaid' => __('Unpaid', 'easy-invoice'),
1274 'easy_invoice_text_cancelled' => __('Cancelled', 'easy-invoice'),
1275 ];
1276
1277 foreach ($text_settings as $option_key => $default_value) {
1278 if (!get_option($option_key)) {
1279 update_option($option_key, $default_value);
1280 }
1281 }
1282
1283 if (!get_option('easy_invoice_invoice_email_enabled')) {
1284 update_option('easy_invoice_invoice_email_enabled', 'yes');
1285 }
1286
1287 if (!get_option('easy_invoice_invoice_email_subject')) {
1288 update_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'));
1289 }
1290
1291 if (!get_option('easy_invoice_invoice_email_body')) {
1292 update_option('easy_invoice_invoice_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('invoice'));
1293 }
1294
1295 if (!get_option('easy_invoice_quote_email_enabled')) {
1296 update_option('easy_invoice_quote_email_enabled', 'yes');
1297 }
1298
1299 if (!get_option('easy_invoice_quote_email_subject')) {
1300 update_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'));
1301 }
1302
1303 if (!get_option('easy_invoice_quote_email_body')) {
1304 update_option('easy_invoice_quote_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('quote'));
1305 }
1306
1307 if (!get_option('easy_invoice_payment_email_enabled')) {
1308 update_option('easy_invoice_payment_email_enabled', 'yes');
1309 }
1310
1311 if (!get_option('easy_invoice_payment_email_subject')) {
1312 update_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'));
1313 }
1314
1315 if (!get_option('easy_invoice_payment_email_body')) {
1316 update_option('easy_invoice_payment_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('payment'));
1317 }
1318 }
1319
1320 /**
1321 * Test email functionality
1322 */
1323 public function testEmail(): void {
1324 // Verify nonce
1325 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_settings')) {
1326 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1327 }
1328
1329 // Check permissions
1330 if (!current_user_can('manage_options')) {
1331 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
1332 }
1333
1334 // Get test email address
1335 $test_email = sanitize_email($_POST['test_email'] ?? '');
1336 if (empty($test_email)) {
1337 wp_send_json_error(['message' => __('Please provide a valid email address', 'easy-invoice')]);
1338 }
1339
1340 // Test email sending
1341 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1342 $result = $email_manager->testEmail($test_email);
1343
1344 if ($result['success']) {
1345 wp_send_json_success($result);
1346 } else {
1347 wp_send_json_error($result);
1348 }
1349 }
1350
1351 /**
1352 * Test payment reminder email functionality
1353 */
1354 public function testPaymentReminderEmail(): void {
1355 // Verify nonce
1356 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_settings')) {
1357 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1358 }
1359
1360 // Check permissions
1361 if (!current_user_can('manage_options')) {
1362 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
1363 }
1364
1365 // Get test email address
1366 $test_email = sanitize_email($_POST['test_email'] ?? '');
1367 if (empty($test_email)) {
1368 wp_send_json_error(['message' => __('Please provide a valid email address', 'easy-invoice')]);
1369 }
1370
1371 // Get payment reminder settings
1372 $subject = get_option('easy_invoice_pro_payment_reminder_subject', __('A friendly reminder - Invoice #{{invoice_number}}', 'easy-invoice'));
1373 $body = get_option('easy_invoice_pro_payment_reminder_body', '');
1374
1375 if (empty($body)) {
1376 wp_send_json_error(['message' => __('Payment reminder email template is empty. Please configure the email message first.', 'easy-invoice')]);
1377 }
1378
1379 // Create sample data for testing
1380 $sample_data = [
1381 'invoice_number' => 'TEST-001',
1382 'client_name' => 'Test Client',
1383 'company_name' => get_option('easy_invoice_company_name', get_bloginfo('name') ?: 'Easy Invoice'),
1384 'company_email' => get_option('easy_invoice_company_email', get_option('admin_email', '')),
1385 'total_amount' => '$1,000.00',
1386 'amount_due' => '$1,000.00',
1387 'due_date' => wp_date('Y-m-d', strtotime('+7 days')),
1388 ];
1389
1390 // Replace placeholders
1391 foreach ($sample_data as $placeholder => $value) {
1392 $subject = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $subject);
1393 $body = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $body);
1394 }
1395
1396 // Test email sending
1397 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1398 $result = $email_manager->sendTestTemplateEmail($test_email, $subject, $body);
1399
1400 if ($result['success']) {
1401 wp_send_json_success($result);
1402 } else {
1403 wp_send_json_error($result);
1404 }
1405 }
1406
1407 /**
1408 * Test template email functionality
1409 */
1410 public function testTemplateEmail(): void {
1411 // Verify nonce
1412 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_settings')) {
1413 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1414 }
1415
1416 // Check permissions
1417 if (!current_user_can('manage_options')) {
1418 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
1419 }
1420
1421 // Get test email address and template type
1422 $test_email = sanitize_email($_POST['test_email'] ?? '');
1423 $template_type = sanitize_text_field($_POST['template_type'] ?? '');
1424
1425 if (empty($test_email)) {
1426 wp_send_json_error(['message' => __('Please provide a valid email address', 'easy-invoice')]);
1427 }
1428
1429 if (empty($template_type)) {
1430 wp_send_json_error(['message' => __('Template type is required', 'easy-invoice')]);
1431 }
1432
1433 // Get the actual configured email settings
1434 $subject = '';
1435 $body = '';
1436
1437 switch ($template_type) {
1438 case 'invoice_available':
1439 $subject = get_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'));
1440 $body = get_option('easy_invoice_invoice_email_body', self::getInvoiceEmailBody());
1441 break;
1442 case 'quote_available':
1443 $subject = get_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'));
1444 $body = get_option('easy_invoice_quote_email_body', self::getQuoteEmailBody());
1445 break;
1446 case 'payment_received':
1447 $subject = get_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'));
1448 $body = get_option('easy_invoice_payment_email_body', self::getPaymentEmailBody());
1449 break;
1450 default:
1451 wp_send_json_error(['message' => __('Invalid template type', 'easy-invoice')]);
1452 }
1453
1454 // Ensure we have valid strings
1455 $subject = is_string($subject) ? $subject : '';
1456 $body = is_string($body) ? $body : '';
1457
1458 if (empty($subject) || empty($body)) {
1459 wp_send_json_error(['message' => __('Email template is empty or invalid', 'easy-invoice')]);
1460 }
1461
1462 // Create sample data for testing
1463 $sample_data = [
1464 'invoice_number' => 'TEST-001',
1465 'quote_number' => 'TEST-Q-001',
1466 'client_name' => 'Test Client',
1467 'company_name' => get_option('easy_invoice_company_name', get_bloginfo('name') ?: 'Easy Invoice'),
1468 'company_email' => get_option('easy_invoice_company_email', get_option('admin_email', '')),
1469 'total_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1470 'payment_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1471 'due_date' => wp_date('Y-m-d', strtotime('+30 days')),
1472 'expiry_date' => wp_date('Y-m-d', strtotime('+30 days')),
1473 'payment_date' => current_time('Y-m-d'),
1474 'payment_method' => 'Credit Card',
1475 'transaction_id' => 'TXN-' . uniqid(),
1476 'payment_terms' => get_option('easy_invoice_payment_terms', __('Due on receipt', 'easy-invoice')),
1477 // Everything the stock templates can mention, so a test never shows a raw tag.
1478 'amount_due' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1479 'subtotal' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1480 'tax_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(0) : '$0.00',
1481 'discount_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(0) : '$0.00',
1482 'issue_date' => current_time('Y-m-d'),
1483 'invoice_title' => __('Sample invoice', 'easy-invoice'),
1484 'quote_title' => __('Sample quote', 'easy-invoice'),
1485 'invoice_url' => home_url('/'),
1486 'quote_url' => home_url('/'),
1487 'payment_url' => home_url('/'),
1488 'site_url' => home_url('/'),
1489 'admin_url' => admin_url(),
1490 'client_email' => 'client@example.com',
1491 'client_first_name' => 'Test',
1492 'client_last_name' => 'Client',
1493 'client_address' => '1 Example Street',
1494 'company_phone' => get_option('easy_invoice_company_phone', ''),
1495 'company_address' => get_option('easy_invoice_company_address', ''),
1496 'company_website' => get_option('easy_invoice_company_website', ''),
1497 ];
1498
1499 // Ensure all sample data values are strings
1500 foreach ($sample_data as $key => $value) {
1501 $sample_data[$key] = is_string($value) ? $value : (string) $value;
1502 }
1503
1504 // Replace placeholders in subject and body
1505 foreach ($sample_data as $placeholder => $value) {
1506 $subject = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $subject);
1507 $body = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $body);
1508 }
1509
1510 // Test email sending
1511 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1512 $result = $email_manager->sendTestTemplateEmail($test_email, $subject, $body);
1513
1514 if ($result['success']) {
1515 wp_send_json_success($result);
1516 } else {
1517 wp_send_json_error($result);
1518 }
1519 }
1520
1521 /**
1522 * Check if quote adjust field should be shown
1523 *
1524 * @return bool True if adjust field should be shown
1525 */
1526 public static function shouldShowQuoteAdjustField(): bool {
1527 return get_option('easy_invoice_quote_show_adjust_field', 'yes') === 'yes';
1528 }
1529
1530 /**
1531 * Check if invoice adjust field should be shown
1532 *
1533 * @return bool True if adjust field should be shown
1534 */
1535 public static function shouldShowInvoiceAdjustField(): bool {
1536 return get_option('easy_invoice_invoice_show_adjust_field', 'yes') === 'yes';
1537 }
1538
1539 /**
1540 * Get invoice prefix
1541 *
1542 * @return string Invoice prefix
1543 */
1544 public static function getInvoicePrefix(): string {
1545 return get_option('easy_invoice_invoice_prefix', 'EIN_');
1546 }
1547
1548 /**
1549 * Get invoice starting number
1550 *
1551 * @return int Invoice starting number
1552 */
1553 public static function getInvoiceStartingNumber(): int {
1554 return (int) get_option('easy_invoice_invoice_starting_number', 0);
1555 }
1556
1557 /**
1558 * Get invoice terms and conditions
1559 *
1560 * @return string Invoice terms and conditions
1561 */
1562 public static function getInvoiceTermsConditions(): string {
1563 return get_option('easy_invoice_invoice_terms_conditions', __('Payment is due within 30 days from date of invoice', 'easy-invoice'));
1564 }
1565
1566 /**
1567 * Get invoice footer text
1568 *
1569 * @return string Invoice footer text
1570 */
1571 public static function getInvoiceFooterText(): string {
1572 return get_option('easy_invoice_invoice_footer_text', '');
1573 }
1574
1575 /**
1576 * Get quote prefix
1577 *
1578 * @return string Quote prefix
1579 */
1580 public static function getQuotePrefix(): string {
1581 return get_option('easy_invoice_quote_prefix', 'EIQN_');
1582 }
1583
1584 /**
1585 * Get quote starting number
1586 *
1587 * @return int Quote starting number
1588 */
1589 public static function getQuoteStartingNumber(): int {
1590 return (int) get_option('easy_invoice_quote_starting_number', 1);
1591 }
1592
1593 /**
1594 * Get quote terms and conditions
1595 *
1596 * @return string Quote terms and conditions
1597 */
1598 public static function getQuoteTermsConditions(): string {
1599 return get_option('easy_invoice_quote_terms_conditions', __('This quote has a fixed price. Upon acceptance, we kindly ask for a 25% deposit prior to initiating the work.', 'easy-invoice'));
1600 }
1601
1602 /**
1603 * Get quote footer text
1604 *
1605 * @return string Quote footer text
1606 */
1607 public static function getQuoteFooterText(): string {
1608 return get_option('easy_invoice_quote_footer_text', __('Thank you for your business.', 'easy-invoice'));
1609 }
1610
1611 /**
1612 * Check if quote accept button should be shown
1613 *
1614 * @return bool True if accept button should be shown
1615 */
1616 public static function shouldShowQuoteAcceptButton(): bool {
1617 return get_option('easy_invoice_quote_accept_button', 'yes') === 'yes';
1618 }
1619
1620 /**
1621 * Get quote accept action
1622 *
1623 * @return string Quote accept action
1624 */
1625 public static function getQuoteAcceptAction(): string {
1626 return get_option('easy_invoice_quote_accept_action', 'convert');
1627 }
1628
1629 /**
1630 * Get quote accept text
1631 *
1632 * @return string Quote accept text
1633 */
1634 public static function getQuoteAcceptText(): string {
1635 return get_option('easy_invoice_quote_accept_text', __('Important: When you accept this Quote, an Invoice will be created automatically. This will form a legally binding contract.', 'easy-invoice'));
1636 }
1637
1638 /**
1639 * Get accepted quote message
1640 *
1641 * @return string Accepted quote message
1642 */
1643 public static function getAcceptedQuoteMessage(): string {
1644 return get_option('easy_invoice_quote_accepted_message', __('You\'ve confirmed the Quote.<br>We\'ll get in touch with you shortly.', 'easy-invoice'));
1645 }
1646
1647 /**
1648 * Check if decline reason is required
1649 *
1650 * @return bool True if decline reason is required
1651 */
1652 public static function isDeclineReasonRequired(): bool {
1653 return get_option('easy_invoice_quote_decline_reason_required', 'no') === 'yes';
1654 }
1655
1656 /**
1657 * Get declined quote message
1658 *
1659 * @return string Declined quote message
1660 */
1661 public static function getDeclinedQuoteMessage(): string {
1662 return get_option('easy_invoice_quote_declined_message', '');
1663 }
1664
1665 // Email Settings Helper Functions
1666
1667 /**
1668 * Check if invoice available email is enabled
1669 *
1670 * @return bool True if invoice email is enabled
1671 */
1672 public static function isInvoiceEmailEnabled(): bool {
1673 return get_option('easy_invoice_invoice_email_enabled', 'yes') === 'yes';
1674 }
1675
1676 /**
1677 * Get invoice email subject
1678 *
1679 * @return string Invoice email subject
1680 */
1681 public static function getInvoiceEmailSubject(): string {
1682 return get_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'));
1683 }
1684
1685 /**
1686 * Get invoice email body
1687 *
1688 * @return string Invoice email body
1689 */
1690 public static function getInvoiceEmailBody(): string {
1691 return get_option('easy_invoice_invoice_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('invoice'));
1692 }
1693
1694 /**
1695 * Check if quote available email is enabled
1696 *
1697 * @return bool True if quote email is enabled
1698 */
1699 public static function isQuoteEmailEnabled(): bool {
1700 return get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes';
1701 }
1702
1703 /**
1704 * Get quote email subject
1705 *
1706 * @return string Quote email subject
1707 */
1708 public static function getQuoteEmailSubject(): string {
1709 return get_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'));
1710 }
1711
1712 /**
1713 * Get quote email body
1714 *
1715 * @return string Quote email body
1716 */
1717 public static function getQuoteEmailBody(): string {
1718 return get_option('easy_invoice_quote_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('quote'));
1719 }
1720
1721 /**
1722 * Check if payment received email is enabled
1723 *
1724 * @return bool True if payment email is enabled
1725 */
1726 public static function isPaymentEmailEnabled(): bool {
1727 return get_option('easy_invoice_payment_email_enabled', 'yes') === 'yes';
1728 }
1729
1730 /**
1731 * Get payment email subject
1732 *
1733 * @return string Payment email subject
1734 */
1735 public static function getPaymentEmailSubject(): string {
1736 return get_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'));
1737 }
1738
1739 /**
1740 * Get payment email body
1741 *
1742 * @return string Payment email body
1743 */
1744 public static function getPaymentEmailBody(): string {
1745 return get_option('easy_invoice_payment_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('payment'));
1746 }
1747
1748 // ========================================
1749 // TEXT SETTINGS HELPER FUNCTIONS
1750 // ========================================
1751 //
1752 // USAGE IN TEMPLATES:
1753 // Instead of: echo __('Invoice', 'easy-invoice');
1754 // Use: echo \EasyInvoice\Controllers\SettingsController::getTextInvoice();
1755 //
1756 // Example:
1757 // <h1>echo \EasyInvoice\Controllers\SettingsController::getTextInvoice();</h1>
1758 // <p>echo \EasyInvoice\Controllers\SettingsController::getTextFrom(); : echo $company_name;</p>
1759 // <p>echo \EasyInvoice\Controllers\SettingsController::getTextTo(); : echo $client_name;</p>
1760 //
1761
1762 /**
1763 * Get custom text setting with fallback to default
1764 *
1765 * @param string $key Text setting key
1766 * @param string $default Default text
1767 * @return string Custom text or default
1768 */
1769 /**
1770 * Every document label the site can override, with its saved value.
1771 *
1772 * The getText*() methods below are what templates call; this list exists
1773 * for anything that needs the whole set at once -- the Pro Translations
1774 * page offers each saved label for translation.
1775 *
1776 * @return array<string,array{label:string,default:string,value:string}> keyed by setting key.
1777 */
1778 public static function getTextLabels(): array {
1779 $labels = [
1780 'invoice' => __('Invoice', 'easy-invoice'),
1781 'invoices' => __('Invoices', 'easy-invoice'),
1782 'to' => __('To', 'easy-invoice'),
1783 'from' => __('From', 'easy-invoice'),
1784 'invoice_number' => __('Invoice Number', 'easy-invoice'),
1785 'invoice_date' => __('Invoice Date', 'easy-invoice'),
1786 'due_date' => __('Due Date', 'easy-invoice'),
1787 'total_due' => __('Total Due', 'easy-invoice'),
1788 'qty' => __('Qty', 'easy-invoice'),
1789 'service' => __('Service', 'easy-invoice'),
1790 'rate_price' => __('Rate', 'easy-invoice'),
1791 'adjust' => __('Adjust', 'easy-invoice'),
1792 'sub_total' => __('Sub Total', 'easy-invoice'),
1793 'total' => __('Total', 'easy-invoice'),
1794 'tax' => __('Tax', 'easy-invoice'),
1795 'discount' => __('Discount', 'easy-invoice'),
1796 'print' => __('Print', 'easy-invoice'),
1797 'download_pdf' => __('Download as PDF', 'easy-invoice'),
1798 'send_email' => __('Send Email', 'easy-invoice'),
1799 'pay_now' => __('Pay Now', 'easy-invoice'),
1800 'quote' => __('Quote', 'easy-invoice'),
1801 'quote_number' => __('Quote Number', 'easy-invoice'),
1802 'quote_date' => __('Quote Date', 'easy-invoice'),
1803 'valid_until' => __('Valid Until Date', 'easy-invoice'),
1804 'accept_quote' => __('Accept Quote', 'easy-invoice'),
1805 'decline_quote' => __('Decline Quote', 'easy-invoice'),
1806 'decline_reason' => __('Reason for declining', 'easy-invoice'),
1807 ];
1808 $out = [];
1809 foreach ($labels as $key => $default) {
1810 $out[$key] = [
1811 'label' => ucfirst(str_replace('_', ' ', $key)),
1812 'default' => $default,
1813 'value' => (string) get_option('easy_invoice_text_' . $key, $default),
1814 ];
1815 }
1816 return $out;
1817 }
1818
1819 public static function getTextSetting(string $key, string $default): string {
1820 $option_key = 'easy_invoice_text_' . $key;
1821 $value = (string) get_option($option_key, $default);
1822
1823 /**
1824 * Filter a document label after the saved override is applied.
1825 *
1826 * Saved labels are literal text and skip gettext, so anything that
1827 * renders a document in another language (Pro's Client Language) uses
1828 * this to run them through the active locale.
1829 *
1830 * @param string $value Label to show.
1831 * @param string $key Setting key, e.g. 'total'.
1832 * @param string $default Translated default.
1833 */
1834 return (string) apply_filters('easy_invoice_text_setting', $value, $key, $default);
1835 }
1836
1837 // Invoice Text Settings
1838 public static function getTextInvoice(): string {
1839 return self::getTextSetting('invoice', __('Invoice', 'easy-invoice'));
1840 }
1841
1842 public static function getTextInvoices(): string {
1843 return self::getTextSetting('invoices', __('Invoices', 'easy-invoice'));
1844 }
1845
1846 public static function getTextTo(): string {
1847 return self::getTextSetting('to', __('To', 'easy-invoice'));
1848 }
1849
1850 public static function getTextInvoiceNumber(): string {
1851 return self::getTextSetting('invoice_number', __('Invoice Number', 'easy-invoice'));
1852 }
1853
1854 public static function getTextInvoiceDate(): string {
1855 return self::getTextSetting('invoice_date', __('Invoice Date', 'easy-invoice'));
1856 }
1857
1858 public static function getTextDueDate(): string {
1859 return self::getTextSetting('due_date', __('Due Date', 'easy-invoice'));
1860 }
1861
1862 public static function getTextTotalDue(): string {
1863 return self::getTextSetting('total_due', __('Total Due', 'easy-invoice'));
1864 }
1865
1866 public static function getTextQty(): string {
1867 return self::getTextSetting('qty', __('Qty', 'easy-invoice'));
1868 }
1869
1870 public static function getTextService(): string {
1871 return self::getTextSetting('service', __('Service', 'easy-invoice'));
1872 }
1873
1874 public static function getTextRatePrice(): string {
1875 return self::getTextSetting('rate_price', __('Rate', 'easy-invoice'));
1876 }
1877
1878 public static function getTextAdjust(): string {
1879 return self::getTextSetting('adjust', __('Adjust', 'easy-invoice'));
1880 }
1881
1882 public static function getTextSubTotal(): string {
1883 return self::getTextSetting('sub_total', __('Sub Total', 'easy-invoice'));
1884 }
1885
1886 public static function getTextTotal(): string {
1887 return self::getTextSetting('total', __('Total', 'easy-invoice'));
1888 }
1889
1890 /** Heading of the line-item amount column, separate from the grand total. */
1891 public static function getTextLineTotal(): string {
1892 return self::getTextSetting('line_total', __('Total', 'easy-invoice'));
1893 }
1894
1895 public static function getTextTax(): string {
1896 return self::getTextSetting('tax', __('Tax', 'easy-invoice'));
1897 }
1898
1899 public static function getTextDiscount(): string {
1900 return self::getTextSetting('discount', __('Discount', 'easy-invoice'));
1901 }
1902
1903 public static function getTextPrint(): string {
1904 return self::getTextSetting('print', __('Print', 'easy-invoice'));
1905 }
1906
1907 public static function getTextDownloadPdf(): string {
1908 return self::getTextSetting('download_pdf', __('Download as PDF', 'easy-invoice'));
1909 }
1910
1911 public static function getTextSendEmail(): string {
1912 return self::getTextSetting('send_email', __('Send Email', 'easy-invoice'));
1913 }
1914
1915 public static function getTextPayNow(): string {
1916 return self::getTextSetting('pay_now', __('Pay Now', 'easy-invoice'));
1917 }
1918
1919 // Quote Text Settings
1920 public static function getTextQuote(): string {
1921 return self::getTextSetting('quote', __('Quote', 'easy-invoice'));
1922 }
1923
1924 public static function getTextQuoteNumber(): string {
1925 return self::getTextSetting('quote_number', __('Quote Number', 'easy-invoice'));
1926 }
1927
1928 public static function getTextAcceptQuote(): string {
1929 return self::getTextSetting('accept_quote', __('Accept Quote', 'easy-invoice'));
1930 }
1931
1932 public static function getTextDeclineQuote(): string {
1933 return self::getTextSetting('decline_quote', __('Decline Quote', 'easy-invoice'));
1934 }
1935
1936 public static function getTextDeclineReason(): string {
1937 return self::getTextSetting('decline_reason', __('Reason for declining', 'easy-invoice'));
1938 }
1939
1940 public static function getTextValidUntil(): string {
1941 return self::getTextSetting('valid_until', __('Valid Until Date', 'easy-invoice'));
1942 }
1943
1944 public static function getTextQuoteDate(): string {
1945 return self::getTextSetting('quote_date', __('Quote Date', 'easy-invoice'));
1946 }
1947
1948 public static function getTextFrom(): string {
1949 return self::getTextSetting('from', __('From', 'easy-invoice'));
1950 }
1951
1952 /**
1953 * Get the actual date format string from the stored format identifier
1954 *
1955 * @param string $format_identifier The format identifier (us, uk, iso)
1956 * @return string The actual date format string
1957 */
1958 public static function getDateFormatString($format_identifier = null): string {
1959 if ($format_identifier === null) {
1960 $format_identifier = get_option('easy_invoice_date_format', 'us');
1961 }
1962
1963 switch ($format_identifier) {
1964 case 'uk':
1965 return 'd/m/Y';
1966 case 'iso':
1967 return 'Y-m-d';
1968 case 'us':
1969 default:
1970 return 'm/d/Y';
1971 }
1972 }
1973
1974 /**
1975 * Get the current date format string
1976 *
1977 * @return string The current date format string
1978 */
1979 public static function getCurrentDateFormat(): string {
1980 return self::getDateFormatString();
1981 }
1982
1983 /**
1984 * Format a date using the Easy Invoice date format setting
1985 *
1986 * @param string|int $date The date to format (timestamp or date string)
1987 * @return string The formatted date
1988 */
1989 public static function formatDate($date): string {
1990 $format = self::getCurrentDateFormat();
1991 $timestamp = is_numeric($date) ? $date : strtotime($date);
1992 return date_i18n($format, $timestamp);
1993 }
1994
1995 /**
1996 * AJAX handler for regenerating invoice numbers
1997 */
1998 public function ajaxRegenerateInvoiceNumbers() {
1999
2000 // Verify nonce
2001 $nonce = $_POST['nonce'] ?? $_POST['easy_invoice_settings_nonce'] ?? '';
2002 if (!wp_verify_nonce($nonce, 'easy_invoice_settings')) {
2003 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
2004 }
2005
2006 // Check permissions
2007 if (!current_user_can('manage_options')) {
2008 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
2009 }
2010
2011 try {
2012 // Get the current next invoice number
2013 $current_next_number = intval(get_option('easy_invoice_next_invoice_number', 1));
2014 $prefix = get_option('easy_invoice_invoice_prefix', 'EIIN_');
2015
2016 // Start regeneration from the current next number
2017 $starting_number = $current_next_number;
2018
2019 // Get all invoices ordered by creation date
2020 $invoices = get_posts([
2021 'post_type' => 'easy_invoice',
2022 'post_status' => 'publish',
2023 'numberposts' => -1,
2024 'orderby' => 'date',
2025 'order' => 'ASC'
2026 ]);
2027
2028 if (empty($invoices)) {
2029 wp_send_json_success([
2030 'message' => __('No invoices found to regenerate', 'easy-invoice'),
2031 'regenerated_count' => 0
2032 ]);
2033 }
2034
2035 $regenerated_count = 0;
2036 $current_number = $starting_number;
2037
2038 foreach ($invoices as $invoice) {
2039 // Generate new invoice number
2040 $new_invoice_number = $prefix . str_pad($current_number, 6, '0', STR_PAD_LEFT);
2041
2042 // Update the invoice number
2043 update_post_meta($invoice->ID, '_easy_invoice_number', $new_invoice_number);
2044
2045 $regenerated_count++;
2046 $current_number++;
2047 }
2048
2049 // Update the next invoice number counter to continue from the last regenerated number + 1
2050 update_option('easy_invoice_next_invoice_number', $current_number);
2051
2052 wp_send_json_success([
2053 /* translators: %d: number regenerated. */
2054 'message' => sprintf(__('Successfully regenerated %d invoice numbers', 'easy-invoice'), $regenerated_count),
2055 'regenerated_count' => $regenerated_count,
2056 'next_number' => $current_number
2057 ]);
2058
2059 } catch (Exception $e) {
2060 wp_send_json_error(['message' => __('Failed to regenerate invoice numbers', 'easy-invoice')]);
2061 }
2062 }
2063
2064 /**
2065 * AJAX handler for regenerating quote numbers
2066 */
2067 public function ajaxRegenerateQuoteNumbers() {
2068
2069 // Verify nonce
2070 $nonce = $_POST['nonce'] ?? $_POST['easy_invoice_settings_nonce'] ?? '';
2071 if (!wp_verify_nonce($nonce, 'easy_invoice_settings')) {
2072 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
2073 }
2074
2075 // Check permissions
2076 if (!current_user_can('manage_options')) {
2077 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
2078 }
2079
2080 try {
2081 $current_next_number = intval(get_option('easy_invoice_next_quote_number', 1));
2082 $prefix = get_option('easy_invoice_quote_prefix', 'QT-');
2083
2084 $starting_number = $current_next_number;
2085
2086 $quotes = get_posts([
2087 'post_type' => 'easy_invoice_quote',
2088 'post_status' => 'publish',
2089 'numberposts' => -1,
2090 'orderby' => 'date',
2091 'order' => 'ASC',
2092 'meta_query' => [
2093 [
2094 'key' => '_easy_invoice_quote_number',
2095 'compare' => 'EXISTS'
2096 ]
2097 ]
2098 ]);
2099
2100 $regenerated_count = 0;
2101 $current_number = $starting_number;
2102
2103 foreach ($quotes as $quote_post) {
2104 $new_quote_number = $prefix . str_pad($current_number, 6, '0', STR_PAD_LEFT);
2105 update_post_meta($quote_post->ID, '_easy_invoice_quote_number', $new_quote_number);
2106 $regenerated_count++;
2107 $current_number++;
2108 }
2109
2110 update_option('easy_invoice_next_quote_number', $current_number);
2111
2112 wp_send_json_success([
2113 /* translators: %d: number regenerated. */
2114 'message' => sprintf(__('Successfully regenerated %d quote numbers', 'easy-invoice'), $regenerated_count),
2115 'regenerated_count' => $regenerated_count,
2116 'next_number' => $current_number
2117 ]);
2118
2119 } catch (Exception $e) {
2120 wp_send_json_error(['message' => __('Failed to regenerate quote numbers', 'easy-invoice')]);
2121 }
2122 }
2123 }
2124