PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / site / resources / chat / session / admin.js

admin.js in VikBooking Hotel Booking Engine & PMS trunk, at site/resources/chat/session/admin.js

570 lines 19.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function($, w) {
2 'use strict';
3
4 const updateMetadata = (chat, key, val) => {
5 return new Promise((resolve, reject) => {
6 VBOChatAjax.do(
7 chat.data.environment.url,
8 {
9 task: 'chat.update_metadata',
10 id_context: chat.data.environment.context.id,
11 context: chat.data.environment.context.alias,
12 key: key,
13 val: val,
14 },
15 (resp) => {
16 resolve();
17 },
18 (error) => {
19 reject(error);
20 }
21 );
22 });
23 }
24
25 const summarizeConversation = (chat) => {
26 return new Promise((resolve, reject) => {
27 // convert messages into an acceptable format
28 const messages = chat.data.environment.messages.map((msg) => {
29 return {
30 role: msg.id_sender == -1 ? 'user' : 'assistant',
31 content: msg.message,
32 name: msg.sender_name,
33 };
34 });
35
36 VBOChatAjax.do(
37 chat.data.environment.url,
38 {
39 task: 'ai.summarize',
40 messages: messages.reverse(),
41 },
42 (resp) => {
43 resolve(resp.result);
44 },
45 (error) => {
46 reject(error);
47 }
48 );
49 });
50 }
51
52 const sendMessageTemplate = (chat, configId) => {
53 return new Promise((resolve, reject) => {
54 // make the request
55 VBOCore.doAjax(
56 chat.data.environment.url,
57 {
58 task: 'chat.send_template',
59 session_id: chat.data.environment.context.id,
60 config_id: configId,
61 },
62 (resp) => {
63 resolve(resp);
64 },
65 (error) => {
66 reject(error);
67 }
68 );
69 });
70 }
71
72 const showContextToast = (chat, panel) => {
73 // identify the target that should hold the info panel
74 const targetEl = $(chat.data.element.conversation).closest('.chat-messages-panel');
75
76 let toast = targetEl.find('.chat-session-toast');
77
78 if (!toast.length) {
79 // create toast template
80 toast = $('<div class="chat-session-toast">\n'+
81 ' <div class="chat-session-toast-message">\n'+
82 ' <div class="chat-session-toast-message-content"></div>\n'+
83 ' </div>\n'+
84 '</div>');
85
86 // append toast HTML to target only once
87 targetEl.append(toast);
88 }
89
90 // update toast panel content
91 toast.find('.chat-session-toast-message-content').html(panel);
92
93 let dismissHandler = toast.find('[data-role="popup.dismiss"]');
94
95 // disable click event
96 toast.off('click');
97 $(document).off('click', '.chat-session-toast [data-role="popup.dismiss"]');
98
99 if (!dismissHandler.length) {
100 // dismiss when clicking inside the toast
101 toast.css('cursor', 'pointer').on('click', () => {
102 toast.removeClass('slide-in');
103 });
104 }
105
106 // dispose when clicked observer element
107 $(document).on('click', '.chat-session-toast [data-role="popup.dismiss"]', () => {
108 toast.removeClass('slide-in');
109 });
110
111 setTimeout(() => {
112 toast.addClass('slide-in');
113 }, 256);
114
115 return toast;
116 }
117
118 const calcNights = (checkin, checkout) => {
119 const start = new Date(checkin + 'T00:00:00Z');
120 const end = new Date(checkout + 'T00:00:00Z');
121
122 return Math.floor((end - start) / (1000 * 60 * 60 * 24));
123 }
124
125 /*********************
126 * BAN/UNBAN SESSION *
127 *********************/
128
129 /**
130 * Ban/unban session button text handler.
131 */
132 $(w).on('chat.session.ban.text', async (event) => {
133 const [root, parentEvent, button, chat] = event.args;
134
135 event.displayText = Joomla.JText._(chat.data.environment.context.metadata.banned ? 'VBO_CHAT_UNBAN_SESSION' : 'VBO_CHAT_BAN_SESSION');
136 });
137
138 /**
139 * Ban/unban session button icon handler.
140 */
141 $(w).on('chat.session.ban.icon', async (event) => {
142 const [root, parentEvent, button, chat] = event.args;
143
144 event.displayIcon = 'fas fa-' + (chat.data.environment.context.metadata.banned ? 'check-circle' : 'ban');
145 });
146
147 /**
148 * Ban/unban session button action handler.
149 */
150 $(w).on('chat.session.ban.action', async (event) => {
151 const [root, parentEvent, button, chat] = event.args;
152
153 const context = chat.data.environment.context;
154
155 try {
156 await updateMetadata(chat, 'banned', context.metadata.banned ? 0 : 1);
157 context.metadata.banned = !context.metadata.banned;
158
159 $(chat.data.element.conversation).parent().css('background', context.metadata.banned ? '#d003' : 'inherit');
160 } catch (error) {
161 console.error(error);
162
163 alert(error.responseText || error.statusText || 'Connection lost!');
164 }
165 });
166
167 /**************************
168 * STOP/RESUME AI REPLIES *
169 **************************/
170
171 /**
172 * Stop/resume AI session button text handler.
173 */
174 $(w).on('chat.session.ai.autoreply.text', async (event) => {
175 const [root, parentEvent, button, chat] = event.args;
176
177 event.displayText = Joomla.JText._(chat.data.environment.context.metadata.use_ai ? 'VBO_CHAT_STOP_AI_SESSION' : 'VBO_CHAT_RESUME_AI_SESSION');
178 });
179
180 /**
181 * Stop/resume AI session button icon handler.
182 */
183 $(w).on('chat.session.ai.autoreply.icon', async (event) => {
184 const [root, parentEvent, button, chat] = event.args;
185
186 event.displayIcon = 'fas fa-' + (chat.data.environment.context.metadata.use_ai ? 'comment-slash' : 'comment');
187 });
188
189 /**
190 * Stop/resume AI session button action handler.
191 */
192 $(w).on('chat.session.ai.autoreply.action', async (event) => {
193 const [root, parentEvent, button, chat] = event.args;
194
195 const context = chat.data.environment.context;
196
197 try {
198 await updateMetadata(chat, 'use_ai', context.metadata.use_ai ? 0 : 1);
199 context.metadata.use_ai = !context.metadata.use_ai;
200 } catch (error) {
201 console.error(error);
202
203 alert(error.responseText || error.statusText || 'Connection lost!');
204 }
205 });
206
207 /**************************
208 * SUMMARIZE CONVERSATION *
209 **************************/
210
211 /**
212 * AI summarize conversation button disabled status handler.
213 */
214 $(w).on('chat.session.ai.summarize.disabled', async (event) => {
215 const [root, parentEvent, button, chat] = event.args;
216
217 // disable in case the popup is already visible
218 event.shouldDisable = $(chat.data.element.conversation)
219 .closest('.chat-messages-panel')
220 .find('.chat-session-toast')
221 .hasClass('slide-in');
222 });
223
224 /**
225 * AI summarize conversation button action handler.
226 */
227 $(w).on('chat.session.ai.summarize.action', async (event) => {
228 const [root, parentEvent, button, chat] = event.args;
229
230 const popup = $('<div class="chat-ai-summary"></div>');
231
232 const displaySummary = (summary) => {
233 popup.html('');
234
235 popup.append(
236 $('<div class="query-summary-head"></div>')
237 .append($('<span></span>').text(Joomla.JText._('VBO_CHAT_SUMMARIZE_TITLE')))
238 .append('<a href="javascript:void(0)" data-role="popup.dismiss"><i class="fas fa-times"></i></a>')
239 );
240
241 popup.append($('<div class="ai-summary-area"></div>').html(summary));
242 }
243
244 // use cached value (if available)
245 if (chat.data.environment.context.aiSummary) {
246 displaySummary(chat.data.environment.context.aiSummary);
247 showContextToast(chat, popup);
248 return;
249 }
250
251 popup.html('<div class="chat-loading"><i class="fas fa-spinner fa-spin fa-3x"></i></div>');
252 const toast = showContextToast(chat, popup);
253
254 // wait until the toast is fully visible
255 setTimeout(async () => {
256 try {
257 if (!button.supported) {
258 // VCM 1.9.19 required to support AI summarize service
259 throw new Error("Update VikChannelManager to the latest version first.");
260 }
261
262 const summary = await summarizeConversation(chat);
263
264 // internally cache result to prevent duplicate requests
265 chat.data.environment.context.aiSummary = summary;
266
267 displaySummary(summary);
268
269 // disable dismiss on toast click
270 toast.off('click').css('cursor', 'default');
271 } catch (error) {
272 console.error(error);
273
274 if (error instanceof Error) {
275 error = {responseText: error};
276 }
277
278 popup.addClass('error-response').text(error.responseText || error.statusText || 'Connection lost!');
279 }
280 }, 600);
281 });
282
283 /***********************
284 * SESSION QUOTE QUERY *
285 ***********************/
286
287 /**
288 * Quote session query button visibility handler.
289 */
290 $(w).on('chat.session.query.quote.visible', async (event) => {
291 const [root, parentEvent, button, chat] = event.args;
292
293 event.shouldDisplay = chat.data.environment.context.metadata?.query?.checkin
294 && chat.data.environment.context.metadata?.query?.checkout;
295 });
296
297 /**
298 * Quote session query button action handler.
299 */
300 $(w).on('chat.session.query.quote.action', async (event) => {
301 const [root, parentEvent, button, chat] = event.args;
302
303 // open quotation maker on a blank page
304 window.open(button.url, '_blank');
305 });
306
307 /*********************
308 * SESSION SEE QUERY *
309 *********************/
310
311 /**
312 * See session query button visibility handler.
313 */
314 $(w).on('chat.session.query.see.visible', async (event) => {
315 const [root, parentEvent, button, chat] = event.args;
316
317 event.shouldDisplay = chat.data.environment.context.metadata?.query?.checkin
318 && chat.data.environment.context.metadata?.query?.checkout;
319 });
320
321 /**
322 * See session query button disabled status handler.
323 */
324 $(w).on('chat.session.query.see.disabled', async (event) => {
325 const [root, parentEvent, button, chat] = event.args;
326
327 // disable in case the popup is already visible
328 event.shouldDisable = $(chat.data.element.conversation)
329 .closest('.chat-messages-panel')
330 .find('.chat-session-toast')
331 .hasClass('slide-in');
332 });
333
334 /**
335 * See session query button action handler.
336 */
337 $(w).on('chat.session.query.see.action', async (event) => {
338 const [root, parentEvent, button, chat] = event.args;
339
340 const query = chat.data.environment.context.metadata.query;
341
342 const popup = $('<div class="chat-query-summary"></div>');
343
344 popup.append(
345 $('<div class="query-summary-head"></div>')
346 .append($('<span></span>').text(Joomla.JText._('VBO_CHAT_QUERY_SUMMARY_TITLE')))
347 .append('<a href="javascript:void(0)" data-role="popup.dismiss"><i class="fas fa-times"></i></a>')
348 );
349
350 //////////////////////////////////////////////////
351
352 const body = $('<div class="query-summary-body"></div>');
353
354 //////////////////////////////////////////////////
355
356 const datesBox = $('<div class="query-dates-box"></div>');
357
358 datesBox.append(
359 $('<div class="query-dates-title"></div>')
360 .append('<i class="fas fa-calendar"></i>')
361 .append($('<span></span>').text(Joomla.JText._('VBO_CONDTEXT_RULE_STAYDATES')))
362 );
363
364 datesBox.append(
365 $('<div class="query-dates-main"></div>')
366 .append($('<span></span>').text(query.checkin))
367 .append('<i class="fas fa-arrow-right"></i>')
368 .append($('<span></span>').text(query.checkout))
369 );
370
371 const nights = calcNights(query.checkin, query.checkout);
372 datesBox.append(
373 $('<div class="query-dates-sub"></div>').text(
374 Joomla.JText._(nights > 1 ? 'VBOSEASONCALNUMNIGHTS' : 'VBOSEASONCALNUMNIGHT').replace(/%d/, nights).toLowerCase()
375 )
376 );
377
378 body.append(datesBox);
379
380 //////////////////////////////////////////////////
381
382 const guestsBox = $('<div class="query-guests-box"></div>');
383
384 guestsBox.append(
385 $('<div class="query-guests-title"></div>')
386 .append('<i class="fas fa-users"></i>')
387 .append($('<span></span>').text(Joomla.JText._('VBO_NOTIFS_GROUP_GUESTS')))
388 );
389
390 guestsBox.append('<div class="query-guests-main"></div>');
391 query.parties.forEach((party, index) => {
392 const guestDetails = $('<strong></strong>');
393
394 let components = [
395 Joomla.JText._(party.adults > 1 ? 'VBO_N_ADULTS' : 'VBO_N_ADULTS_1').replace(/%d/, party.adults)
396 ];
397
398 if (party?.children) {
399 components.push(Joomla.JText._(party.children > 1 ? 'VBO_N_CHILDREN' : 'VBO_N_CHILDREN_1').replace(/%d/, party.children));
400 }
401
402 guestDetails.text(components.join(', '));
403
404 let guestBox = $('<div class="query-quest-info"></div>');
405 guestBox.append($('<div class="query-quest-info-left"></div>').text(Joomla.JText._('VBMAILROOMNUM') + (index + 1)));
406 guestBox.append($('<div class="query-quest-info-right"></div>').append(guestDetails));
407
408 if (party?.pets) {
409 guestBox.find('.query-quest-info-right').append('<br>').append(
410 $('<small></small>').text((party.pets + ' ' + Joomla.JText._(party.pets > 1 ? 'VBO_PETS' : 'VBO_PET').toLowerCase()))
411 );
412 }
413
414 guestsBox.find('.query-guests-main').append(guestBox);
415 });
416
417 body.append(guestsBox);
418
419 //////////////////////////////////////////////////
420
421 if ((query?.rooms || []).length) {
422 const roomsBox = $('<div class="query-rooms-box"></div>');
423
424 roomsBox.append(
425 $('<div class="query-rooms-title"></div>')
426 .append('<i class="fas fa-bed"></i>')
427 .append($('<span></span>').text(Joomla.JText._('VBO_CHAT_QUERY_SUMMARY_PREF_ROOMS')))
428 );
429
430 roomsBox.append($('<div class="query-rooms-main"></div>'));
431
432 query.rooms.forEach((room) => {
433 roomsBox.find('.query-rooms-main').append(
434 $('<span class="badge badge-info"></span>').text(room.name)
435 );
436 });
437
438
439 body.append(roomsBox);
440 }
441
442 //////////////////////////////////////////////////
443
444 popup.append(body);
445
446 showContextToast(chat, popup);
447 });
448
449 /**************************
450 * SEND WHATSAPP TEMPLATE *
451 **************************/
452
453 /**
454 * See session query button action handler.
455 */
456 $(w).on('chat.session.messaging.sendtmpl.action', async (event) => {
457 const [root, parentEvent, button, chat] = event.args;
458
459 const popup = $('<div class="chat-query-summary"></div>');
460
461 popup.append(
462 $('<div class="query-summary-head"></div>')
463 .append($('<span></span>').text(Joomla.JText._('VBO_MESSAGE_TEMPLATE')))
464 .append('<a href="javascript:void(0)" data-role="popup.dismiss"><i class="fas fa-times"></i></a>')
465 );
466
467 //////////////////////////////////////////////////
468
469 const body = $('<div class="query-summary-body"></div>');
470
471 body.append('<div class="messaging-tmpl-preview" style="display: none;"></div>');
472 body.append('<div class="messaging-tmpl-select"></div>');
473
474 //////////////////////////////////////////////////
475
476 const tmplSelect = $('<select></select>');
477
478 // add placeholder option
479 tmplSelect.append(
480 $('<option value=""></option>').text(Joomla.JText._('JGLOBAL_SELECT_AN_OPTION'))
481 );
482
483 // build template select options
484 (button.templates || []).forEach((tmpl) => {
485 tmplSelect.append(
486 $('<option></option>').text(`${tmpl.name} (${tmpl.lang})`).val(tmpl.identifier)
487 );
488 });
489
490 // append dropdown to popup body
491 body.find('.messaging-tmpl-select').append(tmplSelect);
492
493 // build button to send the template
494 const sendButton = $('<button type="button" class="btn btn-success" style="display: none;"><i class="fas fa-paper-plane no-margin"></i></button>');
495
496 // append send button to popup body
497 body.find('.messaging-tmpl-select').append(sendButton);
498
499 //////////////////////////////////////////////////
500
501 popup.append(body);
502
503 const toast = showContextToast(chat, popup);
504
505 // handle template change event
506 tmplSelect.on('change', (event) => {
507 // find selected template
508 const tmpl = button.templates.find(tmpl => tmpl.identifier === tmplSelect.val());
509
510 if (tmpl) {
511 // display template preview
512 body.find('.messaging-tmpl-preview').html(tmpl.preview_html).show();
513 sendButton.show();
514 } else {
515 // hide template preview
516 body.find('.messaging-tmpl-preview').hide().html('');
517 sendButton.hide();
518 }
519 });
520
521 // handle template send event
522 sendButton.on('click', async (event) => {
523 sendButton.prop('disabled', true).find('i').attr('class', 'fas fa-spinner fa-spin no-margin');
524
525 // obtain selected template details
526 let tplIdentifierParts = tmplSelect.val().split(':');
527
528 try {
529 // send template
530 await sendMessageTemplate(chat, tplIdentifierParts[2]);
531
532 // dismiss toast
533 toast.find('[data-role="popup.dismiss"]').trigger('click');
534
535 // download new message
536 chat.synchronizeMessages();
537 } catch (error) {
538 console.error(error);
539
540 alert(error.responseText || error.statusText || 'Connection lost!');
541
542 // enable button again
543 sendButton.prop('disabled', false).find('i').attr('class', 'fas fa-paper-plane no-margin');
544 }
545 });
546 })
547
548 /***************
549 * CHAT EVENTS *
550 ***************/
551
552 /**
553 * Fires when a chat is prepared and ready to be used.
554 * Changes the background color depending on the "banned" status.
555 */
556 window.addEventListener('chat.prepare', (event) => {
557 const {chat} = event.detail;
558
559 if (chat.data.environment.user.id != 0 || chat.data.environment.context.alias !== 'session') {
560 // ignore if we are not visiting the chat as admin
561 return;
562 }
563
564 if (chat.data.environment.context.metadata.banned) {
565 // change background for banned sessions
566 $(chat.data.element.conversation).parent().css('background', '#d003');
567 }
568 });
569
570 })(jQuery, window);