JudgePole/Public/ecma/Views/VotesView.ecma.js
2026-09-20 17:12:09 +02:00

785 lines
39 KiB
JavaScript

"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;
})();