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