785 lines
39 KiB
JavaScript
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;
|
|
})(); |