PageSourceSearch

https://sabor.club/plugins/br.com.fabricadigital.plus.base/static/…/js/plus_evaluation.58e477a9d17c.js

js sabor.club collected 2026-10-04 01:27:37 UTC 31,411 bytes, 823 lines download raw bytes

1(function($) {
2  'use strict';
3
4  // --- Verificações de dependências ---
5  if (!window.Looper || !window.nunjucks || !window.Urls) {
6    console.warn("Dependências (Looper, Nunjucks, Urls) não encontradas. Evaluation não será executado.");
7    return;
8  }
9
10  // Flag de controle para evitar execuções simultâneas
11  var isProcessing = false;
12
13  // --- Constantes ---
14  var CONSTS = {
15    CONTAINER_SELECTOR: ".plus_rating_evaluation",
16    CRITERIA_BOX_CLASS: "plus_rating_evaluation_criteria",
17    REVIEW_BOX_CLASS: "plus_rating_evaluation_review",
18    MESSAGE_CLASS: "plus_rating_evaluation_message",
19    SUBMIT_BTN_CLASS: "plus_rating_evaluation_submit",
20    PROCESSED_ATTR: "data-processed",
21    MODEL_ATTR: "data-plus-rating-evaluation-model",
22    NODE_ID_ATTR: "data-plus-rating-evaluation-nodeid",
23    IS_AUTH_ATTR: "data-plus-rating-evaluation-is-authenticated",
24    API_KEY: "evaluation",
25    API_CONFIG_URL: "hidra-rest-api:plus_rating_evaluation_rest-config",
26    API_SUMMARY_LIGHT_URL: "hidra-rest-api:plus_rating_evaluation_rest-summary-light",
27    API_SUMMARY_URL: "hidra-rest-api:plus_rating_evaluation_rest-summary",
28    API_SUBMIT_URL: "hidra-rest-api:plus_rating_evaluation_rest-submit",
29    API_DELETE_SESSION_URL: "hidra-rest-api:plus_rating_evaluation_rest-delete-session",
30    API_TOGGLE_REVIEW_PUBLIC_URL: "hidra-rest-api:plus_rating_evaluation_rest-toggle-review-public",
31    API_EDIT_REVIEW_URL: "hidra-rest-api:plus_rating_evaluation_rest-edit-review",
32    API_EDIT_EVALUATION_URL: "hidra-rest-api:plus_rating_evaluation_rest-edit-evaluation",
33    TEMPLATE_LOADING: "plus_rating/nunjucks/plugins/evaluation/loading.njk",
34    TEMPLATE_BADGE: "plus_rating/nunjucks/plugins/evaluation/badge.njk",
35    TEMPLATE_INLINE: "plus_rating/nunjucks/plugins/evaluation/inline.njk",
36    VARIANT_ATTR: "data-plus-rating-evaluation-variant",
37    TEMPLATE_MODAL: "plus_rating/nunjucks/plugins/evaluation/modal.njk",
38    TEMPLATE_DONE: "plus_rating/nunjucks/plugins/evaluation/done.njk",
39    TEMPLATE_FAIL: "plus_rating/nunjucks/plugins/evaluation/fail.njk",
40    TEMPLATE_MSG_SUCCESS: "plus_rating/nunjucks/plugins/evaluation/message_success.njk",
41    TEMPLATE_MSG_ERROR: "plus_rating/nunjucks/plugins/evaluation/message_error.njk",
42    TEMPLATE_CONFIRM_MODAL: "plus_rating/nunjucks/plugins/evaluation/confirm_modal.njk",
43    TEMPLATE_MODAL_WRAPPER: "plus_rating/nunjucks/plugins/evaluation/modal_wrapper.njk",
44    MODAL_ID: "plus-rating-evaluation-modal",
45    CONFIRM_MODAL_ID: "plus-rating-evaluation-confirm-modal"
46  };
47
48  // --- Verificações de URLs ---
49  var requiredUrls = [
50    CONSTS.API_CONFIG_URL, CONSTS.API_SUMMARY_LIGHT_URL, CONSTS.API_SUMMARY_URL,
51    CONSTS.API_SUBMIT_URL, CONSTS.API_DELETE_SESSION_URL,
52    CONSTS.API_TOGGLE_REVIEW_PUBLIC_URL, CONSTS.API_EDIT_REVIEW_URL,
53    CONSTS.API_EDIT_EVALUATION_URL
54  ];
55  for (var i = 0; i < requiredUrls.length; i++) {
56    if (typeof Urls[requiredUrls[i]] !== 'function') {
57      console.warn("Url do evaluation nao encontrada: " + requiredUrls[i]);
58      return;
59    }
60  }
61
62  // --- Modal de confirmação ---
63  function ensureConfirmModal() {
64    if ($('#' + CONSTS.CONFIRM_MODAL_ID).length > 0) return;
65    $('body').append(nunjucks.render(CONSTS.TEMPLATE_CONFIRM_MODAL, {
66      modal_id: CONSTS.CONFIRM_MODAL_ID
67    }));
68  }
69
70  function showConfirmModal(title, body, onConfirm) {
71    ensureConfirmModal();
72    var $modal = $('#' + CONSTS.CONFIRM_MODAL_ID);
73    $modal.find('.modal-title').text(title);
74    $modal.find('.modal-body').text(body);
75    var $confirmBtn = $modal.find('.plus_rating_evaluation_confirm_btn');
76    $confirmBtn.off('click').on('click', function() {
77      $modal.modal('hide');
78      onConfirm();
79    });
80    $modal.modal({backdrop: 'static'});
81  }
82
83  // --- Modal principal de avaliação ---
84  function ensureEvaluationModal() {
85    if ($('#' + CONSTS.MODAL_ID).length > 0) return;
86    $('body').append(nunjucks.render(CONSTS.TEMPLATE_MODAL_WRAPPER, {
87      modal_id: CONSTS.MODAL_ID
88    }));
89  }
90
91  // --- Configuração padrão do rater ---
92  var raterDefaultOptions = {
93    step_size: 1,
94    max_value: 5,
95    cursor: "pointer",
96    initial_value: 0,
97    readonly: false,
98    change_once: false,
99    symbols: {
100      star: {
101        base: "<i class='fal fa-lg text-rating fa-rating'></i>",
102        hover: "<i class='fa fa-lg text-rating-hover fa-rating'></i>",
103        selected: "<i class='fa fa-lg text-rating-active fa-rating'></i>"
104      }
105    },
106    selected_symbol_type: "star"
107  };
108
109  // Cache de configs por content_type
110  var configCache = {};
111
112  /**
113   * Determina se o usuário pode avaliar (novo submit ou atualização).
114   * Anônimo nunca pode — backend rejeita mutação sem autenticação.
115   */
116  function canUserEvaluate(config, summary, isAuthenticated) {
117    if (!isAuthenticated) return false;
118    if (!summary.user_scores || Object.keys(summary.user_scores).length === 0) return true;
119    if (config.allow_multiple) {
120      return summary.can_evaluate_again !== false;
121    }
122    return true;
123  }
124
125  // --- Helpers reutilizáveis ---
126
127  /**
128   * Coleta scores dos raters de critério dentro de um container.
129   * Retorna {scores, allRated} — scores é dict {criteria_id: value}.
130   */
131  function collectScores($container, config, raterClassPrefix) {
132    var scores = {};
133    var allRated = true;
134    config.criteria.forEach(function(criterion) {
135      var $rater = $container.find('.' + raterClassPrefix + criterion.id);
136      var value = $rater.rate("getValue");
137      if (value > 0) {
138        scores[criterion.id] = value;
139      } else {
140        allRated = false;
141      }
142    });
143    return { scores: scores, allRated: allRated };
144  }
145
146  /**
147   * Extrai mensagem de erro de um xhr response do DRF.
148   */
149  function getErrorMessage(xhr) {
150    if (xhr.responseJSON && xhr.responseJSON.detail) {
151      return xhr.responseJSON.detail;
152    }
153    return '';
154  }
155
156  /**
157   * Busca os scores de uma sessão específica dentro do summary.
158   */
159  function getSessionScores(summary, sessionId) {
160    for (var i = 0; i < summary.user_sessions.length; i++) {
161      if (summary.user_sessions[i].session_id === sessionId && summary.user_sessions[i].scores) {
162        return summary.user_sessions[i].scores;
163      }
164    }
165    return summary.user_scores || {};
166  }
167
168  /**
169   * Inicializa raters editáveis com scores de uma sessão.
170   */
171  function initSessionRaters($container, config, sessionScores, raterClassPrefix) {
172    config.criteria.forEach(function(criterion) {
173      var $rater = $container.find('.' + raterClassPrefix + criterion.id);
174      var initialValue = sessionScores[criterion.id] || 0;
175      $rater.attr("data-rate-value", initialValue).rate($.extend(true, {}, raterDefaultOptions, {
176        max_value: config.max_stars
177      }));
178    });
179  }
180
181  // =========================================================================
182  // FASE 1 — Badge inline (summary-light)
183  // =========================================================================
184
185  /**
186   * Renderiza o badge compacto dentro do container.
187   */
188  function renderBadge($container, lightSummary) {
189    var variant = $container.attr(CONSTS.VARIANT_ATTR);
190    var template = variant === 'inline' ? CONSTS.TEMPLATE_INLINE : CONSTS.TEMPLATE_BADGE;
191    var isAuthenticated = parseInt($container.attr(CONSTS.IS_AUTH_ATTR), 10) === 1;
192    $container.addClass(Looper.template_nunjucks_initialized_class);
193    $container.html(nunjucks.render(template, {
194      avg_score: lightSummary.avg_score || 0,
195      avg_score_pct: lightSummary.avg_score_pct || 0,
196      total_evaluations: lightSummary.total_evaluations || 0,
197      criteria_scores: lightSummary.criteria_scores || [],
198      is_authenticated: isAuthenticated
199    }));
200
201    var badgeRaterOptions = $.extend(true, {}, raterDefaultOptions, {
202      max_value: lightSummary.max_stars || 5,
203      readonly: true,
204      step_size: 0.1
205    });
206
207    // Inicializa o rater em readonly para cada critério
208    if (lightSummary.criteria_scores) {
209      lightSummary.criteria_scores.forEach(function(criterion, index) {
210        var $rater = $container.find('.plus_rating_badge_criterion_' + index);
211        if ($rater.length > 0) {
212          $rater.attr("data-rate-value", criterion.avg_score || 0).rate(badgeRaterOptions);
213        }
214      });
215    }
216
217    // Inicializa o rater em readonly para a média geral no detalhe
218    var $overallRater = $container.find('.plus_rating_badge_overall');
219    if ($overallRater.length > 0) {
220      $overallRater.attr("data-rate-value", lightSummary.avg_score || 0).rate(badgeRaterOptions);
221    }
222
223    // Inicializa o rater em readonly para a média geral no summary
224    var $overallMainRater = $container.find('.plus_rating_badge_overall_main');
225    if ($overallMainRater.length > 0) {
226      $overallMainRater.attr("data-rate-value", lightSummary.avg_score || 0).rate(badgeRaterOptions);
227    }
228
229    // Handler: ao clicar no link, abre o modal com dados completos
230    $container.find('.plus_rating_evaluation_open_modal').on('click', function(e) {
231      e.preventDefault();
232      openEvaluationModal($container);
233    });
234  }
235
236  // =========================================================================
237  // FASE 2 — Modal completo (summary + config sob demanda)
238  // =========================================================================
239
240  /**
241   * Abre o modal e carrega dados completos do summary + config.
242   */
243  function openEvaluationModal($container) {
244    ensureEvaluationModal();
245    var $modal = $('#' + CONSTS.MODAL_ID);
246    var contentType = $container.attr(CONSTS.MODEL_ATTR);
247    var objectId = $container.attr(CONSTS.NODE_ID_ATTR);
248
249    // Renderiza o modal com loading
250    $modal.html(nunjucks.render(CONSTS.TEMPLATE_MODAL, {}));
251    $modal.modal('show');
252
253    var $modalBody = $modal.find('.plus_rating_evaluation_modal_body');
254
255    // Fetch paralelo: summary completo + config
256    var summaryReq = $.ajax({
257      url: Urls[CONSTS.API_SUMMARY_URL](),
258      data: { content_type: contentType, object_id: objectId },
259      method: "GET",
260      dataType: "json"
261    });
262
263    var configReq;
264    if (configCache[contentType]) {
265      configReq = $.Deferred().resolve(configCache[contentType]);
266    } else {
267      configReq = $.ajax({
268        url: Urls[CONSTS.API_CONFIG_URL](),
269        data: { content_type: contentType },
270        method: "GET",
271        dataType: "json"
272      });
273    }
274
275    $.when(summaryReq, configReq).done(function(summaryResult, configResult) {
276      // $.when retorna arrays quando são ajax, valor direto quando deferred
277      var summaryData = Array.isArray(summaryResult) ? summaryResult[0] : summaryResult;
278      var configData = Array.isArray(configResult) ? configResult[0] : configResult;
279
280      configCache[contentType] = configData;
281
282      if (!configData.active) {
283        $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {}));
284        return;
285      }
286
287      renderModalContent($modal, $container, configData, summaryData);
288    }).fail(function() {
289      $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {}));
290    });
291  }
292
293  /**
294   * Renderiza o conteúdo completo dentro do modal e liga os handlers.
295   */
296  function renderModalContent($modal, $container, config, summary) {
297    var $modalBody = $modal.find('.plus_rating_evaluation_modal_body');
298    var isAuthenticated = parseInt($container.attr(CONSTS.IS_AUTH_ATTR), 10) === 1;
299    var userCanEvaluate = canUserEvaluate(config, summary, isAuthenticated);
300    var widgetId = 'modal_eval_' + Math.random().toString(36).substr(2, 6);
301    var contentType = $container.attr(CONSTS.MODEL_ATTR);
302    var objectId = $container.attr(CONSTS.NODE_ID_ATTR);
303
304    var nunjucksContext = {
305      criteria_box_class: CONSTS.CRITERIA_BOX_CLASS,
306      review_box_class: CONSTS.REVIEW_BOX_CLASS,
307      message_class: CONSTS.MESSAGE_CLASS,
308      submit_btn_class: CONSTS.SUBMIT_BTN_CLASS,
309      widget_id: widgetId,
310      criteria: config.criteria,
311      max_stars: config.max_stars,
312      allow_multiple: config.allow_multiple || false,
313      criteria_scores: summary.criteria_scores || [],
314      avg_score: summary.avg_score || 0,
315      avg_score_pct: summary.avg_score_pct || 0,
316      total_evaluations: summary.total_evaluations || 0,
317      total_reviews: summary.total_reviews || 0,
318      user_evaluated: summary.user_scores && Object.keys(summary.user_scores).length > 0,
319      is_authenticated: isAuthenticated,
320      can_evaluate: userCanEvaluate,
321      can_evaluate_again: summary.can_evaluate_again !== false,
322      can_edit: summary.can_edit || false,
323      user_scores: summary.user_scores || {},
324      user_sessions: (summary.user_sessions || []).map(function(s) {
325        var scores = s.scores || {};
326        var vals = Object.values(scores);
327        var avg = vals.length > 0
328          ? vals.reduce(function(a, b) { return a + b; }, 0) / vals.length
329          : 0;
330        return Object.assign({}, s, { avg_score: Math.round(avg * 10) / 10 });
331      }),
332      reviews: summary.reviews || []
333    };
334
335    $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_DONE, nunjucksContext));
336
337    // Quando o usuário não pode avaliar, troca a chamada de ação do header
338    // por uma frase de leitura (modal vira somente visualização das reviews).
339    if (!userCanEvaluate) {
340      $modal.find('.rating-evaluation-modal-sub').text(gettext("See what others are saying"));
341    }
342
343    // Inicializa rater readonly para a média geral no summary do lightbox
344    var $modalOverallStars = $modalBody.find('.plus_rating_modal_overall_stars');
345    if ($modalOverallStars.length > 0) {
346      $modalOverallStars.attr("data-rate-value", summary.avg_score || 0).rate($.extend(true, {}, raterDefaultOptions, {
347        max_value: config.max_stars,
348        readonly: true,
349        step_size: 0.1
350      }));
351    }
352
353    // Inicializa rater para cada critério
354    if (userCanEvaluate) {
355      config.criteria.forEach(function(criterion) {
356        var $rater = $modalBody.find('.plus_rating_evaluation_criterion_' + criterion.id);
357        if ($rater.length === 0) return;
358
359        var initialValue = 0;
360        if (!config.allow_multiple && summary.user_scores) {
361          initialValue = summary.user_scores[criterion.id] || 0;
362        }
363
364        $rater.attr("data-rate-value", initialValue).rate($.extend(true, {}, raterDefaultOptions, {
365          max_value: config.max_stars
366        }));
367      });
368    }
369
370    // Opções dos raters readonly para avg_score nas sessões e reviews
371    var avgStarOptions = $.extend(true, {}, raterDefaultOptions, {
372      max_value: config.max_stars,
373      readonly: true,
374      step_size: 0.1
375    });
376
377    // Inicializa raters readonly de avg_score nas sessões do usuário
378    nunjucksContext.user_sessions.forEach(function(session, index) {
379      if (session.avg_score) {
380        var $rater = $modalBody.find('.plus_rating_session_avg_stars_' + index);
381        if ($rater.length > 0) {
382          $rater.attr('data-rate-value', session.avg_score).rate(avgStarOptions);
383        }
384      }
385    });
386
387    // Inicializa raters readonly de avg_score nas reviews de outros usuários
388    (summary.reviews || []).forEach(function(review, index) {
389      if (review.avg_score) {
390        var $rater = $modalBody.find('.plus_rating_review_avg_stars_' + index);
391        if ($rater.length > 0) {
392          $rater.attr('data-rate-value', review.avg_score).rate(avgStarOptions);
393        }
394      }
395    });
396
397    var $message = $modalBody.find('.' + CONSTS.MESSAGE_CLASS);
398
399    // --- Função para recarregar o modal após uma ação ---
400    function refreshModal() {
401      $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_LOADING, {}));
402      $.ajax({
403        // nocache=1 só nesse fluxo de refresh pós-mutação — bypass o cache do
404        // browser (HTTP max-age=5 servido pelo middleware) que de outra forma
405        // devolveria user_sessions/avg_score em estado pré-mutação.
406        url: Urls[CONSTS.API_SUMMARY_URL](),
407        data: { content_type: contentType, object_id: objectId, nocache: 1 },
408        method: "GET",
409        dataType: "json"
410      }).done(function(summaryData) {
411        renderModalContent($modal, $container, config, summaryData);
412        // Atualiza o badge inline também
413        refreshBadge($container);
414      }).fail(function() {
415        $modalBody.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {}));
416      });
417    }
418
419    // --- Handler de submit ---
420    var $submitBtn = $modalBody.find('.' + CONSTS.SUBMIT_BTN_CLASS);
421    $submitBtn.on('click', function(e) {
422      e.preventDefault();
423      $submitBtn.prop('disabled', true);
424      $message.addClass('d-none');
425
426      var result = collectScores($modalBody, config, 'plus_rating_evaluation_criterion_');
427      if (!result.allRated) {
428        $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
429          custom_message: gettext("Please rate all criteria before submitting.")
430        }));
431        $submitBtn.prop('disabled', false);
432        return;
433      }
434
435      var $reviewTextarea = $modalBody.find('.' + CONSTS.REVIEW_BOX_CLASS + ' textarea');
436      var reviewText = $reviewTextarea.length > 0 ? $reviewTextarea.val() : '';
437      var $isPublicCheck = $modalBody.find('.plus_rating_evaluation_is_public');
438      var isPublic = $isPublicCheck.length > 0 ? $isPublicCheck.is(':checked') : true;
439
440      $.ajax({
441        url: Urls[CONSTS.API_SUBMIT_URL](),
442        method: "POST",
443        contentType: "application/json",
444        data: JSON.stringify({
445          content_type: contentType,
446          object_id: objectId,
447          scores: result.scores,
448          review_text: reviewText,
449          is_public: isPublic
450        }),
451        headers: { "X-CSRFToken": Looper.getCsrftoken() },
452        dataType: "json"
453      }).done(function() {
454        $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_SUCCESS, {}));
455        setTimeout(refreshModal, 1500);
456      }).fail(function(xhr) {
457        $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
458          custom_message: getErrorMessage(xhr)
459        }));
460        $submitBtn.prop('disabled', false);
461      });
462    });
463
464    // --- Handler de delete sessão ---
465    $modalBody.find('.plus_rating_evaluation_delete_session').on('click', function(e) {
466      e.preventDefault();
467      var $btn = $(this);
468      var sessionId = $btn.data('session-id');
469
470      showConfirmModal(
471        gettext("Delete evaluation"),
472        gettext("Are you sure you want to delete this evaluation?"),
473        function() {
474          $btn.prop('disabled', true);
475          $.ajax({
476            url: Urls[CONSTS.API_DELETE_SESSION_URL](),
477            method: "POST",
478            contentType: "application/json",
479            data: JSON.stringify({ session_id: sessionId }),
480            headers: { "X-CSRFToken": Looper.getCsrftoken() },
481            dataType: "json"
482          }).done(function() {
483            refreshModal();
484            refreshBadge($container);
485          }).fail(function() {
486            $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
487              custom_message: gettext("Failed to delete evaluation.")
488            }));
489            $btn.prop('disabled', false);
490          });
491        }
492      );
493    });
494
495    // --- Handler de toggle public/private ---
496    $modalBody.find('.plus_rating_evaluation_toggle_public').on('click', function(e) {
497      e.preventDefault();
498      var $btn = $(this);
499      var sessionId = $btn.data('session-id');
500      var currentIsPublic = !!parseInt($btn.data('is-public'), 10);
501      var newIsPublic = !currentIsPublic;
502
503      $btn.prop('disabled', true);
504
505      $.ajax({
506        url: Urls[CONSTS.API_TOGGLE_REVIEW_PUBLIC_URL](),
507        method: "POST",
508        contentType: "application/json",
509        data: JSON.stringify({
510          session_id: sessionId,
511          is_public: newIsPublic
512        }),
513        headers: { "X-CSRFToken": Looper.getCsrftoken() },
514        dataType: "json"
515      }).done(function() {
516        refreshModal();
517      }).fail(function(xhr) {
518        console.error("Toggle review visibility failed:", xhr.status, xhr.responseText);
519        $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
520          custom_message: getErrorMessage(xhr) || gettext("Failed to update review visibility.")
521        }));
522        $btn.prop('disabled', false);
523      });
524    });
525
526    // --- Handler de edição de review (texto + estrelas) ---
527    $modalBody.find('.plus_rating_evaluation_edit_review').on('click', function(e) {
528      e.preventDefault();
529      var $btn = $(this);
530      var sessionId = $btn.data('session-id');
531      var currentText = $btn.data('review-text');
532      var currentIsPublic = !!parseInt($btn.data('is-public'), 10);
533      var $sessionDiv = $btn.closest('.plus_rating_user_session_card');
534      var $textEl = $sessionDiv.find('.plus_rating_evaluation_review_text');
535
536      var sessionScores = getSessionScores(summary, sessionId);
537
538      // Monta bloco de edição: estrelas + texto + visibilidade
539      var $editBlock = $('<div class="plus_rating_evaluation_edit_block"></div>');
540
541      // Critérios com raters para edição de scores
542      if (summary.can_edit) {
543        var $criteriaBox = $('<div class="mb-2"></div>');
544        config.criteria.forEach(function(criterion) {
545          var initialValue = sessionScores[criterion.id] || 0;
546          var $criterionDiv = $('<div class="plus_rating_evaluation_criterion mb-1">' +
547            '<label class="d-block mb-0 small font-weight-bold">' + criterion.name + '</label>' +
548            '<div class="plus_rating_evaluation_inline_criterion_' + criterion.id + '"></div></div>');
549          $criteriaBox.append($criterionDiv);
550        });
551        $editBlock.append($criteriaBox);
552      }
553
554      var $textarea = $('<textarea class="form-control form-control-sm mb-1" rows="2"></textarea>').val(currentText);
555      var $isPublicCheck = $('<div class="form-check mt-1"><input type="checkbox" class="form-check-input" ' +
556        (currentIsPublic ? 'checked' : '') + '><label class="form-check-label small">' +
557        gettext("Make my review public") + '</label></div>');
558      var $saveBtn = $('<button type="button" class="btn btn-sm btn-primary mt-1 mr-1">' +
559        gettext("Save") + '</button>');
560      var $cancelBtn = $('<button type="button" class="btn btn-sm btn-secondary mt-1">' +
561        gettext("Cancel") + '</button>');
562
563      $editBlock.append($textarea).append($isPublicCheck)
564        .append($('<div class="mt-1"></div>').append($saveBtn).append($cancelBtn));
565
566      if ($textEl.length > 0) {
567        $textEl.replaceWith($editBlock);
568      } else {
569        $sessionDiv.find('.flex-grow-1').html($editBlock);
570      }
571      $btn.hide();
572
573      // Inicializa raters após inserir no DOM
574      if (summary.can_edit) {
575        initSessionRaters($editBlock, config, sessionScores, 'plus_rating_evaluation_inline_criterion_');
576      }
577
578      $cancelBtn.on('click', function() {
579        refreshModal();
580      });
581
582      $saveBtn.on('click', function() {
583        var newText = $textarea.val().trim();
584        if (!newText) return;
585        $saveBtn.prop('disabled', true);
586
587        // Coleta scores se os raters existem
588        var hasScores = summary.can_edit && config.criteria.length > 0;
589        var scores = {};
590
591        if (hasScores) {
592          var result = collectScores($editBlock, config, 'plus_rating_evaluation_inline_criterion_');
593          scores = result.scores;
594
595          if (!result.allRated) {
596            $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
597              custom_message: gettext("Please rate all criteria before submitting.")
598            }));
599            $saveBtn.prop('disabled', false);
600            return;
601          }
602        }
603
604        // Salva review + scores em paralelo
605        var reviewReq = $.ajax({
606          url: Urls[CONSTS.API_EDIT_REVIEW_URL](),
607          method: "POST",
608          contentType: "application/json",
609          data: JSON.stringify({
610            session_id: sessionId,
611            review_text: newText,
612            is_public: $isPublicCheck.find('input').is(':checked')
613          }),
614          headers: { "X-CSRFToken": Looper.getCsrftoken() },
615          dataType: "json"
616        });
617
618        var requests = [reviewReq];
619
620        if (hasScores) {
621          var evalReq = $.ajax({
622            url: Urls[CONSTS.API_EDIT_EVALUATION_URL](),
623            method: "POST",
624            contentType: "application/json",
625            data: JSON.stringify({
626              content_type: contentType,
627              object_id: objectId,
628              session_id: sessionId,
629              scores: scores
630            }),
631            headers: { "X-CSRFToken": Looper.getCsrftoken() },
632            dataType: "json"
633          });
634          requests.push(evalReq);
635        }
636
637        $.when.apply($, requests).done(function() {
638          refreshModal();
639          refreshBadge($container);
640        }).fail(function(xhr) {
641          $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
642            custom_message: getErrorMessage(xhr)
643          }));
644          $saveBtn.prop('disabled', false);
645        });
646      });
647    });
648
649    // --- Handler de edição de scores (botão standalone por sessão) ---
650    $modalBody.find('.plus_rating_evaluation_edit_scores').on('click', function(e) {
651      e.preventDefault();
652      var $btn = $(this);
653      var sessionId = $btn.data('session-id');
654      $btn.hide();
655
656      var currentScores = getSessionScores(summary, sessionId);
657
658      // Mostra os critérios com rater para edição
659      var $criteriaBox = $('<div class="' + CONSTS.CRITERIA_BOX_CLASS + ' mt-2"></div>');
660      config.criteria.forEach(function(criterion) {
661        var $criterionDiv = $('<div class="plus_rating_evaluation_criterion mb-2">' +
662          '<label class="d-block mb-1 small font-weight-bold">' + criterion.name + '</label>' +
663          '<div class="plus_rating_evaluation_edit_criterion_' + criterion.id + '"></div></div>');
664        $criteriaBox.append($criterionDiv);
665      });
666
667      var $saveBtn = $('<button type="button" class="btn btn-sm btn-primary mt-2 mr-1">' +
668        gettext("Save") + '</button>');
669      var $cancelBtn = $('<button type="button" class="btn btn-sm btn-secondary mt-2">' +
670        gettext("Cancel") + '</button>');
671      var $editContainer = $('<div class="plus_rating_evaluation_edit_scores_container"></div>')
672        .append($criteriaBox).append($saveBtn).append($cancelBtn);
673
674      $btn.after($editContainer);
675
676      // Inicializa raters com valores atuais
677      initSessionRaters($editContainer, config, currentScores, 'plus_rating_evaluation_edit_criterion_');
678
679      $cancelBtn.on('click', function() {
680        refreshModal();
681      });
682
683      $saveBtn.on('click', function() {
684        $saveBtn.prop('disabled', true);
685
686        var result = collectScores($editContainer, config, 'plus_rating_evaluation_edit_criterion_');
687        if (!result.allRated) {
688          $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
689            custom_message: gettext("Please rate all criteria before submitting.")
690          }));
691          $saveBtn.prop('disabled', false);
692          return;
693        }
694
695        $.ajax({
696          url: Urls[CONSTS.API_EDIT_EVALUATION_URL](),
697          method: "POST",
698          contentType: "application/json",
699          data: JSON.stringify({
700            content_type: contentType,
701            object_id: objectId,
702            session_id: sessionId,
703            scores: result.scores
704          }),
705          headers: { "X-CSRFToken": Looper.getCsrftoken() },
706          dataType: "json"
707        }).done(function() {
708          $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_SUCCESS, {}));
709          setTimeout(refreshModal, 1500);
710          refreshBadge($container);
711        }).fail(function(xhr) {
712          $message.removeClass('d-none').html(nunjucks.render(CONSTS.TEMPLATE_MSG_ERROR, {
713            custom_message: getErrorMessage(xhr)
714          }));
715          $saveBtn.prop('disabled', false);
716        });
717      });
718    });
719  }
720
721  // =========================================================================
722  // FASE 1 — Inicialização batch (summary-light)
723  // =========================================================================
724
725  /**
726   * Atualiza o badge inline de um container (após ação no modal).
727   */
728  function refreshBadge($container) {
729    var contentType = $container.attr(CONSTS.MODEL_ATTR);
730    var objectId = $container.attr(CONSTS.NODE_ID_ATTR);
731
732    $.ajax({
733      // nocache=1 — chamado apenas no refresh pós-mutação para escapar do
734      // cache do browser (HTTP max-age=5) e refletir o avg_score atualizado.
735      url: Urls[CONSTS.API_SUMMARY_LIGHT_URL](),
736      data: { content_type: contentType, object_id: objectId, nocache: 1 },
737      method: "GET",
738      dataType: "json"
739    }).done(function(data) {
740      renderBadge($container, data);
741    });
742  }
743
744  /**
745   * Função principal que encontra e inicializa os badges inline.
746   */
747  function initializeEvaluationComponents() {
748    if (isProcessing) return;
749    isProcessing = true;
750
751    var $unprocessed = $(CONSTS.CONTAINER_SELECTOR + ':not([' + CONSTS.PROCESSED_ATTR + '])');
752    if ($unprocessed.length === 0) {
753      isProcessing = false;
754      return;
755    }
756
757    // Marca como processado o mais rápido possível
758    $unprocessed.attr(CONSTS.PROCESSED_ATTR, 'true');
759
760    var evaluationsToFetch = [];
761    var evaluationsDataMap = {};
762    var counter = 0;
763
764    $unprocessed.each(function() {
765      var $el = $(this);
766      var evalData = {
767        content_type: $el.attr(CONSTS.MODEL_ATTR),
768        object_id: $el.attr(CONSTS.NODE_ID_ATTR)
769      };
770
771      if (evalData.content_type && evalData.object_id) {
772        var fetchData = $.extend({}, evalData, { id: counter });
773        evaluationsToFetch.push(fetchData);
774        evaluationsDataMap[counter] = { ref: $el, data: evalData };
775        counter++;
776      }
777    });
778
779    if (evaluationsToFetch.length === 0) {
780      isProcessing = false;
781      return;
782    }
783
784    // Usa summary-light para o batch (leve, sem reviews/scores)
785    var summaryUrl = Urls[CONSTS.API_SUMMARY_LIGHT_URL]();
786    var batches = Looper.chunkArrayByUrlSize(summaryUrl, CONSTS.API_KEY, evaluationsToFetch);
787    var ajaxPromises = [];
788
789    batches.forEach(function(batch) {
790      var request = $.ajax({
791        url: summaryUrl,
792        data: { evaluation: JSON.stringify(batch) },
793        method: "GET",
794        dataType: "json"
795      }).done(function(data) {
796        $.each(data, function(_, obj) {
797          if (obj.id === undefined || !evaluationsDataMap[obj.id]) return;
798          var mapped = evaluationsDataMap[obj.id];
799          renderBadge(mapped.ref, obj);
800        });
801      }).fail(function() {
802        batch.forEach(function(item) {
803          var mapped = evaluationsDataMap[item.id];
804          if (mapped) {
805            mapped.ref.addClass(Looper.template_nunjucks_initialized_class);
806            mapped.ref.html(nunjucks.render(CONSTS.TEMPLATE_FAIL, {}));
807          }
808        });
809      });
810
811      ajaxPromises.push(request);
812    });
813
814    $.when.apply($, ajaxPromises).always(function() {
815      isProcessing = false;
816    });
817  }
818
819  // --- Inicialização ---
820  initializeEvaluationComponents();
821  $(document).on("theme:newDomNodes", initializeEvaluationComponents);
822
823})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.