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 +390 -44 1.10.1 → 2.4.5 View file →
@@ -3,12 +3,13 @@
3 3 namespace FluentSupport\App\Services\EmailNotification;
4 4 use FluentSupportPro\Database\Migrations\TimeTrackMigrator;
5 5
6 6 use FluentSupport\App\Services\Helper;
7 +use FluentSupport\App\Services\Notifications\NotificationSettings;
8 +use FluentSupport\Framework\Support\Arr;
7 9
8 10 class Settings
9 11 {
10 -
11 12 public function getEmailSettingsKeys()
12 13 {
13 14 $key = apply_filters('fluent_support/email_setting_keys', [
14 15 'ticket_created_email_to_customer',
@@ -16,9 +17,10 @@
16 17 'ticket_closed_by_agent_email_to_customer',
17 18 'ticket_created_email_to_admin',
18 19 'ticket_replied_by_customer_email_to_admin',
19 20 'ticket_agent_on_change',
20 - 'ticket_created_by_agent_email_to_customer'
21 + 'ticket_created_by_agent_email_to_customer',
22 + 'ticket_created_by_agent_on_behalf_email_to_customer'
21 23 ]);
22 24 return $key;
23 25 }
24 26
@@ -30,8 +32,17 @@
30 32 'fields' => $this->getGlobalBusinessSettingsFields()
31 33 ];
32 34 }
33 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 +
34 45 return [
35 46 'settings' => [],
36 47 'fields' => []
37 48 ];
@@ -44,12 +55,39 @@
44 55 * @return mixed
45 56 */
46 57 public function save($settingsKey, $settings)
47 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 +
48 66 if ($settingsKey == 'global_business_settings' && empty($settings['accepted_file_types'])) {
49 67 $settings['accepted_file_types'] = [];
50 68 }
51 69
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 +
52 90 if ($settingsKey == 'global_business_settings' && !empty($settings['agent_time_tracking'])) {
53 91 if (class_exists(TimeTrackMigrator::class) && $settings['agent_time_tracking'] === 'yes') {
54 92 TimeTrackMigrator::migrate();
55 93 }
@@ -54,9 +92,15 @@
54 92 TimeTrackMigrator::migrate();
55 93 }
56 94 }
57 95
58 - return Helper::updateOption($settingsKey, $settings);
96 + if ($settingsKey == NotificationSettings::OPTION_KEY) {
97 + return (new NotificationSettings())->save($settings);
98 + }
99 +
100 + $result = Helper::updateOption($settingsKey, $settings);
101 +
102 + return $result;
59 103 }
60 104
61 105 /**
62 106 * globalBusinessSettings method will fetch global settings from database, parse and return
@@ -72,8 +116,11 @@
72 116 }
73 117
74 118 $defaults = [
75 119 'portal_page_id' => '',
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
76 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>'),
77 124 'disable_public_ticket' => 'no',
78 125 'accepted_file_types' => ['images', 'csv', 'documents', 'zip', 'json'],
79 126 'max_file_size' => 2,
@@ -79,11 +126,17 @@
79 126 'max_file_size' => 2,
80 127 'max_file_upload' => 3,
81 128 'del_files_on_close' => 'no',
82 129 'enable_admin_bar_summary' => 'no',
130 + 'internal_notifications_enabled' => 'no',
83 131 'enable_draft_mode' => 'no',
84 132 'agent_feedback_rating' => 'no',
85 - 'keyboard_shortcuts' => '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',
86 139 ];
87 140
88 141 //Get default/existing settings from database using the key global_business_settings
89 142 $existingSettings = Helper::getOption('global_business_settings', []);
@@ -93,13 +146,44 @@
93 146 return $settings;
94 147 }
95 148
96 149 $settings = wp_parse_args($existingSettings, $defaults);
150 + $settings['ticket_link_portal'] = $settings['ticket_link_portal'] ?: 'default';
97 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 +
98 158 return $settings;
99 159 }
100 160
161 + private function resolveLegacyTicketLinkPortal($settings)
162 + {
163 + $selectedPortal = Arr::get($settings, 'ticket_link_portal', 'default');
101 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 +
102 186 /**
103 187 * getGlobalBusinessSettingsFields method will prepare the list of field, and it's property that will be used in global settings form
104 188 * @return array
105 189 */
@@ -104,8 +188,9 @@
104 188 * @return array
105 189 */
106 190 private function getGlobalBusinessSettingsFields()
107 191 {
192 + $nextSerialNumber = \FluentSupport\App\Models\Ticket::getNextSerialNumber();
108 193
109 194 $mimeGroups = Helper::getMimeGroups();
110 195
111 196 $formattedMimeGroups = [];
@@ -113,33 +198,56 @@
113 198 foreach ($mimeGroups as $mimeGroup => $mime) {
114 199 $formattedMimeGroups[$mimeGroup] = $mime['title'];
115 200 }
116 201
117 - $customRegistrationFormOptions = array(
118 - 'address_line_1' => 'Address Line 1',
119 - 'address_line_2' => 'Address Line 2',
120 - 'city' => 'City',
121 - 'zip' => 'Zip Code',
122 - 'state' => 'State',
123 - 'country' => 'Country',
124 - );
202 + $customRegistrationFormOptions = Helper::getCustomRegistrationFieldOptions();
125 203
126 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 + ],
127 225 'portal_page_id' => [
128 226 'type' => 'input-options',
129 - 'label' => __('Portal Page', 'fluent-support'),
227 + 'label' => __('Select a Page', 'fluent-support'),
228 + 'wrapper_class' => 'fs_portal_page_selector',
130 229 'show_id' => true,
131 230 'placeholder' => __('Select Portal Page', 'fluent-support'),
132 231 'options' => Helper::getWPPages(),//Get list of published pages
133 - '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')
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 + ],
134 240 ],
135 241 'login_message' => [
136 242 'type' => 'wp-editor',
137 243 'label' => __('Message for non logged in users', 'fluent-support'),
138 - '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>', 'fluent-support')
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')
139 245 ],
140 246 'disable_public_ticket' => [
247 + 'wrapper_class' => '',
141 248 'type' => 'inline-checkbox',
249 + 'label' => __('Public Ticket Interaction', 'fluent-support'),
142 250 'true_label' => 'yes',
143 251 'false-label' => 'no',
144 252 'checkbox_label' => __('Disable Public Ticket interaction', 'fluent-support'),
145 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')
@@ -144,26 +252,27 @@
144 252 'checkbox_label' => __('Disable Public Ticket interaction', 'fluent-support'),
145 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')
146 254 ],
147 255 'accepted_file_types' => [
148 - 'wrapper_class' => 'fs_half_field',
256 + 'wrapper_class' => '',
149 257 'type' => 'checkbox-group',
150 258 'label' => __('Accepted File Types', 'fluent-support'),
151 259 'options' => $formattedMimeGroups
152 260 ],
153 261 'max_file_size' => [
154 - 'wrapper_class' => 'fs_half_field',
262 + 'wrapper_class' => 'fs_settings_half_field',
155 263 'type' => 'input-text',
156 264 'data_type' => 'number',
157 265 'label' => __('Max File Size (in MegaByte)', 'fluent-support'),
158 266 ],
159 267 'max_file_upload' => [
160 - 'wrapper_class' => 'fs_half_field',
268 + 'wrapper_class' => 'fs_settings_half_field',
161 269 'type' => 'input-text',
162 270 'data_type' => 'number',
163 271 'label' => __('Maximum File Upload', 'fluent-support'),
164 272 ],
165 273 'del_files_on_close' => [
274 + 'wrapper_class' => '',
166 275 'type' => 'inline-checkbox',
167 276 'true_label' => 'yes',
168 277 'false-label' => 'no',
169 278 'checkbox_label' => __('Delete all attachments on ticket close', 'fluent-support'),
@@ -169,8 +278,9 @@
169 278 'checkbox_label' => __('Delete all attachments on ticket close', 'fluent-support'),
170 279 'inline_help' => __('If you enable this feature, all attachments associated with a ticket will be deleted when the ticket is closed.', 'fluent-support')
171 280 ],
172 281 'enable_admin_bar_summary' => [
282 + 'wrapper_class' => '',
173 283 'type' => 'inline-checkbox',
174 284 'true_label' => 'yes',
175 285 'false-label' => 'no',
176 286 'checkbox_label' => __('Enable Fluent Summary In Admin Bar', 'fluent-support'),
@@ -175,9 +285,22 @@
175 285 'false-label' => 'no',
176 286 'checkbox_label' => __('Enable Fluent Summary In Admin Bar', 'fluent-support'),
177 287 'inline_help' => __('If you enable this, logged in user can see the ticket summary from top nav bar.', 'fluent-support')
178 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 + ],
179 301 'enable_draft_mode' => [
302 + 'wrapper_class' => '',
180 303 'type' => 'inline-checkbox',
181 304 'true_label' => 'yes',
182 305 'false-label' => 'no',
183 306 'checkbox_label' => __('Enable Draft Mode', 'fluent-support'),
@@ -183,14 +306,15 @@
183 306 'checkbox_label' => __('Enable Draft Mode', 'fluent-support'),
184 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')
185 308 ],
186 309 'custom_registration_form_field' => [
187 - 'wrapper_class' => 'inline-checkbox',
310 + 'wrapper_class' => '',
188 311 'type' => 'checkbox-group',
189 312 'label' => __('Custom Registration Form Field', 'fluent-support'),
190 313 'options' => $customRegistrationFormOptions
191 314 ],
192 315 'enable_two_fa' => [
316 + 'wrapper_class' => '',
193 317 'type' => 'inline-checkbox',
194 318 'true_label' => 'yes',
195 319 'false-label' => 'no',
196 320 'checkbox_label' => __('Enable Two-Factor Authentication', 'fluent-support'),
@@ -196,18 +320,79 @@
196 320 'checkbox_label' => __('Enable Two-Factor Authentication', 'fluent-support'),
197 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')
198 322 ],
199 323 'keyboard_shortcuts' => [
324 + 'wrapper_class' => '',
200 325 'type' => 'inline-checkbox',
201 326 'true_label' => 'yes',
202 327 'false-label' => 'no',
203 328 'checkbox_label' => __('Enable Keyboard Shortcuts', 'fluent-support'),
204 - 'inline_help' => __("If you enable this, agents can use keyboard shortcuts for faster actions.", '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'),
205 379 ]
206 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 + }
207 391
208 392 if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION')) {
209 393 $fields['agent_feedback_rating'] = [
394 + 'wrapper_class' => '',
210 395 'type' => 'inline-checkbox',
211 396 'true_label' => 'yes',
212 397 'false-label' => 'no',
213 398 'checkbox_label' => __('Agent Feedback Rating', 'fluent-support'),
@@ -214,8 +399,9 @@
214 399 'inline_help' => __("If you enable this setting, users will have the option to provide feedback on an agent's response.", 'fluent-support')
215 400 ];
216 401
217 402 $fields['agent_time_tracking'] = [
403 + 'wrapper_class' => '',
218 404 'type' => 'inline-checkbox',
219 405 'true_label' => 'yes',
220 406 'false-label' => 'no',
221 407 'checkbox_label' => __('Agent Time Tracking', 'fluent-support'),
@@ -221,22 +407,166 @@
221 407 'checkbox_label' => __('Agent Time Tracking', 'fluent-support'),
222 408 'inline_help' => __("If you enable this setting, the agent can specify the amount of time needed to complete a ticket.", 'fluent-support')
223 409 ];
224 410
225 - if (defined('FLUENTCART_VERSION')) {
226 - $fields['enable_fc_menu'] = [
227 - 'type' => 'inline-checkbox',
228 - 'checkbox_label' => 'Add support link to Fluent Cart account navigation',
229 - 'inline_help' => __("If you enable this setting, support link will be added to Fluent Cart account navigation.", 'fluent-support'),
230 - 'true_label' => 'yes',
231 - 'false_label' => 'no'
232 - ];
411 + }
412 +
413 + if (Helper::getAuthProvider() === 'fluent_auth') {
414 + $fields = $this->replaceTwoFaWithFluentAuthNotice($fields);
415 + }
416 +
417 + return $fields;
418 + }
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;
233 446 }
447 +
448 + $replaced[$key] = $field;
234 449 }
235 450
236 - return $fields;
451 + return $replaced;
237 452 }
238 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 +
239 569 public function saveBoxEmailSettings($box, $emailKey, $settings)
240 570 {
241 571 return $box->saveMeta('_email_' . $emailKey, $settings);
242 572 }
@@ -264,9 +594,9 @@
264 594 'ticket_created_email_to_customer' => [
265 595 'key' => 'ticket_created_email_to_customer',
266 596 'title' => __('Ticket Created (To Customer)', 'fluent-support'),
267 597 'description' => __('This email will be sent when a customer submit a support ticket', 'fluent-support'),
268 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
598 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
269 599 'default_status' => 'no',
270 600 'send_attachments'=> 'no'
271 601 ],
272 602 'ticket_replied_by_agent_email_to_customer' => [
@@ -272,9 +602,9 @@
272 602 'ticket_replied_by_agent_email_to_customer' => [
273 603 'key' => 'ticket_replied_by_agent_email_to_customer',
274 604 'title' => __('Replied by Agent (To Customer)', 'fluent-support'),
275 605 'description' => __('This email will be sent when an agent reply to a ticket', 'fluent-support'),
276 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
606 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
277 607 'default_status' => 'yes',
278 608 'send_attachments'=> 'no'
279 609 ],
280 610 'ticket_closed_by_agent_email_to_customer' => [
@@ -280,9 +610,9 @@
280 610 'ticket_closed_by_agent_email_to_customer' => [
281 611 'key' => 'ticket_closed_by_agent_email_to_customer',
282 612 'title' => __('Ticket Closed by Agent (To Customer)', 'fluent-support'),
283 613 'description' => __('This email will be sent when an agent close a ticket', 'fluent-support'),
284 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
614 + 'email_subject' => 'Re: {{ticket.title}} #{{ticket.public_id}}',
285 615 'default_status' => 'no',
286 616 'send_attachments'=> 'no'
287 617 ],
288 618 'ticket_created_email_to_admin' => [
@@ -288,9 +618,9 @@
288 618 'ticket_created_email_to_admin' => [
289 619 'key' => 'ticket_created_email_to_admin',
290 620 'title' => __('Ticket Created (To Admin)', 'fluent-support'),
291 621 'description' => __('This email will be sent when the business when a new ticket has been submitted', 'fluent-support'),
292 - 'email_subject' => 'New Ticket: {{ticket.title}} #{{ticket.id}}',
622 + 'email_subject' => 'New Ticket: {{ticket.title}} #{{ticket.public_id}}',
293 623 'default_status' => 'yes',
294 624 'send_attachments'=> 'no'
295 625 ],
296 626 'ticket_replied_by_customer_email_to_admin' => [
@@ -296,9 +626,9 @@
296 626 'ticket_replied_by_customer_email_to_admin' => [
297 627 'key' => 'ticket_replied_by_customer_email_to_admin',
298 628 'title' => __('Replied by Customer (To Agent/Admin)', 'fluent-support'),
299 629 'description' => __('This email will be sent to Assigned Agent or Admin when a customer reply to a ticket', 'fluent-support'),
300 - 'email_subject' => 'New Response: {{ticket.title}} #{{ticket.id}}',
630 + 'email_subject' => 'New Response: {{ticket.title}} #{{ticket.public_id}}',
301 631 'default_status' => 'yes',
302 632 'send_attachments'=> 'no'
303 633 ],
304 634 'ticket_agent_on_change' => [
@@ -304,19 +634,28 @@
304 634 'ticket_agent_on_change' => [
305 635 'key' => 'ticket_agent_on_change',
306 636 'title' => __('Ticket Agent Change (To Agent)', 'fluent-support'),
307 637 'description' => __('This email will be sent to newly assigned agent', 'fluent-support'),
308 - 'email_subject' => 'Ticket Agent Change: {{ticket.title}} #{{ticket.id}}',
638 + 'email_subject' => 'Ticket Agent Change: {{ticket.title}} #{{ticket.public_id}}',
309 639 'default_status' => 'yes',
310 640 'send_attachments'=> 'no'
311 641 ],
312 642 'ticket_created_by_agent_email_to_customer' => [
313 643 'key' => 'ticket_created_by_agent_email_to_customer',
314 - 'title' => __('Ticket Created by Agent (To Customer)', 'fluent-support'),
315 - 'description' => __('This email will be sent when an agent create a ticket for a customer', 'fluent-support'),
316 - 'email_subject' => 'Re: {{ticket.title}} #{{ticket.id}}',
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}}',
317 647 'default_status' => 'no',
318 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'
319 658 ]
320 659 ];
321 660
322 661 if (!isset($settingsDefaults[$emailKey])) {
@@ -382,32 +721,39 @@
382 721 private function getDefaultEmailBody($emailKey, $type = 'web')
383 722 {
384 723 if ($emailKey == 'ticket_created_email_to_customer') {
385 724 if ($type == 'web') {
386 - 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>';
387 726 } else {
388 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>';
389 728 }
390 729 } else if ($emailKey == 'ticket_replied_by_agent_email_to_customer') {
391 730 if ($type == 'web') {
392 - 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>';
393 732 } else {
394 - 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}}';
395 735 }
396 736 } else if ($emailKey == 'ticket_closed_by_agent_email_to_customer') {
397 737 if ($type == 'web') {
398 - 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>';
399 739 } else {
400 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>';
401 741 }
402 742 } else if ($emailKey == 'ticket_created_email_to_admin') {
403 - 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>';
404 744 } else if ($emailKey == 'ticket_replied_by_customer_email_to_admin') {
405 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>';
406 746 } else if($emailKey == 'ticket_agent_on_change') {
407 - 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>';
408 748 } else if($emailKey == 'ticket_created_by_agent_email_to_customer') {
409 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}}';
410 756 }
411 757
412 758 return '';
413 759 }