"use strict"; const VotesView = (function(){ const VotesView = function(judge_pole){ const self = this; let container = null; let rank_wrapper = null; let filter_query = ""; let expanded_project_id = null; this.go = () => { judge_pole.content_area.innerHTML = ""; judge_pole.html(judge_pole.content_area, build()); }; // Extrae las puntuaciones y pesos normalizados (con retrocompatibilidad) const normalize_project_data = (raw_data) => { if(!raw_data) return null; // Si el objeto fue guardado con la nueva estructura if(raw_data.scores !== undefined){ return { scores : raw_data.scores || {}, judge_weights : raw_data.judge_weights || {}, criteria_weights : raw_data.criteria_weights || {} }; } // Compatibilidad hacia atrás return { scores : raw_data, judge_weights : {}, criteria_weights : {} }; }; // Calcula métricas naturales y ponderadas completas const calculate_project_metrics = (project_id) => { const raw_entry = judge_pole.db.get_data().votes[project_id]; const pack = normalize_project_data(raw_entry); if(!pack) return null; const { scores, judge_weights, criteria_weights } = pack; const user_ids = Object.keys(scores); if(!user_ids.length) return null; const criteria_stats = {}; let natural_sum = 0, natural_count = 0; let weighted_sum = 0, total_weight_mass = 0; user_ids.forEach(u_id => { const u_weight = parseInt(judge_weights[u_id]) || 20; const u_scores = scores[u_id]; Object.keys(u_scores).forEach(crit => { const val = u_scores[crit]; const c_weight = parseInt(criteria_weights[crit]) || 20; if(!criteria_stats[crit]) { criteria_stats[crit] = { nat_sum : 0, nat_count : 0, pond_sum : 0, pond_weights : 0 }; } // Natural por criterio criteria_stats[crit].nat_sum += val; criteria_stats[crit].nat_count++; // Ponderado de criterio según peso de juez criteria_stats[crit].pond_sum += (val * u_weight); criteria_stats[crit].pond_weights += u_weight; // Globales natural_sum += val; natural_count++; const joint_weight = u_weight * c_weight; weighted_sum += (val * joint_weight); total_weight_mass += joint_weight; }); }); const criteria_averages = {}; Object.keys(criteria_stats).forEach(crit => { const stat = criteria_stats[crit]; criteria_averages[crit] = { natural : stat.nat_count > 0 ? (stat.nat_sum / stat.nat_count).toFixed(2) : "0.00", weighted : stat.pond_weights > 0 ? (stat.pond_sum / stat.pond_weights).toFixed(2) : "0.00" }; }); return { natural_avg : natural_count > 0 ? (natural_sum / natural_count).toFixed(2) : "0.00", weighted_avg : total_weight_mass > 0 ? (weighted_sum / total_weight_mass).toFixed(2) : "0.00", criteria : criteria_averages }; }; const build = () => { const is_admin = judge_pole.session.is_admin(); const topbar_actions = [ ["input", { "type" : "search", "class" : "jp-search-input", "data-i18n" : "search_rankings_placeholder", "data-i18n-without" : "true", "placeholder" : judge_pole.i18n.get("search_rankings_placeholder"), "value" : filter_query, "oninput" : (input) => { filter_query = input.value.trim().toLowerCase(); render_rankings(); } }] ]; if(is_admin){ topbar_actions.push(["button", { "type" : "button", "class" : "jp-btn-primary", "data-i18n" : "btn_new_vote", "onclick" : () => show_vote_modal() }, judge_pole.i18n.get("btn_new_vote")]); } container = judge_pole.html(["div", { "class" : "jp-view jp-votes-view" }, [ ["div", { "class" : "jp-view-topbar" }, [ ["h2", { "data-i18n" : "votes_title" }, judge_pole.i18n.get("votes_title")], ["div", { "style" : "display:flex; gap:0.5em; align-items:center;" }, topbar_actions] ]], rank_wrapper = judge_pole.html(["div", { "class" : "jp-rank-table-wrapper" }])[0] ]])[0]; render_rankings(); return container; }; const render_rankings = () => { if(!rank_wrapper) return; rank_wrapper.innerHTML = ""; const db_data = judge_pole.db.get_data(); const projects = db_data.projects; const is_admin = judge_pole.session.is_admin(); const scored_projects = []; projects.forEach(project => { const metrics = calculate_project_metrics(project.id); if(metrics){ scored_projects.push({ project : project, natural_avg : metrics.natural_avg, weighted_avg : metrics.weighted_avg, weighted_num : parseFloat(metrics.weighted_avg), criteria : metrics.criteria }); } }); scored_projects.sort((a, b) => b.weighted_num - a.weighted_num); scored_projects.forEach((item, index) => { item.global_rank = index + 1; }); if(!scored_projects.length){ rank_wrapper.appendChild(judge_pole.html(["p", { "class" : "jp-empty", "data-i18n" : "no_votes_registered" }, judge_pole.i18n.get("no_votes_registered")])[0]); return; } const filtered_list = scored_projects.filter(item => { if(!filter_query) return true; return item.project.name.toLowerCase().includes(filter_query); }); // Cabeceras de tabla: la columna acciones sólo se muestra si es admin const thead_cols = [ ["th", { "data-i18n" : "rank_global" }, judge_pole.i18n.get("rank_global")], ["th", { "data-i18n" : "rank_filtered" }, judge_pole.i18n.get("rank_filtered")], ["th", { "data-i18n" : "rank_project" }, judge_pole.i18n.get("rank_project")], ["th", { "data-i18n" : "rank_average" }, judge_pole.i18n.get("rank_average")] ]; if(is_admin){ thead_cols.push(["th", { "data-i18n" : "rank_actions", "style" : "text-align:right;" }, judge_pole.i18n.get("rank_actions")]); } const table = judge_pole.html(["table", { "class" : "jp-rank-table" }, [ ["thead", {}, [ ["tr", {}, thead_cols] ]], ["tbody", {}] ]])[0]; const tbody = table.querySelector("tbody"); filtered_list.forEach((item, filter_idx) => { const is_expanded = expanded_project_id === item.project.id; const row_cols = [ ["td", { "class" : "jp-rank-badge-global" }, "#" + item.global_rank], ["td", { "class" : "jp-rank-badge-filter" }, "#" + (filter_idx + 1)], ["td", { "style" : "font-weight:bold;" }, item.project.name], ["td", {}, [ ["span", { "class" : "jp-rank-avg-score" }, item.weighted_avg + " pts "], ["small", { "style" : "color:#8b949e;" }, "(Nat: " + item.natural_avg + ")"] ]] ]; // Celda de acciones sólo si es admin if(is_admin){ row_cols.push(["td", { "style" : "text-align:right;" }, [ ["button", { "type" : "button", "class" : "jp-btn", "style" : "margin-right:0.4em; padding:0.2em 0.5em;", "data-i18n" : "btn_edit", "data-i18n-without" : "true", "title" : judge_pole.i18n.get("btn_edit"), "onclick" : () => show_vote_modal(item.project.id) }, "✏️"], ["button", { "type" : "button", "class" : "jp-btn-danger", "style" : "padding:0.2em 0.5em;", "data-i18n" : "btn_delete", "data-i18n-without" : "true", "title" : judge_pole.i18n.get("btn_delete"), "onclick" : () => { const msg = judge_pole.i18n.get("confirm_delete_vote", { project : item.project.name }); if(confirm(msg)){ delete db_data.votes[item.project.id]; if(expanded_project_id === item.project.id) expanded_project_id = null; render_rankings(); } } }, "✕"] ]]); } const tr = judge_pole.html(["tr", { "onclick" : (row, event) => { if(event.target.closest("button")) return; expanded_project_id = is_expanded ? null : item.project.id; render_rankings(); } }, row_cols])[0]; tbody.appendChild(tr); // Desglose expandido (visible para todos en solo lectura) if(is_expanded){ const breakdown_items = Object.keys(item.criteria).map(crit => ["div", { "class" : "jp-breakdown-item" }, [ ["span", { "class" : "jp-crit-name" }, crit], ["div", { "style" : "display:flex; gap:0.4em;" }, [ ["span", { "style" : "color:#8b949e;" }, "N: " + item.criteria[crit].natural], ["span", { "class" : "jp-crit-val" }, "P: " + item.criteria[crit].weighted] ]] ]]); const raw_pack = normalize_project_data(db_data.votes[item.project.id]) || { scores:{}, judge_weights:{}, criteria_weights:{} }; const participating_users = db_data.users.filter(u => Object.keys(raw_pack.scores).includes(u.id)); const active_criteria_keys = Object.keys(item.criteria); const static_table = judge_pole.html(["table", { "class" : "jp-matrix-table", "style" : "margin-top:0.8em;" }, [ ["thead", {}, [ ["tr", {}, [ ["th", { "style" : "text-align:left;" }, "Criterio"] ].concat(participating_users.map(u => [ "th", {}, [ ["div", {}, (u.type === 'ai' ? '🤖 ' : '👤 ') + u.nick], ["small", { "style" : "color:#f1e05a;" }, "(W: " + (raw_pack.judge_weights[u.id] || 20) + ")"] ] ])).concat([ ["th", { "class" : "jp-summary-cell" }, "Media Nat."], ["th", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;" }, "Media Pond."] ])] ]], ["tbody", {}] ]])[0]; const static_tbody = static_table.querySelector("tbody"); active_criteria_keys.forEach(crit => { const crit_w = raw_pack.criteria_weights[crit] || 20; const cells = [ ["td", { "class" : "jp-crit-cell" }, [ ["span", {}, crit + " "], ["small", { "style" : "color:#f1e05a;" }, "(W: " + crit_w + ")"] ]] ]; participating_users.forEach(u => { const score = (raw_pack.scores[u.id] && raw_pack.scores[u.id][crit] !== undefined) ? raw_pack.scores[u.id][crit] : "-"; cells.push(["td", { "style" : "font-weight:bold;" }, "" + score]); }); cells.push(["td", { "class" : "jp-summary-subcell" }, item.criteria[crit].natural]); cells.push(["td", { "class" : "jp-summary-subcell", "style" : "color:#f1e05a;" }, item.criteria[crit].weighted]); static_tbody.appendChild(judge_pole.html(["tr", {}, cells])[0]); }); const col_count = is_admin ? "5" : "4"; const tr_detail = judge_pole.html(["tr", { "class" : "jp-breakdown-row" }, [ ["td", { "colspan" : col_count }, [ ["div", { "style" : "font-size:0.75em; color:#8b949e;", "data-i18n" : "rank_criteria_breakdown" }, judge_pole.i18n.get("rank_criteria_breakdown")], ["div", { "class" : "jp-breakdown-grid" }, breakdown_items], static_table ]] ]])[0]; tbody.appendChild(tr_detail); } }); rank_wrapper.appendChild(table); }; const show_vote_modal = (target_project_id = null) => { const db_data = judge_pole.db.get_data(); let selected_p_id = target_project_id || (db_data.projects[0] ? db_data.projects[0].id : null); const existing_pack = normalize_project_data(db_data.votes[selected_p_id]) || { scores : {}, judge_weights : {}, criteria_weights : {} }; let chosen_judges = Object.keys(existing_pack.scores).length ? Object.keys(existing_pack.scores) : db_data.users.map(u => u.id); let chosen_criteria = [...db_data.criteria]; // Pesos en memoria const active_judge_weights = { ...existing_pack.judge_weights }; const active_criteria_weights = { ...existing_pack.criteria_weights }; const close_modal = () => { if(modal_overlay && modal_overlay.parentNode) modal_overlay.remove(); }; const modal_overlay = judge_pole.html(["div", { "class" : "jp-modal-overlay", "onclick" : (overlay, e) => { if(e.target === overlay) close_modal(); } }, [ ["div", { "class" : "jp-modal-window", "style" : "max-width:50em;" }, [ ["div", { "class" : "jp-modal-header" }, [ ["h3", {}, target_project_id ? judge_pole.i18n.get("modal_edit_vote_title", { project : db_data.projects.find(p => p.id === target_project_id).name }) : judge_pole.i18n.get("modal_new_vote_title")], ["button", { "type" : "button", "class" : "jp-btn-danger", "onclick" : () => close_modal() }, "✕"] ]], ["div", { "class" : "jp-modal-body" }] ]] ]])[0]; const modal_body = modal_overlay.querySelector(".jp-modal-body"); // --- 1. Selector de Proyecto --- const project_options = db_data.projects.map(p => ["option", { "value" : p.id, ...(p.id === selected_p_id ? { "selected" : "selected" } : {}) }, p.name]); const project_select = judge_pole.html(["div", { "class" : "jp-field" }, [ ["label", { "data-i18n" : "select_project" }, judge_pole.i18n.get("select_project")], ["select", { "onchange" : (sel) => { selected_p_id = sel.value; rebuild_matrix(); } }, project_options] ]])[0]; // --- 2. Picker de Jueces con Pesos Numéricos --- const judges_picker = judge_pole.html(["div", { "class" : "jp-field" }, [ ["label", { "data-i18n" : "participating_judges" }, judge_pole.i18n.get("participating_judges")], ["div", { "class" : "jp-authors-picker" }, [ ["input", { "type" : "search", "class" : "jp-authors-search-input", "data-i18n" : "search_authors_placeholder", "data-i18n-without" : "true", "placeholder" : judge_pole.i18n.get("search_authors_placeholder"), "oninput" : (input) => { const q = input.value.trim().toLowerCase(); judges_list_box.querySelectorAll(".jp-criterion-row").forEach(lbl => { const nick = lbl.getAttribute("data-name") || ""; lbl.style.display = (!q || nick.includes(q)) ? "flex" : "none"; }); } }], ["div", { "class" : "jp-authors-select-box" }] ]] ]])[0]; const judges_list_box = judges_picker.querySelector(".jp-authors-select-box"); db_data.users.forEach(user => { const is_checked = chosen_judges.includes(user.id); const current_w = active_judge_weights[user.id] !== undefined ? active_judge_weights[user.id] : 20; active_judge_weights[user.id] = current_w; const row = judge_pole.html(["div", { "class" : "jp-criterion-row", "data-name" : user.nick.toLowerCase() }, [ ["label", { "class" : "jp-check-label", "style" : "flex:1; cursor:pointer;" }, [ ["input", { "type" : "checkbox", "value" : user.id, ...(is_checked ? { "checked" : "checked" } : {}), "onchange" : (chk) => { // Buscamos el input de peso relativo a esta fila de forma segura const row_parent = chk.closest(".jp-criterion-row"); const input_target = row_parent ? row_parent.querySelector(".jp-weight-input") : null; if(input_target) input_target.disabled = !chk.checked; if(chk.checked) chosen_judges.push(user.id); else chosen_judges = chosen_judges.filter(id => id !== user.id); rebuild_matrix(); } }], (user.type === "ai" ? "🤖 " : "👤 ") + user.nick ]], ["div", { "style" : "display:flex; align-items:center;" }, [ ["small", { "style" : "color:#8b949e; font-size:0.7em;", "data-i18n" : "weight_label" }, judge_pole.i18n.get("weight_label")], ["input", { "type" : "number", "min" : "1", "step" : "1", "class" : "jp-weight-input", "value" : current_w, ...(!is_checked ? { "disabled" : "disabled" } : {}), "oninput" : (inp) => { const val = parseInt(inp.value) || 1; active_judge_weights[user.id] = Math.max(1, Math.floor(val)); rebuild_matrix(); } }] ]] ]])[0]; judges_list_box.appendChild(row); }); // --- 3. Picker de Criterios con Pesos Numéricos --- const criteria_picker = judge_pole.html(["div", { "class" : "jp-field" }, [ ["div", { "style" : "display:flex; justify-content:space-between; align-items:center;" }, [ ["label", { "data-i18n" : "evaluation_criteria" }, judge_pole.i18n.get("evaluation_criteria")], ["button", { "type" : "button", "class" : "jp-btn-secondary", "style" : "padding:0.2em 0.5em; font-size:0.7em;", "data-i18n" : "btn_new_criterion", "onclick" : () => { const new_c = prompt(judge_pole.i18n.get("new_criterion_prompt")); if(new_c && judge_pole.db.add_criterion(new_c)){ chosen_criteria.push(new_c.trim()); active_criteria_weights[new_c.trim()] = 20; render_criteria_rows(); rebuild_matrix(); } } }, judge_pole.i18n.get("btn_new_criterion")] ]], ["div", { "class" : "jp-authors-picker" }, [ ["input", { "type" : "search", "class" : "jp-authors-search-input", "data-i18n" : "search_criteria_placeholder", "data-i18n-without" : "true", "placeholder" : judge_pole.i18n.get("search_criteria_placeholder"), "oninput" : (input) => { const q = input.value.trim().toLowerCase(); criteria_list_box.querySelectorAll(".jp-criterion-row").forEach(lbl => { const name = lbl.getAttribute("data-name") || ""; lbl.style.display = (!q || name.includes(q)) ? "flex" : "none"; }); } }], ["div", { "class" : "jp-authors-select-box" }] ]] ]])[0]; const criteria_list_box = criteria_picker.querySelector(".jp-authors-select-box"); const render_criteria_rows = () => { criteria_list_box.innerHTML = ""; db_data.criteria.forEach(crit => { const is_checked = chosen_criteria.includes(crit); const current_w = active_criteria_weights[crit] !== undefined ? active_criteria_weights[crit] : 20; active_criteria_weights[crit] = current_w; const row = judge_pole.html(["div", { "class" : "jp-criterion-row", "data-name" : crit.toLowerCase() }, [ ["label", { "class" : "jp-check-label", "style" : "flex:1; cursor:pointer;" }, [ ["input", { "type" : "checkbox", "value" : crit, ...(is_checked ? { "checked" : "checked" } : {}), "onchange" : (chk) => { const row_parent = chk.closest(".jp-criterion-row"); const input_target = row_parent ? row_parent.querySelector(".jp-weight-input") : null; if(input_target) input_target.disabled = !chk.checked; if(chk.checked) chosen_criteria.push(crit); else chosen_criteria = chosen_criteria.filter(c => c !== crit); rebuild_matrix(); } }], crit ]], ["div", { "class" : "jp-crit-actions", "style" : "display:flex; align-items:center;" }, [ ["small", { "style" : "color:#8b949e; font-size:0.7em;", "data-i18n" : "weight_label" }, judge_pole.i18n.get("weight_label")], ["input", { "type" : "number", "min" : "1", "step" : "1", "class" : "jp-weight-input", "value" : current_w, ...(!is_checked ? { "disabled" : "disabled" } : {}), "oninput" : (inp) => { const val = parseInt(inp.value) || 1; active_criteria_weights[crit] = Math.max(1, Math.floor(val)); rebuild_matrix(); } }], ["button", { "type" : "button", "class" : "jp-btn", "style" : "padding:0.1em 0.3em; font-size:0.65em; margin-left:0.4em;", "title" : judge_pole.i18n.get("btn_edit"), "onclick" : () => { const updated_name = prompt(judge_pole.i18n.get("edit_criterion_prompt"), crit); if(updated_name && judge_pole.db.update_criterion(crit, updated_name)){ const idx = chosen_criteria.indexOf(crit); if(idx !== -1) chosen_criteria[idx] = updated_name.trim(); active_criteria_weights[updated_name.trim()] = active_criteria_weights[crit]; delete active_criteria_weights[crit]; render_criteria_rows(); rebuild_matrix(); } } }, "✏️"], ["button", { "type" : "button", "class" : "jp-btn-danger", "style" : "padding:0.1em 0.3em; font-size:0.65em; margin-left:0.2em;", "title" : judge_pole.i18n.get("btn_delete"), "onclick" : () => { const confirm_msg = judge_pole.i18n.get("confirm_delete_criterion", { criterion : crit }); if(confirm(confirm_msg)){ judge_pole.db.delete_criterion(crit); chosen_criteria = chosen_criteria.filter(c => c !== crit); delete active_criteria_weights[crit]; render_criteria_rows(); rebuild_matrix(); } } }, "✕"] ]] ]])[0]; criteria_list_box.appendChild(row); }); }; render_criteria_rows(); // --- 4. Matriz Transpuesta con Doble Medias --- const matrix_container = judge_pole.html(["div", { "style" : "overflow-x:auto; margin-top:1em;" }])[0]; const rebuild_matrix = () => { matrix_container.innerHTML = ""; if(!selected_p_id || !chosen_judges.length || !chosen_criteria.length) return; const current_scores = existing_pack.scores || {}; const participating_users = db_data.users.filter(u => chosen_judges.includes(u.id)); const table = judge_pole.html(["table", { "class" : "jp-matrix-table" }, [ ["thead", {}, [ ["tr", {}, [ ["th", { "style" : "text-align:left;" }, "Criterio (Peso)"] ].concat(participating_users.map(u => [ "th", {}, [ ["div", {}, (u.type === 'ai' ? '🤖 ' : '👤 ') + u.nick], ["small", { "style" : "color:#f1e05a;" }, "(W: " + (active_judge_weights[u.id] || 20) + ")"] ] ])).concat([ ["th", { "class" : "jp-summary-cell", "data-i18n" : "crit_avg_natural" }, judge_pole.i18n.get("crit_avg_natural")], ["th", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;", "data-i18n" : "crit_avg_weighted" }, judge_pole.i18n.get("crit_avg_weighted")] ])] ]], ["tbody", {}], ["tfoot", {}, [ // Tupla 1: Media Natural de Jueces ["tr", { "class" : "jp-summary-row" }, [ ["td", { "class" : "jp-crit-cell", "data-i18n" : "judge_avg_natural" }, judge_pole.i18n.get("judge_avg_natural")] ].concat(participating_users.map(u => ["td", { "class" : "jp-summary-cell", "data-judge-nat" : u.id }, "0.00"])).concat([ ["td", { "class" : "jp-summary-cell", "data-total-nat" : "true" }, "0.00"], ["td", { "class" : "jp-summary-cell", "style" : "background:#0d1117;" }, "-"] ])], // Tupla 2: Media Ponderada de Jueces (según peso de criterios) ["tr", { "class" : "jp-summary-row" }, [ ["td", { "class" : "jp-crit-cell", "style" : "color:#f1e05a;", "data-i18n" : "judge_avg_weighted" }, judge_pole.i18n.get("judge_avg_weighted")] ].concat(participating_users.map(u => ["td", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;", "data-judge-pond" : u.id }, "0.00"])).concat([ ["td", { "class" : "jp-summary-cell", "style" : "background:#0d1117;" }, "-"], ["td", { "class" : "jp-summary-cell", "style" : "color:#f1e05a;", "data-total-pond" : "true" }, "0.00"] ])] ]] ]])[0]; const tbody = table.querySelector("tbody"); chosen_criteria.forEach(crit => { const c_w = active_criteria_weights[crit] || 20; const row_cells = [ ["td", { "class" : "jp-crit-cell" }, [ ["span", {}, crit + " "], ["small", { "style" : "color:#f1e05a;" }, "(W: " + c_w + ")"] ]] ]; participating_users.forEach(user => { const saved_score = (current_scores[user.id] && current_scores[user.id][crit] !== undefined) ? current_scores[user.id][crit] : 5; row_cells.push(["td", {}, [ ["input", { "type" : "number", "min" : "0", "max" : "10", "step" : "0.5", "value" : saved_score, "class" : "jp-score-input", "data-judge" : user.id, "data-crit" : crit, "oninput" : () => calculate_live_averages(table) }] ]]); }); // 2 Columnas de media por criterio: Natural y Ponderada (por peso de juez) row_cells.push(["td", { "class" : "jp-summary-subcell", "data-crit-nat" : crit }, "0.00"]); row_cells.push(["td", { "class" : "jp-summary-subcell", "style" : "color:#f1e05a;", "data-crit-pond" : crit }, "0.00"]); tbody.appendChild(judge_pole.html(["tr", {}, row_cells])[0]); }); matrix_container.appendChild(table); calculate_live_averages(table); }; const calculate_live_averages = (table_element) => { const inputs = table_element.querySelectorAll(".jp-score-input"); const crit_data = {}; const judge_data = {}; let overall_nat_sum = 0, overall_nat_count = 0; let overall_pond_sum = 0, overall_pond_mass = 0; inputs.forEach(input => { const judge = input.getAttribute("data-judge"); const crit = input.getAttribute("data-crit"); const val = parseFloat(input.value) || 0; const j_w = active_judge_weights[judge] || 20; const c_w = active_criteria_weights[crit] || 20; // Acumuladores de Criterio if(!crit_data[crit]) crit_data[crit] = { nat_sum : 0, count : 0, pond_sum : 0, j_weight_sum : 0 }; crit_data[crit].nat_sum += val; crit_data[crit].count++; crit_data[crit].pond_sum += (val * j_w); crit_data[crit].j_weight_sum += j_w; // Acumuladores de Juez if(!judge_data[judge]) judge_data[judge] = { nat_sum : 0, count : 0, pond_sum : 0, c_weight_sum : 0 }; judge_data[judge].nat_sum += val; judge_data[judge].count++; judge_data[judge].pond_sum += (val * c_w); judge_data[judge].c_weight_sum += c_w; // Totales globales overall_nat_sum += val; overall_nat_count++; const joint_weight = j_w * c_w; overall_pond_sum += (val * joint_weight); overall_pond_mass += joint_weight; }); // Actualizar columnas por criterio Object.keys(crit_data).forEach(crit => { const d = crit_data[crit]; const nat_cell = table_element.querySelector(`[data-crit-nat="${crit}"]`); const pond_cell = table_element.querySelector(`[data-crit-pond="${crit}"]`); if(nat_cell) nat_cell.textContent = (d.nat_sum / (d.count || 1)).toFixed(2); if(pond_cell) pond_cell.textContent = (d.pond_sum / (d.j_weight_sum || 1)).toFixed(2); }); // Actualizar tuplas por juez Object.keys(judge_data).forEach(judge => { const d = judge_data[judge]; const nat_cell = table_element.querySelector(`[data-judge-nat="${judge}"]`); const pond_cell = table_element.querySelector(`[data-judge-pond="${judge}"]`); if(nat_cell) nat_cell.textContent = (d.nat_sum / (d.count || 1)).toFixed(2); if(pond_cell) pond_cell.textContent = (d.pond_sum / (d.c_weight_sum || 1)).toFixed(2); }); // Esquinas globales const total_nat_cell = table_element.querySelector(`[data-total-nat="true"]`); const total_pond_cell = table_element.querySelector(`[data-total-pond="true"]`); if(total_nat_cell) total_nat_cell.textContent = (overall_nat_sum / (overall_nat_count || 1)).toFixed(2); if(total_pond_cell) total_pond_cell.textContent = (overall_pond_sum / (overall_pond_mass || 1)).toFixed(2); }; const actions = judge_pole.html(["div", { "style" : "display:flex; justify-content:flex-end; gap:0.5em; margin-top:1em;" }, [ ["button", { "type" : "button", "class" : "jp-btn", "onclick" : () => close_modal() }, judge_pole.i18n.get("btn_cancel_edit")], ["button", { "type" : "button", "class" : "jp-btn-primary", "data-i18n" : "btn_save_scores", "onclick" : () => { const scores = {}; matrix_container.querySelectorAll(".jp-score-input").forEach(input => { const j_id = input.getAttribute("data-judge"); const c_name = input.getAttribute("data-crit"); const val = parseFloat(input.value) || 0; if(!scores[j_id]) scores[j_id] = {}; scores[j_id][c_name] = Math.max(0, Math.min(10, val)); }); const payload = { scores : scores, judge_weights : active_judge_weights, criteria_weights : active_criteria_weights }; judge_pole.db.save_votes(selected_p_id, payload); close_modal(); render_rankings(); } }, judge_pole.i18n.get("btn_save_scores")] ]])[0]; modal_body.appendChild(project_select); modal_body.appendChild(judges_picker); modal_body.appendChild(criteria_picker); modal_body.appendChild(matrix_container); modal_body.appendChild(actions); rebuild_matrix(); judge_pole.item_self.appendChild(modal_overlay); }; }; return VotesView; })();