| @@ -216,15 +216,64 @@ | ||
| 216 | 216 | $('#mxch-select-all').prop('checked', totalItems > 0 && checkedItems === totalItems); |
| 217 | 217 | $('#mxch-select-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems); |
| 218 | 218 | } |
| 219 | 219 | |
| 220 | - // Sort button | |
| 221 | - $('#mxch-sort-btn').on('click', function() { | |
| 222 | - currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc'; | |
| 223 | - $(this).find('svg').css('transform', currentSortOrder === 'asc' ? 'rotate(180deg)' : 'rotate(0deg)'); | |
| 224 | - loadChatList(currentPage, $('#mxch-search-transcripts').val()); | |
| 220 | + // Sort button — opens a small menu with 4 sort modes (plan-a5b006 adds rating sorts). | |
| 221 | + $('#mxch-sort-btn').on('click', function(e) { | |
| 222 | + e.stopPropagation(); | |
| 223 | + const $btn = $(this); | |
| 224 | + let $menu = $('#mxch-sort-menu'); | |
| 225 | + if (!$menu.length) { | |
| 226 | + $menu = $( | |
| 227 | + '<div id="mxch-sort-menu" class="mxch-sort-menu" role="menu">' + | |
| 228 | + ' <button type="button" class="mxch-sort-option" data-sort="desc" role="menuitem">Newest first</button>' + | |
| 229 | + ' <button type="button" class="mxch-sort-option" data-sort="asc" role="menuitem">Oldest first</button>' + | |
| 230 | + ' <button type="button" class="mxch-sort-option" data-sort="rating_positive" role="menuitem"><span class="mxch-sort-option-icon mxch-rating-thumb-up"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>Positive ratings first</button>' + | |
| 231 | + ' <button type="button" class="mxch-sort-option" data-sort="rating_negative" role="menuitem"><span class="mxch-sort-option-icon mxch-rating-thumb-down"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>Negative ratings first</button>' + | |
| 232 | + '</div>' | |
| 233 | + ); | |
| 234 | + $('body').append($menu); | |
| 235 | + $menu.on('click', '.mxch-sort-option', function() { | |
| 236 | + currentSortOrder = $(this).data('sort'); | |
| 237 | + $menu.find('.mxch-sort-option').removeClass('active'); | |
| 238 | + $(this).addClass('active'); | |
| 239 | + $menu.hide(); | |
| 240 | + loadChatList(1, $('#mxch-search-transcripts').val()); | |
| 241 | + }); | |
| 242 | + $(document).on('click.mxchSortMenu', function(ev) { | |
| 243 | + if (!$(ev.target).closest('#mxch-sort-menu, #mxch-sort-btn').length) { | |
| 244 | + $menu.hide(); | |
| 245 | + } | |
| 246 | + }); | |
| 247 | + } | |
| 248 | + $menu.find('.mxch-sort-option').removeClass('active'); | |
| 249 | + $menu.find('[data-sort="' + currentSortOrder + '"]').addClass('active'); | |
| 250 | + const offset = $btn.offset(); | |
| 251 | + const btnHeight = $btn.outerHeight(); | |
| 252 | + $menu.css({ | |
| 253 | + position: 'absolute', | |
| 254 | + top: (offset.top + btnHeight + 4) + 'px', | |
| 255 | + left: offset.left + 'px' | |
| 256 | + }).toggle(); | |
| 225 | 257 | }); |
| 226 | 258 | |
| 259 | + // Render a rating badge for a session row. Uses inline SVG so the glyph | |
| 260 | + // renders the same across OSes (emoji fonts vary). Tooltip surfaces the | |
| 261 | + // optional feedback text. | |
| 262 | + function renderRatingBadge(session) { | |
| 263 | + const value = (session && typeof session.rating_value === 'number') ? session.rating_value : null; | |
| 264 | + const feedback = (session && session.rating_feedback) ? session.rating_feedback : ''; | |
| 265 | + if (value === 1) { | |
| 266 | + const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat positively'; | |
| 267 | + return '<span class="mxch-chat-rating mxch-chat-rating-up" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>'; | |
| 268 | + } | |
| 269 | + if (value === -1) { | |
| 270 | + const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat negatively'; | |
| 271 | + return '<span class="mxch-chat-rating mxch-chat-rating-down" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>'; | |
| 272 | + } | |
| 273 | + return '<span class="mxch-chat-rating mxch-chat-rating-empty" title="No rating yet" aria-label="No rating yet">—</span>'; | |
| 274 | + } | |
| 275 | + | |
| 227 | 276 | // Delete selected button — opens the shared confirm modal with the "also delete lead" checkbox. |
| 228 | 277 | $('#mxch-delete-selected').on('click', function() { |
| 229 | 278 | const count = selectedSessions.size; |
| 230 | 279 | if (count === 0) return; |
| @@ -366,8 +415,9 @@ | ||
| 366 | 415 | <div class="mxch-chat-name">${escapeHtml(session.display_name)}</div> |
| 367 | 416 | <div class="mxch-chat-preview">${escapeHtml(session.preview)}</div> |
| 368 | 417 | </div> |
| 369 | 418 | <div class="mxch-chat-meta"> |
| 419 | + ${renderRatingBadge(session)} | |
| 370 | 420 | <span class="mxch-chat-time">${escapeHtml(session.time_display)}</span> |
| 371 | 421 | <span class="mxch-chat-count">${session.message_count}</span> |
| 372 | 422 | </div> |
| 373 | 423 | </div> |
| @@ -518,8 +568,18 @@ | ||
| 518 | 568 | $('#mxch-detail-email').text(data.user.email); |
| 519 | 569 | $('#mxch-detail-email-row').show(); |
| 520 | 570 | } else { |
| 521 | 571 | $('#mxch-detail-email-row').hide(); |
| 572 | + } | |
| 573 | + | |
| 574 | + // Feedback row — .text() (not .html()) for XSS-safe display of user-submitted text. | |
| 575 | + // rating_feedback is already sanitize_text_field()'d + mb_substr(200) on save. | |
| 576 | + var feedback = (data && data.rating_feedback) ? String(data.rating_feedback).trim() : ''; | |
| 577 | + if (feedback) { | |
| 578 | + $('#mxch-detail-feedback').text(feedback); | |
| 579 | + $('#mxch-detail-feedback-row').show(); | |
| 580 | + } else { | |
| 581 | + $('#mxch-detail-feedback-row').hide(); | |
| 522 | 582 | } |
| 523 | 583 | |
| 524 | 584 | // Clicked links |
| 525 | 585 | if (data.clicked_urls && data.clicked_urls.length > 0) { |