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 +519 -33 1.5.4 → 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',
@@ -14,9 +16,11 @@
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 19 'ticket_replied_by_customer_email_to_admin',
18 - 'ticket_agent_on_change'
20 + 'ticket_agent_on_change',
21 + 'ticket_created_by_agent_email_to_customer',
22 + 'ticket_created_by_agent_on_behalf_email_to_customer'
19 23 ]);
20 24 return $key;
21 25 }
22 26
@@ -28,8 +32,17 @@
28 32 'fields' => $this->getGlobalBusinessSettingsFields()
29 33 ];
30 34 }
31 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 +
32 45 return [
33 46 'settings' => [],
34 47 'fields' => []
35 48 ];
@@ -34,17 +47,67 @@
34 47 'fields' => []
35 48 ];
36 49 }
37 50
51 + /**
52 + * save method will save the requested settings by settings key
53 + * @param $settingsKey
54 + * @param $settings
55 + * @return mixed
56 + */
38 57 public function save($settingsKey, $settings)
39 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 +
40 66 if ($settingsKey == 'global_business_settings' && empty($settings['accepted_file_types'])) {
41 67 $settings['accepted_file_types'] = [];
42 68 }
43 69
44 - 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;
45 103 }
46 104
105 + /**
106 + * globalBusinessSettings method will fetch global settings from database, parse and return
107 + * @param bool $cached
108 + * @return array|mixed
109 + */
47 110 public function globalBusinessSettings($cached = true)
48 111 {
49 112 static $settings;
50 113
@@ -53,15 +116,30 @@
53 116 }
54 117
55 118 $defaults = [
56 119 'portal_page_id' => '',
57 - '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>'),
58 124 'disable_public_ticket' => 'no',
59 125 'accepted_file_types' => ['images', 'csv', 'documents', 'zip', 'json'],
60 126 'max_file_size' => 2,
61 - 'del_files_on_close' => 'no'
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',
62 139 ];
63 140
141 + //Get default/existing settings from database using the key global_business_settings
64 142 $existingSettings = Helper::getOption('global_business_settings', []);
65 143
66 144 if (!$existingSettings) {
67 145 $settings = $defaults;
@@ -68,14 +146,51 @@
68 146 return $settings;
69 147 }
70 148
71 149 $settings = wp_parse_args($existingSettings, $defaults);
150 + $settings['ticket_link_portal'] = $settings['ticket_link_portal'] ?: 'default';
72 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 +
73 158 return $settings;
74 159 }
75 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 + */
76 190 private function getGlobalBusinessSettingsFields()
77 191 {
192 + $nextSerialNumber = \FluentSupport\App\Models\Ticket::getNextSerialNumber();
78 193
79 194 $mimeGroups = Helper::getMimeGroups();
80 195
81 196 $formattedMimeGroups = [];
@@ -83,25 +198,56 @@
83 198 foreach ($mimeGroups as $mimeGroup => $mime) {
84 199 $formattedMimeGroups[$mimeGroup] = $mime['title'];
85 200 }
86 201
202 + $customRegistrationFormOptions = Helper::getCustomRegistrationFieldOptions();
87 203
88 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 + ],
89 225 'portal_page_id' => [
90 226 'type' => 'input-options',
91 - 'label' => __('Portal Page', 'fluent-support'),
227 + 'label' => __('Select a Page', 'fluent-support'),
228 + 'wrapper_class' => 'fs_portal_page_selector',
92 229 'show_id' => true,
93 230 'placeholder' => __('Select Portal Page', 'fluent-support'),
94 - 'options' => Helper::getWPPages(),
95 - '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 + ],
96 240 ],
97 241 'login_message' => [
98 242 'type' => 'wp-editor',
99 243 'label' => __('Message for non logged in users', 'fluent-support'),
100 - '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')
101 245 ],
102 246 'disable_public_ticket' => [
247 + 'wrapper_class' => '',
103 248 'type' => 'inline-checkbox',
249 + 'label' => __('Public Ticket Interaction', 'fluent-support'),
104 250 'true_label' => 'yes',
105 251 'false-label' => 'no',
106 252 'checkbox_label' => __('Disable Public Ticket interaction', 'fluent-support'),
107 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,36 +252,332 @@
106 252 'checkbox_label' => __('Disable Public Ticket interaction', 'fluent-support'),
107 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')
108 254 ],
109 255 'accepted_file_types' => [
110 - 'wrapper_class' => 'fs_half_field',
256 + 'wrapper_class' => '',
111 257 'type' => 'checkbox-group',
112 258 'label' => __('Accepted File Types', 'fluent-support'),
113 259 'options' => $formattedMimeGroups
114 260 ],
115 261 'max_file_size' => [
116 - 'wrapper_class' => 'fs_half_field',
262 + 'wrapper_class' => 'fs_settings_half_field',
117 263 'type' => 'input-text',
118 264 'data_type' => 'number',
119 - 'label' => 'Max File Size (in MegaByte)',
265 + 'label' => __('Max File Size (in MegaByte)', 'fluent-support'),
120 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 + ],
121 273 'del_files_on_close' => [
274 + 'wrapper_class' => '',
122 275 'type' => 'inline-checkbox',
123 276 'true_label' => 'yes',
124 277 'false-label' => 'no',
125 278 'checkbox_label' => __('Delete all attachments on ticket close', 'fluent-support'),
126 - 'inline_help' => __('If you enable this then when a ticket get closed it will delete all the attachments associated with the particular ticket.', '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'),
127 379 ]
128 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 + }
129 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 +
130 417 return $fields;
131 418 }
132 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 +
133 569 public function saveBoxEmailSettings($box, $emailKey, $settings)
134 570 {
135 571 return $box->saveMeta('_email_' . $emailKey, $settings);
136 572 }
137 573
574 + /**
575 + * getBoxEmailSettings method will reply the email settings
576 + * @param $box
577 + * @param $emailKey
578 + * @return array|false
579 + */
138 580 public function getBoxEmailSettings($box, $emailKey)
139 581 {
140 582 if (!$box) {
141 583 return false;
@@ -140,12 +582,13 @@
140 582 if (!$box) {
141 583 return false;
142 584 }
143 585
586 + #ticket_closed_by_agent_email_to_customer 2 times, is it wrong or right!!!
144 587 $strictSubjectKeys = apply_filters('fluent_support/strict_subjects', [
145 588 'ticket_replied_by_agent_email_to_customer',
146 589 'ticket_closed_by_agent_email_to_customer',
147 - 'ticket_closed_by_agent_email_to_customer'
590 + 'ticket_created_email_to_customer'
148 591 ]);
149 592
150 593 $settingsDefaults = [
151 594 'ticket_created_email_to_customer' => [
@@ -151,45 +594,68 @@
151 594 'ticket_created_email_to_customer' => [
152 595 'key' => 'ticket_created_email_to_customer',
153 596 'title' => __('Ticket Created (To Customer)', 'fluent-support'),
154 597 'description' => __('This email will be sent when a customer submit a support ticket', 'fluent-support'),
155 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
156 - 'default_status' => 'no'
598 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
599 + 'default_status' => 'no',
600 + 'send_attachments'=> 'no'
157 601 ],
158 602 'ticket_replied_by_agent_email_to_customer' => [
159 603 'key' => 'ticket_replied_by_agent_email_to_customer',
160 604 'title' => __('Replied by Agent (To Customer)', 'fluent-support'),
161 605 'description' => __('This email will be sent when an agent reply to a ticket', 'fluent-support'),
162 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
163 - 'default_status' => 'yes'
606 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
607 + 'default_status' => 'yes',
608 + 'send_attachments'=> 'no'
164 609 ],
165 610 'ticket_closed_by_agent_email_to_customer' => [
166 611 'key' => 'ticket_closed_by_agent_email_to_customer',
167 612 'title' => __('Ticket Closed by Agent (To Customer)', 'fluent-support'),
168 613 'description' => __('This email will be sent when an agent close a ticket', 'fluent-support'),
169 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
170 - 'default_status' => 'yes'
614 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
615 + 'default_status' => 'no',
616 + 'send_attachments'=> 'no'
171 617 ],
172 618 'ticket_created_email_to_admin' => [
173 619 'key' => 'ticket_created_email_to_admin',
174 620 'title' => __('Ticket Created (To Admin)', 'fluent-support'),
175 621 'description' => __('This email will be sent when the business when a new ticket has been submitted', 'fluent-support'),
176 - 'email_subject' => 'New Ticket: {{ticket.title}} #{{ticket.id}}',
177 - 'default_status' => 'yes'
622 + 'email_subject' => 'New Ticket: {{ticket.title}} #{{ticket.public_id}}',
623 + 'default_status' => 'yes',
624 + 'send_attachments'=> 'no'
178 625 ],
179 626 'ticket_replied_by_customer_email_to_admin' => [
180 627 'key' => 'ticket_replied_by_customer_email_to_admin',
181 628 'title' => __('Replied by Customer (To Agent/Admin)', 'fluent-support'),
182 629 'description' => __('This email will be sent to Assigned Agent or Admin when a customer reply to a ticket', 'fluent-support'),
183 - 'email_subject' => 'New Response: {{ticket.title}} #{{ticket.id}}',
184 - 'default_status' => 'yes'
630 + 'email_subject' => 'New Response: {{ticket.title}} #{{ticket.public_id}}',
631 + 'default_status' => 'yes',
632 + 'send_attachments'=> 'no'
185 633 ],
186 634 'ticket_agent_on_change' => [
187 635 'key' => 'ticket_agent_on_change',
188 636 'title' => __('Ticket Agent Change (To Agent)', 'fluent-support'),
189 637 'description' => __('This email will be sent to newly assigned agent', 'fluent-support'),
190 - 'email_subject' => 'Ticket Agent Change: {{ticket.title}} #{{ticket.id}}',
191 - 'default_status' => 'yes'
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'
192 658 ]
193 659 ];
194 660
195 661 if (!isset($settingsDefaults[$emailKey])) {
@@ -205,9 +671,10 @@
205 671 'title' => $settingsDefaults[$emailKey]['title'],
206 672 'email_subject' => $settingsDefaults[$emailKey]['email_subject'],
207 673 'email_body' => $this->getDefaultEmailBody($emailKey, $box->box_type),
208 674 'status' => $settingsDefaults[$emailKey]['default_status'],
209 - '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']
210 677 ];
211 678
212 679 if ($box->box_type == 'email' && in_array($emailKey, $strictSubjectKeys)) {
213 680 $savedSettings['email_subject'] = 'Re: {{ticket.title}}';
@@ -237,37 +704,56 @@
237 704 if (empty($savedSettings['email_body'])) {
238 705 $savedSettings['email_body'] = $this->getDefaultEmailBody($emailKey, $box->box_type);
239 706 }
240 707
708 + if (empty($savedSettings['send_attachments'])) {
709 + $savedSettings['send_attachments'] = $settingsDefaults[$emailKey]['send_attachments'];
710 + }
711 +
241 712 return $savedSettings;
242 713 }
243 714
715 + /**
716 + * getDefaultEmailBody method will return html for email body
717 + * @param $emailKey
718 + * @param string $type
719 + * @return string
720 + */
244 721 private function getDefaultEmailBody($emailKey, $type = 'web')
245 722 {
246 723 if ($emailKey == 'ticket_created_email_to_customer') {
247 724 if ($type == 'web') {
248 - 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>';
249 726 } else {
250 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>';
251 728 }
252 729 } else if ($emailKey == 'ticket_replied_by_agent_email_to_customer') {
253 730 if ($type == 'web') {
254 - 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>';
255 732 } else {
256 - 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}}';
257 735 }
258 736 } else if ($emailKey == 'ticket_closed_by_agent_email_to_customer') {
259 737 if ($type == 'web') {
260 - 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>';
261 739 } else {
262 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>';
263 741 }
264 742 } else if ($emailKey == 'ticket_created_email_to_admin') {
265 - 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>';
266 744 } else if ($emailKey == 'ticket_replied_by_customer_email_to_admin') {
267 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>';
268 746 } else if($emailKey == 'ticket_agent_on_change') {
269 - return '<p>Hi <strong><em>{{agent.full_name}}</em>,</strong></p><p>Ticket "<a href="{{ticket.admin_url}}">#{{ticket.id}}</a>" assigned to you.</p>';
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}}';
270 756 }
271 757
272 758 return '';
273 759 }