PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Services/EmailNotification/Settings.php +534 -30 1.5.3 → 2.4.5 View file →
@@ -1,13 +1,15 @@
1 1 <?php
2 2
3 3 namespace FluentSupport\App\Services\EmailNotification;
4 +use FluentSupportPro\Database\Migrations\TimeTrackMigrator;
4 5
5 6 use FluentSupport\App\Services\Helper;
7 +use FluentSupport\App\Services\Notifications\NotificationSettings;
8 +use FluentSupport\Framework\Support\Arr;
6 9
7 10 class Settings
8 11 {
9 -
10 12 public function getEmailSettingsKeys()
11 13 {
12 14 $key = apply_filters('fluent_support/email_setting_keys', [
13 15 'ticket_created_email_to_customer',
@@ -13,9 +15,12 @@
13 15 'ticket_created_email_to_customer',
14 16 'ticket_replied_by_agent_email_to_customer',
15 17 'ticket_closed_by_agent_email_to_customer',
16 18 'ticket_created_email_to_admin',
17 - 'ticket_replied_by_customer_email_to_admin'
19 + 'ticket_replied_by_customer_email_to_admin',
20 + 'ticket_agent_on_change',
21 + 'ticket_created_by_agent_email_to_customer',
22 + 'ticket_created_by_agent_on_behalf_email_to_customer'
18 23 ]);
19 24 return $key;
20 25 }
21 26
@@ -27,8 +32,17 @@
27 32 'fields' => $this->getGlobalBusinessSettingsFields()
28 33 ];
29 34 }
30 35
36 + if ($settingsKey == NotificationSettings::OPTION_KEY) {
37 + $notificationSettings = new NotificationSettings();
38 +
39 + return [
40 + 'settings' => $notificationSettings->get(false),
41 + 'fields' => $notificationSettings->getFields()
42 + ];
43 + }
44 +
31 45 return [
32 46 'settings' => [],
33 47 'fields' => []
34 48 ];
@@ -33,17 +47,67 @@
33 47 'fields' => []
34 48 ];
35 49 }
36 50
51 + /**
52 + * save method will save the requested settings by settings key
53 + * @param $settingsKey
54 + * @param $settings
55 + * @return mixed
56 + */
37 57 public function save($settingsKey, $settings)
38 58 {
59 + if ($settingsKey == 'global_business_settings' && is_array($settings) && array_key_exists('internal_notifications_enabled', $settings)) {
60 + $notificationSettings = new NotificationSettings();
61 + $savedNotificationSettings = $notificationSettings->get(false);
62 + $savedNotificationSettings['enabled'] = $settings['internal_notifications_enabled'];
63 + $notificationSettings->save($savedNotificationSettings);
64 + }
65 +
39 66 if ($settingsKey == 'global_business_settings' && empty($settings['accepted_file_types'])) {
40 67 $settings['accepted_file_types'] = [];
41 68 }
42 69
43 - return Helper::updateOption($settingsKey, $settings);
70 + if ($settingsKey == 'global_business_settings') {
71 + $settings['ticket_link_portal_migrated'] = 'yes';
72 + if (isset($settings['min_serial_number'])) {
73 + $settings['min_serial_number'] = max(1, (int) $settings['min_serial_number']);
74 + }
75 + // The frontend only ever offers 3/7/15 — reject anything else
76 + // (a tampered request, or a stale/unexpected value) back to the
77 + // 15-day default rather than trusting the submitted value as-is.
78 + if (isset($settings['report_snapshot_retention']) && !in_array($settings['report_snapshot_retention'], ['3', '7', '15'], true)) {
79 + $settings['report_snapshot_retention'] = '15';
80 + }
81 + // These keys become user meta on signup, so keep only the offered fields.
82 + if (isset($settings['custom_registration_form_field'])) {
83 + $settings['custom_registration_form_field'] = array_values(array_intersect(
84 + (array) $settings['custom_registration_form_field'],
85 + array_keys(Helper::getCustomRegistrationFieldOptions())
86 + ));
87 + }
88 + }
89 +
90 + if ($settingsKey == 'global_business_settings' && !empty($settings['agent_time_tracking'])) {
91 + if (class_exists(TimeTrackMigrator::class) && $settings['agent_time_tracking'] === 'yes') {
92 + TimeTrackMigrator::migrate();
93 + }
94 + }
95 +
96 + if ($settingsKey == NotificationSettings::OPTION_KEY) {
97 + return (new NotificationSettings())->save($settings);
98 + }
99 +
100 + $result = Helper::updateOption($settingsKey, $settings);
101 +
102 + return $result;
44 103 }
45 104
105 + /**
106 + * globalBusinessSettings method will fetch global settings from database, parse and return
107 + * @param bool $cached
108 + * @return array|mixed
109 + */
46 110 public function globalBusinessSettings($cached = true)
47 111 {
48 112 static $settings;
49 113
@@ -52,14 +116,30 @@
52 116 }
53 117
54 118 $defaults = [
55 119 'portal_page_id' => '',
56 - 'login_message' => sprintf(__('%1sPlease login or create an account to access the Customer Support Portal%2s [fluent_support_auth]', 'fluent-support'), '<p>', '</p>'),
120 + 'ticket_link_portal' => 'default',
121 + 'ticket_link_portal_migrated' => 'no',
122 + // translators: %1$s is opening paragraph tag, %2$s is closing paragraph tag
123 + 'login_message' => sprintf(__('%1$sPlease login or create an account to access the Customer Support Portal%2$s [fluent_support_auth]', 'fluent-support'), '<p>', '</p>'),
57 124 'disable_public_ticket' => 'no',
58 125 'accepted_file_types' => ['images', 'csv', 'documents', 'zip', 'json'],
59 - 'max_file_size' => 2
126 + 'max_file_size' => 2,
127 + 'max_file_upload' => 3,
128 + 'del_files_on_close' => 'no',
129 + 'enable_admin_bar_summary' => 'no',
130 + 'internal_notifications_enabled' => 'no',
131 + 'enable_draft_mode' => 'no',
132 + 'agent_feedback_rating' => 'no',
133 + 'keyboard_shortcuts' => 'yes',
134 + 'enable_min_serial_number' => 'no',
135 + 'ticket_prefix' => '',
136 + 'min_serial_number' => 1,
137 + 'enable_fluent_booking_integration' => 'no',
138 + 'report_snapshot_retention' => '15',
60 139 ];
61 140
141 + //Get default/existing settings from database using the key global_business_settings
62 142 $existingSettings = Helper::getOption('global_business_settings', []);
63 143
64 144 if (!$existingSettings) {
65 145 $settings = $defaults;
@@ -66,14 +146,51 @@
66 146 return $settings;
67 147 }
68 148
69 149 $settings = wp_parse_args($existingSettings, $defaults);
150 + $settings['ticket_link_portal'] = $settings['ticket_link_portal'] ?: 'default';
70 151
152 + if (Arr::get($settings, 'ticket_link_portal_migrated') !== 'yes') {
153 + $settings['ticket_link_portal'] = $this->resolveLegacyTicketLinkPortal($settings);
154 + $settings['ticket_link_portal_migrated'] = 'yes';
155 + Helper::updateOption('global_business_settings', $settings);
156 + }
157 +
71 158 return $settings;
72 159 }
73 160
161 + private function resolveLegacyTicketLinkPortal($settings)
162 + {
163 + $selectedPortal = Arr::get($settings, 'ticket_link_portal', 'default');
164 +
165 + if ($selectedPortal && $selectedPortal !== 'default') {
166 + return $selectedPortal;
167 + }
168 +
169 + $ticketFormSettings = Helper::getOption('_ticket_form_settings', []);
170 +
171 + if (Arr::get($settings, 'enable_fc_menu') === 'yes') {
172 + return 'fluent_cart';
173 + }
174 +
175 + // The legacy pro plugin defaulted enable_woo_menu to 'yes', so treat an
176 + // absent value the same as 'yes' — only skip migration if explicitly 'no'.
177 + // WC portal was always Pro-only, so require Pro to be active as well.
178 + if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && defined('WC_PLUGIN_FILE') && Arr::get($ticketFormSettings, 'enable_woo_menu', 'yes') !== 'no') {
179 + return 'woocommerce';
180 + }
181 +
182 + return 'default';
183 + }
184 +
185 +
186 + /**
187 + * getGlobalBusinessSettingsFields method will prepare the list of field, and it's property that will be used in global settings form
188 + * @return array
189 + */
74 190 private function getGlobalBusinessSettingsFields()
75 191 {
192 + $nextSerialNumber = \FluentSupport\App\Models\Ticket::getNextSerialNumber();
76 193
77 194 $mimeGroups = Helper::getMimeGroups();
78 195
79 196 $formattedMimeGroups = [];
@@ -81,25 +198,56 @@
81 198 foreach ($mimeGroups as $mimeGroup => $mime) {
82 199 $formattedMimeGroups[$mimeGroup] = $mime['title'];
83 200 }
84 201
202 + $customRegistrationFormOptions = Helper::getCustomRegistrationFieldOptions();
85 203
86 204 $fields = [
205 + 'ticket_link_portal_description' => [
206 + 'type' => 'html-viewer',
207 + 'label' => __('Customer Portal', 'fluent-support'),
208 + 'wrapper_class' => 'fs_portal_destination_description',
209 + ],
210 + 'ticket_link_portal' => [
211 + 'type' => 'input-radio',
212 + 'wrapper_class' => 'fs_portal_destination_options',
213 + 'options' => $this->getTicketLinkPortalOptions(),
214 + ],
215 + 'fluent_community_portal_link' => [
216 + 'type' => 'html-viewer',
217 + 'wrapper_class' => 'fs_fluent_community_portal_link',
218 + 'html' => $this->getFluentCommunityPortalLinkHtml(),
219 + 'dependency' => [
220 + 'depends_on' => 'ticket_link_portal',
221 + 'operator' => '=',
222 + 'value' => 'fluent_community'
223 + ],
224 + ],
87 225 'portal_page_id' => [
88 226 'type' => 'input-options',
89 - 'label' => __('Portal Page', 'fluent-support'),
227 + 'label' => __('Select a Page', 'fluent-support'),
228 + 'wrapper_class' => 'fs_portal_page_selector',
90 229 'show_id' => true,
91 230 'placeholder' => __('Select Portal Page', 'fluent-support'),
92 - 'options' => Helper::getWPPages(),
93 - 'inline_help' => __('Please provide the page id where you want to show the tickets for your customers. Use shortcode <code>[fluent_support_portal]</code> in that page', 'fluent-support')
231 + 'options' => Helper::getWPPages(),//Get list of published pages
232 + 'inline_help' => __('Please provide the page id where you want to show the tickets for your customers. Use shortcode <code>[fluent_support_portal]</code> in that page', 'fluent-support'),
233 + 'admin_url' => admin_url(),
234 + 'home_url' => home_url('/'),
235 + 'dependency' => [
236 + 'depends_on' => 'ticket_link_portal',
237 + 'operator' => '=',
238 + 'value' => 'default'
239 + ],
94 240 ],
95 241 'login_message' => [
96 242 'type' => 'wp-editor',
97 243 'label' => __('Message for non logged in users', 'fluent-support'),
98 - 'inline_help' => 'Please provide message for not logged in users. You can place login shortcode too Use shortcode <code>[fluent_support_login]</code> to show built-in login form. For the user registration use this shortcode <code>[fluent_support_signup]</code> and for both form please use <code>[fluent_support_auth]</code>'
244 + 'inline_help' => __('Please provide message for not logged in users. You can place login shortcode too. Use shortcode <code>[fluent_support_login]</code> to show built-in login form. For the user registration use this shortcode <code>[fluent_support_signup]</code> and for both form please use <code>[fluent_support_auth]</code>. Registration forms only show when "Anyone can register" is enabled in Settings > General.', 'fluent-support')
99 245 ],
100 246 'disable_public_ticket' => [
247 + 'wrapper_class' => '',
101 248 'type' => 'inline-checkbox',
249 + 'label' => __('Public Ticket Interaction', 'fluent-support'),
102 250 'true_label' => 'yes',
103 251 'false-label' => 'no',
104 252 'checkbox_label' => __('Disable Public Ticket interaction', 'fluent-support'),
105 253 'inline_help' => __('If you enable this then only logged in user can reply the tickets. Otherwise, url will be signed and intended user can reply without logging in', 'fluent-support')
@@ -104,29 +252,332 @@
104 252 'checkbox_label' => __('Disable Public Ticket interaction', 'fluent-support'),
105 253 'inline_help' => __('If you enable this then only logged in user can reply the tickets. Otherwise, url will be signed and intended user can reply without logging in', 'fluent-support')
106 254 ],
107 255 'accepted_file_types' => [
108 - 'wrapper_class' => 'fs_half_field',
256 + 'wrapper_class' => '',
109 257 'type' => 'checkbox-group',
110 - 'label' => 'Accepted File Types',
258 + 'label' => __('Accepted File Types', 'fluent-support'),
111 259 'options' => $formattedMimeGroups
112 260 ],
113 261 'max_file_size' => [
114 - 'wrapper_class' => 'fs_half_field',
262 + 'wrapper_class' => 'fs_settings_half_field',
115 263 'type' => 'input-text',
116 264 'data_type' => 'number',
117 - 'label' => 'Max File Size (in MegaByte)',
265 + 'label' => __('Max File Size (in MegaByte)', 'fluent-support'),
266 + ],
267 + 'max_file_upload' => [
268 + 'wrapper_class' => 'fs_settings_half_field',
269 + 'type' => 'input-text',
270 + 'data_type' => 'number',
271 + 'label' => __('Maximum File Upload', 'fluent-support'),
272 + ],
273 + 'del_files_on_close' => [
274 + 'wrapper_class' => '',
275 + 'type' => 'inline-checkbox',
276 + 'true_label' => 'yes',
277 + 'false-label' => 'no',
278 + 'checkbox_label' => __('Delete all attachments on ticket close', 'fluent-support'),
279 + 'inline_help' => __('If you enable this feature, all attachments associated with a ticket will be deleted when the ticket is closed.', 'fluent-support')
280 + ],
281 + 'enable_admin_bar_summary' => [
282 + 'wrapper_class' => '',
283 + 'type' => 'inline-checkbox',
284 + 'true_label' => 'yes',
285 + 'false-label' => 'no',
286 + 'checkbox_label' => __('Enable Fluent Summary In Admin Bar', 'fluent-support'),
287 + 'inline_help' => __('If you enable this, logged in user can see the ticket summary from top nav bar.', 'fluent-support')
288 + ],
289 + 'internal_notifications_enabled' => [
290 + 'wrapper_class' => '',
291 + 'type' => 'inline-checkbox',
292 + 'true_label' => 'yes',
293 + 'false-label' => 'no',
294 + 'checkbox_label' => __('Enable Internal Notifications', 'fluent-support'),
295 + 'inline_help' => sprintf(
296 + '<ul><li>%1$s</li><li>%2$s</li></ul>',
297 + __('Enable the in-app notification bell, unread counts, and notification event storage for agents.', 'fluent-support'),
298 + __('Notification data older than 15 days will be removed automatically.', 'fluent-support')
299 + )
300 + ],
301 + 'enable_draft_mode' => [
302 + 'wrapper_class' => '',
303 + 'type' => 'inline-checkbox',
304 + 'true_label' => 'yes',
305 + 'false-label' => 'no',
306 + 'checkbox_label' => __('Enable Draft Mode', 'fluent-support'),
307 + 'inline_help' => __('If you enable this setting, any written response will be saved as a draft if an agent accidentally closes a ticket.', 'fluent-support')
308 + ],
309 + 'custom_registration_form_field' => [
310 + 'wrapper_class' => '',
311 + 'type' => 'checkbox-group',
312 + 'label' => __('Custom Registration Form Field', 'fluent-support'),
313 + 'options' => $customRegistrationFormOptions
314 + ],
315 + 'enable_two_fa' => [
316 + 'wrapper_class' => '',
317 + 'type' => 'inline-checkbox',
318 + 'true_label' => 'yes',
319 + 'false-label' => 'no',
320 + 'checkbox_label' => __('Enable Two-Factor Authentication', 'fluent-support'),
321 + 'inline_help' => __('If you enable this setting, users will be required to submit a second form of authentication, such as a code sent to their email, to login.', 'fluent-support')
322 + ],
323 + 'keyboard_shortcuts' => [
324 + 'wrapper_class' => '',
325 + 'type' => 'inline-checkbox',
326 + 'true_label' => 'yes',
327 + 'false-label' => 'no',
328 + 'checkbox_label' => __('Enable Keyboard Shortcuts', 'fluent-support'),
329 + 'inline_help' => __("If you enable this, agents can use keyboard shortcuts for faster actions.", 'fluent-support'),
330 + 'shortcut_modal' => $this->getKeyboardShortcutModalData()
331 + ],
332 + 'enable_min_serial_number' => [
333 + 'wrapper_class' => '',
334 + 'type' => 'inline-checkbox',
335 + 'true_label' => 'yes',
336 + 'false-label' => 'no',
337 + 'checkbox_label' => __('Enable Minimum Ticket Number', 'fluent-support'),
338 + 'inline_help' => __('Enable this to start public ticket numbers from a custom minimum number.', 'fluent-support')
339 + ],
340 + 'min_serial_number' => [
341 + 'wrapper_class' => 'fs_settings_half_field',
342 + 'type' => 'input-text',
343 + 'data_type' => 'number',
344 + 'label' => __('Minimum Ticket Number', 'fluent-support'),
345 + 'help' => __('Once a ticket is created, you cannot lower this value below the current highest ticket number. Set this carefully — raising it is easy, but lowering it below an existing ticket number is not allowed.', 'fluent-support'),
346 + 'inline_help' => sprintf(
347 + __('Next Ticket Number: %d', 'fluent-support'),
348 + $nextSerialNumber
349 + ),
350 + 'next_serial_number' => $nextSerialNumber,
351 + 'dependency' => [
352 + 'depends_on' => 'enable_min_serial_number',
353 + 'operator' => '=',
354 + 'value' => 'yes'
355 + ]
356 + ],
357 + 'ticket_prefix' => [
358 + 'wrapper_class' => 'fs_settings_half_field',
359 + 'type' => 'input-text',
360 + 'data_type' => 'text',
361 + 'label' => __('Ticket Prefix', 'fluent-support'),
362 + 'inline_help' => __('Optional prefix for newly created public ticket numbers.', 'fluent-support'),
363 + 'dependency' => [
364 + 'depends_on' => 'enable_min_serial_number',
365 + 'operator' => '=',
366 + 'value' => 'yes'
367 + ]
368 + ],
369 + 'report_snapshot_retention' => [
370 + 'wrapper_class' => '',
371 + 'type' => 'input-radio',
372 + 'label' => __('Snapshot Retention', 'fluent-support'),
373 + 'options' => [
374 + ['id' => '3', 'label' => __('3 days', 'fluent-support')],
375 + ['id' => '7', 'label' => __('7 days', 'fluent-support')],
376 + ['id' => '15', 'label' => __('15 days', 'fluent-support')],
377 + ],
378 + 'inline_help' => __('Advanced Reports saves a snapshot of each report every 6 hours. Snapshots older than this many days are automatically deleted.', 'fluent-support'),
118 379 ]
119 380 ];
381 + if (defined('FLUENT_BOOKING_VERSION')) {
382 + $fields['enable_fluent_booking_integration'] = [
383 + 'wrapper_class' => '',
384 + 'type' => 'inline-checkbox',
385 + 'true_label' => 'yes',
386 + 'false-label' => 'no',
387 + 'checkbox_label' => __('Enable Fluent Booking Integration', 'fluent-support'),
388 + 'inline_help' => __('If you enable this, agents can create booking links and view meeting details inside tickets.', 'fluent-support')
389 + ];
390 + }
120 391
392 + if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION')) {
393 + $fields['agent_feedback_rating'] = [
394 + 'wrapper_class' => '',
395 + 'type' => 'inline-checkbox',
396 + 'true_label' => 'yes',
397 + 'false-label' => 'no',
398 + 'checkbox_label' => __('Agent Feedback Rating', 'fluent-support'),
399 + 'inline_help' => __("If you enable this setting, users will have the option to provide feedback on an agent's response.", 'fluent-support')
400 + ];
401 +
402 + $fields['agent_time_tracking'] = [
403 + 'wrapper_class' => '',
404 + 'type' => 'inline-checkbox',
405 + 'true_label' => 'yes',
406 + 'false-label' => 'no',
407 + 'checkbox_label' => __('Agent Time Tracking', 'fluent-support'),
408 + 'inline_help' => __("If you enable this setting, the agent can specify the amount of time needed to complete a ticket.", 'fluent-support')
409 + ];
410 +
411 + }
412 +
413 + if (Helper::getAuthProvider() === 'fluent_auth') {
414 + $fields = $this->replaceTwoFaWithFluentAuthNotice($fields);
415 + }
416 +
121 417 return $fields;
122 418 }
123 419
420 + /**
421 + * With FluentAuth handling portal sign-in, our 2FA toggle no longer applies, so a note
422 + * pointing to FluentAuth's 2FA settings takes its place. The stored value is kept.
423 + *
424 + * @param array $fields
425 + * @return array
426 + */
427 + private function replaceTwoFaWithFluentAuthNotice($fields)
428 + {
429 + $notice = [
430 + 'type' => 'html-viewer',
431 + 'label' => __('Two-Factor Authentication', 'fluent-support'),
432 + 'wrapper_class' => 'fs_warn_alert_wrapper',
433 + 'html' => sprintf(
434 + /* translators: %s: URL of the FluentAuth settings page */
435 + __('Portal sign-in is handled by the FluentAuth plugin, so two-factor authentication for your customers and agents is set up there. FluentAuth applies it to the roles you choose, so include your customers\' role. <a href="%s">Open FluentAuth settings</a>', 'fluent-support'),
436 + esc_url(admin_url('admin.php?page=fluent-auth#/settings'))
437 + ),
438 + ];
439 +
440 + $replaced = [];
441 +
442 + foreach ($fields as $key => $field) {
443 + if ($key === 'enable_two_fa') {
444 + $replaced['fluent_auth_two_fa_notice'] = $notice;
445 + continue;
446 + }
447 +
448 + $replaced[$key] = $field;
449 + }
450 +
451 + return $replaced;
452 + }
453 +
454 + private function getTicketLinkPortalOptions()
455 + {
456 + $options = [
457 + [
458 + 'id' => 'default',
459 + 'label' => __('Default Portal Page', 'fluent-support'),
460 + ],
461 + ];
462 +
463 + if (Helper::isPortalActive('fluent_cart')) {
464 + $options[] = [
465 + 'id' => 'fluent_cart',
466 + 'label' => __('FluentCart Account Navigation', 'fluent-support'),
467 + ];
468 + }
469 +
470 + if (Helper::isPortalActive('woocommerce')) {
471 + $options[] = [
472 + 'id' => 'woocommerce',
473 + 'label' => __('WooCommerce Account Navigation', 'fluent-support'),
474 + ];
475 + }
476 +
477 + if (Helper::isPortalActive('fluent_community')) {
478 + $options[] = [
479 + 'id' => 'fluent_community',
480 + 'label' => __('FluentCommunity Portal', 'fluent-support'),
481 + ];
482 + }
483 +
484 + return apply_filters('fluent_support/ticket_link_portal_options', $options);
485 + }
486 +
487 + private function getFluentCommunityPortalLinkHtml()
488 + {
489 + if (!defined('FLUENT_COMMUNITY_PLUGIN_VERSION') || !class_exists('\FluentCommunity\App\Services\Helper')) {
490 + return '';
491 + }
492 +
493 + $portalLink = rtrim(\FluentCommunity\App\Services\Helper::baseUrl('support/'), '/\\') . '/';
494 +
495 + return '<div class="fs_portal_link_content">'
496 + . '<span class="fs_portal_link_label">' . esc_html__('FluentCommunity Portal URL', 'fluent-support') . '</span>'
497 + . '<button type="button" class="fs_portal_link_token" data-portal-link="' . esc_attr($portalLink) . '">'
498 + . '<span class="fs_portal_link_text">' . esc_html($portalLink) . '</span>'
499 + . '<span class="fs_portal_link_action">' . esc_html__('Copy', 'fluent-support') . '</span>'
500 + . '</button>'
501 + . '<p class="fc_inline_help">' . esc_html__('Copy this URL and add it to your FluentCommunity menu.', 'fluent-support') . '</p>'
502 + . '</div>';
503 + }
504 +
505 + /**
506 + * Returns translatable keyboard shortcut modal data for the settings form.
507 + *
508 + * @return array{title: string, mac_label: string, win_label: string, action_label: string, shortcut_label: string, mac_shortcuts: array, win_shortcuts: array}
509 + */
510 + public function getKeyboardShortcutModalData()
511 + {
512 + $actionLabel = __('Action', 'fluent-support');
513 + $shortcutLabel = __('Shortcut', 'fluent-support');
514 +
515 + // Windows and Linux use Ctrl where macOS uses ⌘ (see resources/admin/Bits/shortcuts.js).
516 + $macShortcuts = [
517 + ['action' => __('Tickets: Advanced filter toggle', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + f'],
518 + ['action' => __('Tickets: Refresh', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + q'],
519 + ['action' => __('Tickets: Create Ticket', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + n'],
520 + ['action' => __('Tickets: Reset filter', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + r'],
521 + ['action' => __('Tickets: All Tickets', 'fluent-support'), 'shortcut' => '⌘ + ⇧ + a'],
522 + ['action' => __('Tickets: My Tickets', 'fluent-support'), 'shortcut' => '⌘ + ⇧ + m'],
523 + ['action' => __('Tickets: Unassigned', 'fluent-support'), 'shortcut' => '⌘ + ⇧ + u'],
524 + ['action' => __('Tickets: Waiting for reply', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + w'],
525 + ['action' => __('Tickets: Bookmarks', 'fluent-support'), 'shortcut' => '⌘ + ⇧ + b'],
526 + ['action' => __('Tickets: Toggle (Open, active, close, all)', 'fluent-support'), 'shortcut' => '⌘ + ⇧ + → / ←'],
527 + ['action' => __('Ticket Reply: Reply', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + r'],
528 + ['action' => __('Ticket Reply: Personal note', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + n'],
529 + ['action' => __('Ticket Reply: Merge', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + m'],
530 + ['action' => __('Ticket Reply: Bookmarks', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + b'],
531 + ['action' => __('Ticket Reply: Refresh', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + q'],
532 + ['action' => __('Ticket Reply: Next ticket', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + ↓'],
533 + ['action' => __('Ticket Reply: Previous ticket', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + ↑'],
534 + ['action' => __('Ticket Reply: Close ticket', 'fluent-support'), 'shortcut' => '⌘ + ⌥ + x'],
535 + ];
536 +
537 + $winShortcuts = [
538 + ['action' => __('Tickets: Advanced filter toggle', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + f'],
539 + ['action' => __('Tickets: Refresh', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + q'],
540 + ['action' => __('Tickets: Create Ticket', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + n'],
541 + ['action' => __('Tickets: Reset filter', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + r'],
542 + ['action' => __('Tickets: All Tickets', 'fluent-support'), 'shortcut' => 'Ctrl + Shift + a'],
543 + ['action' => __('Tickets: My Tickets', 'fluent-support'), 'shortcut' => 'Ctrl + Shift + m'],
544 + ['action' => __('Tickets: Unassigned', 'fluent-support'), 'shortcut' => 'Ctrl + Shift + u'],
545 + ['action' => __('Tickets: Waiting for reply', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + w'],
546 + ['action' => __('Tickets: Bookmarks', 'fluent-support'), 'shortcut' => 'Ctrl + Shift + b'],
547 + ['action' => __('Tickets: Toggle (Open, active, close, all)', 'fluent-support'), 'shortcut' => 'Ctrl + Shift + → / ←'],
548 + ['action' => __('Ticket Reply: Reply', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + r'],
549 + ['action' => __('Ticket Reply: Personal note', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + n'],
550 + ['action' => __('Ticket Reply: Merge', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + m'],
551 + ['action' => __('Ticket Reply: Bookmarks', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + b'],
552 + ['action' => __('Ticket Reply: Refresh', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + q'],
553 + ['action' => __('Ticket Reply: Next ticket', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + ↓'],
554 + ['action' => __('Ticket Reply: Previous ticket', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + ↑'],
555 + ['action' => __('Ticket Reply: Close ticket', 'fluent-support'), 'shortcut' => 'Ctrl + Alt + x'],
556 + ];
557 +
558 + return [
559 + 'title' => __('Keyboard Shortcuts', 'fluent-support'),
560 + 'mac_label' => __('macOS', 'fluent-support'),
561 + 'win_label' => __('Windows', 'fluent-support'),
562 + 'action_label' => $actionLabel,
563 + 'shortcut_label'=> $shortcutLabel,
564 + 'mac_shortcuts' => $macShortcuts,
565 + 'win_shortcuts' => $winShortcuts,
566 + ];
567 + }
568 +
124 569 public function saveBoxEmailSettings($box, $emailKey, $settings)
125 570 {
126 571 return $box->saveMeta('_email_' . $emailKey, $settings);
127 572 }
128 573
574 + /**
575 + * getBoxEmailSettings method will reply the email settings
576 + * @param $box
577 + * @param $emailKey
578 + * @return array|false
579 + */
129 580 public function getBoxEmailSettings($box, $emailKey)
130 581 {
131 582 if (!$box) {
132 583 return false;
@@ -131,12 +582,13 @@
131 582 if (!$box) {
132 583 return false;
133 584 }
134 585
586 + #ticket_closed_by_agent_email_to_customer 2 times, is it wrong or right!!!
135 587 $strictSubjectKeys = apply_filters('fluent_support/strict_subjects', [
136 588 'ticket_replied_by_agent_email_to_customer',
137 589 'ticket_closed_by_agent_email_to_customer',
138 - 'ticket_closed_by_agent_email_to_customer'
590 + 'ticket_created_email_to_customer'
139 591 ]);
140 592
141 593 $settingsDefaults = [
142 594 'ticket_created_email_to_customer' => [
@@ -142,38 +594,68 @@
142 594 'ticket_created_email_to_customer' => [
143 595 'key' => 'ticket_created_email_to_customer',
144 596 'title' => __('Ticket Created (To Customer)', 'fluent-support'),
145 597 'description' => __('This email will be sent when a customer submit a support ticket', 'fluent-support'),
146 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
147 - 'default_status' => 'no'
598 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
599 + 'default_status' => 'no',
600 + 'send_attachments'=> 'no'
148 601 ],
149 602 'ticket_replied_by_agent_email_to_customer' => [
150 603 'key' => 'ticket_replied_by_agent_email_to_customer',
151 604 'title' => __('Replied by Agent (To Customer)', 'fluent-support'),
152 605 'description' => __('This email will be sent when an agent reply to a ticket', 'fluent-support'),
153 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
154 - 'default_status' => 'yes'
606 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
607 + 'default_status' => 'yes',
608 + 'send_attachments'=> 'no'
155 609 ],
156 610 'ticket_closed_by_agent_email_to_customer' => [
157 611 'key' => 'ticket_closed_by_agent_email_to_customer',
158 612 'title' => __('Ticket Closed by Agent (To Customer)', 'fluent-support'),
159 613 'description' => __('This email will be sent when an agent close a ticket', 'fluent-support'),
160 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
161 - 'default_status' => 'yes'
614 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
615 + 'default_status' => 'no',
616 + 'send_attachments'=> 'no'
162 617 ],
163 618 'ticket_created_email_to_admin' => [
164 619 'key' => 'ticket_created_email_to_admin',
165 620 'title' => __('Ticket Created (To Admin)', 'fluent-support'),
166 621 'description' => __('This email will be sent when the business when a new ticket has been submitted', 'fluent-support'),
167 - 'email_subject' => 'New Ticket: {{ticket.title}} #{{ticket.id}}',
168 - 'default_status' => 'yes'
622 + 'email_subject' => 'New Ticket: {{ticket.title}} #{{ticket.public_id}}',
623 + 'default_status' => 'yes',
624 + 'send_attachments'=> 'no'
169 625 ],
170 626 'ticket_replied_by_customer_email_to_admin' => [
171 627 'key' => 'ticket_replied_by_customer_email_to_admin',
172 628 'title' => __('Replied by Customer (To Agent/Admin)', 'fluent-support'),
173 629 'description' => __('This email will be sent to Assigned Agent or Admin when a customer reply to a ticket', 'fluent-support'),
174 - 'email_subject' => 'New Response: {{ticket.title}} #{{ticket.id}}',
175 - 'default_status' => 'yes'
630 + 'email_subject' => 'New Response: {{ticket.title}} #{{ticket.public_id}}',
631 + 'default_status' => 'yes',
632 + 'send_attachments'=> 'no'
633 + ],
634 + 'ticket_agent_on_change' => [
635 + 'key' => 'ticket_agent_on_change',
636 + 'title' => __('Ticket Agent Change (To Agent)', 'fluent-support'),
637 + 'description' => __('This email will be sent to newly assigned agent', 'fluent-support'),
638 + 'email_subject' => 'Ticket Agent Change: {{ticket.title}} #{{ticket.public_id}}',
639 + 'default_status' => 'yes',
640 + 'send_attachments'=> 'no'
641 + ],
642 + 'ticket_created_by_agent_email_to_customer' => [
643 + 'key' => 'ticket_created_by_agent_email_to_customer',
644 + 'title' => __('Ticket Logged by Agent (To Customer)', 'fluent-support'),
645 + 'description' => __('This email will be sent when an agent logs a ticket on behalf of a customer (e.g. from a phone call or email received outside the system)', 'fluent-support'),
646 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
647 + 'default_status' => 'no',
648 + 'send_attachments'=> 'no'
649 + ],
650 + 'ticket_created_by_agent_on_behalf_email_to_customer' => [
651 + 'key' => 'ticket_created_by_agent_on_behalf_email_to_customer',
652 + 'title' => __('Agent Outreach Ticket (To Customer)', 'fluent-support'),
653 + 'description' => __('This email will be sent when an agent creates a ticket using the "Agent Initiated" option to proactively reach out to a customer — includes the agent\'s message content', 'fluent-support'),
654 + // Outreach reads like an email the agent wrote: their subject, their message.
655 + 'email_subject' => '{{ticket.title}}',
656 + 'default_status' => 'yes',
657 + 'send_attachments'=> 'no'
176 658 ]
177 659 ];
178 660
179 661 if (!isset($settingsDefaults[$emailKey])) {
@@ -189,9 +671,10 @@
189 671 'title' => $settingsDefaults[$emailKey]['title'],
190 672 'email_subject' => $settingsDefaults[$emailKey]['email_subject'],
191 673 'email_body' => $this->getDefaultEmailBody($emailKey, $box->box_type),
192 674 'status' => $settingsDefaults[$emailKey]['default_status'],
193 - 'can_edit_subject' => (in_array($emailKey, $strictSubjectKeys) && $box->box_type == 'email') ? 'no' : 'yes'
675 + 'can_edit_subject' => (in_array($emailKey, $strictSubjectKeys) && $box->box_type == 'email') ? 'no' : 'yes',
676 + 'send_attachments' => $settingsDefaults[$emailKey]['send_attachments']
194 677 ];
195 678
196 679 if ($box->box_type == 'email' && in_array($emailKey, $strictSubjectKeys)) {
197 680 $savedSettings['email_subject'] = 'Re: {{ticket.title}}';
@@ -221,35 +704,56 @@
221 704 if (empty($savedSettings['email_body'])) {
222 705 $savedSettings['email_body'] = $this->getDefaultEmailBody($emailKey, $box->box_type);
223 706 }
224 707
708 + if (empty($savedSettings['send_attachments'])) {
709 + $savedSettings['send_attachments'] = $settingsDefaults[$emailKey]['send_attachments'];
710 + }
711 +
225 712 return $savedSettings;
226 713 }
227 714
715 + /**
716 + * getDefaultEmailBody method will return html for email body
717 + * @param $emailKey
718 + * @param string $type
719 + * @return string
720 + */
228 721 private function getDefaultEmailBody($emailKey, $type = 'web')
229 722 {
230 723 if ($emailKey == 'ticket_created_email_to_customer') {
231 724 if ($type == 'web') {
232 - return '<p>Hi <strong><em>{{customer.full_name}}</em>,</strong></p><p>Your request (<a href="{{ticket.public_url}}">#{{ticket.id}}</a>) has been received, and is being reviewed by our support staff.</p><p>To add additional comments, follow the link below:</p><h4><a href="{{ticket.public_url}}">View Ticket</a></h4><p>&nbsp;</p><p>or follow this link: {{ticket.public_url}}</p><hr /><p>{{business.name}}</p>';
725 + return '<p>Hi <strong><em>{{customer.full_name}}</em>,</strong></p><p>Your request (<a href="{{ticket.public_url}}">#{{ticket.public_id}}</a>) has been received, and is being reviewed by our support staff.</p><p>To add additional comments, follow the link below:</p><h4><a href="{{ticket.public_url}}">View Ticket</a></h4><p>&nbsp;</p><p>or follow this link: {{ticket.public_url}}</p><hr /><p>{{business.name}}</p>';
233 726 } else {
234 727 return '<p>Hi <strong><em>{{customer.full_name}}</em>,</strong></p><p>Your request has been received, and is being reviewed by our support staff.</p><p>Our support staff will reply back to you soon</p>';
235 728 }
236 729 } else if ($emailKey == 'ticket_replied_by_agent_email_to_customer') {
237 730 if ($type == 'web') {
238 - return '<p>Hi <strong><em>{{customer.full_name}}</em>,</strong></p><p>An agent just replied to your ticket "<strong>{{ticket.title}}</strong>" (<a href="{{ticket.public_url}}">#{{ticket.id}}</a>). To view his reply or add additional comments, click the button below:</p><h4><a href="{{ticket.public_url}}">View Ticket</a></h4><p>or follow this link: {{ticket.public_url}}</p><hr /><p>Regards,<br />{{business.name}}</p>';
731 + return '<p>Hi <strong><em>{{customer.full_name}}</em>,</strong></p><p>An agent just replied to your ticket "<strong>{{ticket.title}}</strong>" (<a href="{{ticket.public_url}}">#{{ticket.public_id}}</a>). To view the reply or add additional comments, click the button below:</p><h4><a href="{{ticket.public_url}}">View Ticket</a></h4><p>or follow this link: {{ticket.public_url}}</p><hr /><p>Regards,<br />{{business.name}}</p>';
239 732 } else {
240 - return '{{response.full_content}}<p>Regards,<br />{{agent.full_name}}</p>';
733 + // {{agent.signature}} is empty when the agent has none.
734 + return '{{response.full_content}}{{agent.signature}}';
241 735 }
242 736 } else if ($emailKey == 'ticket_closed_by_agent_email_to_customer') {
243 737 if ($type == 'web') {
244 - return '<p>Hi <strong><em>{{customer.full_name}},</strong></p><p>Your ticket - {{ticket.title}}</p><p>We hope that the ticket was resolved to your satisfaction. If you feel that the ticket should not be closed or if the ticket has not been resolved, please reopen the ticket (<a href="{{ticket.public_url}}">#{{ticket.id}}</a>)</p><p>Regards,<br />{{business.name}}</p>';
738 + return '<p>Hi <strong><em>{{customer.full_name}},</strong></p><p>Your ticket - {{ticket.title}}</p><p>We hope that the ticket was resolved to your satisfaction. If you feel that the ticket should not be closed or if the ticket has not been resolved, please reopen the ticket (<a href="{{ticket.public_url}}">#{{ticket.public_id}}</a>)</p><p>Regards,<br />{{business.name}}</p>';
245 739 } else {
246 740 return '<p>Hi <strong><em>{{customer.full_name}},</strong></p><p>Your ticket - {{ticket.title}}</p><p>We hope that the ticket was resolved to your satisfaction. If you feel that the ticket should not be closed or if the ticket has not been resolved, please feel free to reply back.<p>Regards,<br />{{business.name}}</p>';
247 741 }
248 742 } else if ($emailKey == 'ticket_created_email_to_admin') {
249 - return '<p>A new ticket (<a href="{{ticket.admin_url}}">{{ticket.title}}</a>) as been submitted by {{customer.full_name}}</p><h4>Ticket Body</h4><p>{{ticket.content}}</p><p><b><a href="{{ticket.admin_url}}">View Ticket</a></b></p>';
743 + return '<p>A new ticket (<a href="{{ticket.admin_url}}">{{ticket.title}}</a>) has been submitted by {{customer.full_name}}</p><h4>Ticket Body</h4><p>{{ticket.content}}</p><p><b><a href="{{ticket.admin_url}}">View Ticket</a></b></p>';
250 744 } else if ($emailKey == 'ticket_replied_by_customer_email_to_admin') {
251 745 return '<p>A new response has been added to "<a href="{{ticket.admin_url}}">{{ticket.title}}</a>" by {{customer.full_name}}</p><h4>Response Body</h4><p>{{response.content}}</p><p><b><a href="{{ticket.admin_url}}">View Ticket</a></b></p>';
746 + } else if($emailKey == 'ticket_agent_on_change') {
747 + return '<p>Hi <strong><em>{{agent.full_name}}</em>,</strong></p><p>Ticket "<a href="{{ticket.admin_url}}">#{{ticket.public_id}}</a>" assigned to you.</p>';
748 + } else if($emailKey == 'ticket_created_by_agent_email_to_customer') {
749 + return '<p>Hi <strong><em>{{customer.full_name}}</em>,</strong></p><p>{{agent.full_name}} created a ticket on behalf of you, you can check it <a href="{{ticket.public_url}}">here</a></p>.';
750 + } else if($emailKey == 'ticket_created_by_agent_on_behalf_email_to_customer') {
751 + if ($type == 'web') {
752 + // Web mailboxes take replies in the portal, not by email.
753 + return '{{response.full_content}}{{agent.signature}}<p><a href="{{ticket.public_url}}">Reply to this conversation</a></p>';
754 + }
755 + return '{{response.full_content}}{{agent.signature}}';
252 756 }
253 757
254 758 return '';
255 759 }