Spaces:
Running
Running
Update index.html
Browse files- index.html +143 -2
index.html
CHANGED
|
@@ -966,6 +966,20 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 966 |
<div class="panel-b"><div class="dist" id="dist"></div></div>
|
| 967 |
</div>
|
| 968 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
<div class="panel">
|
| 970 |
<div class="tools">
|
| 971 |
<div class="search"><input type="search" id="search" placeholder="filter rows…"></div>
|
|
@@ -976,6 +990,14 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 976 |
<option>Account</option>
|
| 977 |
<option>Art / Other</option>
|
| 978 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 979 |
<a class="link hidden" id="sheetLink" href="#" target="_blank" rel="noopener">OPEN SHEET →</a>
|
| 980 |
</div>
|
| 981 |
<div class="table-wrap">
|
|
@@ -1104,11 +1126,120 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 1104 |
if (x.includes("account")) return "a";
|
| 1105 |
return "o";
|
| 1106 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
function show(data) {
|
| 1108 |
$("empty").classList.add("hidden");
|
| 1109 |
$("results").classList.remove("hidden");
|
| 1110 |
const list = data.rows || data.rows_preview || [];
|
| 1111 |
rows = list;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
$("kpiTotal").textContent = data.total ?? list.length;
|
| 1113 |
$("kpiOk").textContent = data.ok ?? 0;
|
| 1114 |
$("kpiFail").textContent = data.failed ?? 0;
|
|
@@ -1131,7 +1262,10 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 1131 |
$("sheetLink").href = data.sheet_url;
|
| 1132 |
$("sheetLink").classList.remove("hidden");
|
| 1133 |
} else $("sheetLink").classList.add("hidden");
|
| 1134 |
-
|
|
|
|
|
|
|
|
|
|
| 1135 |
let all = data.columns || (list[0] ? Object.keys(list[0]) : []);
|
| 1136 |
const ordered = [];
|
| 1137 |
prefer.forEach((c) => {
|
|
@@ -1139,14 +1273,17 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 1139 |
if (real && !ordered.includes(real)) ordered.push(real);
|
| 1140 |
});
|
| 1141 |
all.forEach((c) => { if (!ordered.includes(c)) ordered.push(c); });
|
| 1142 |
-
cols = ordered.slice(0,
|
| 1143 |
filter();
|
| 1144 |
}
|
|
|
|
| 1145 |
function filter() {
|
| 1146 |
const q = ($("search").value || "").toLowerCase().trim();
|
| 1147 |
const stream = $("streamFilter").value;
|
|
|
|
| 1148 |
let list = rows;
|
| 1149 |
if (stream) list = list.filter((r) => (r["Recommended Stream"] || "") === stream);
|
|
|
|
| 1150 |
if (q) list = list.filter((r) => cols.some((c) => String(r[c] ?? "").toLowerCase().includes(q)));
|
| 1151 |
$("thead").innerHTML = "<tr>" + cols.map((c) => "<th>" + esc(c) + "</th>").join("") + "</tr>";
|
| 1152 |
$("tbody").innerHTML = list.length ? list.map((r) => {
|
|
@@ -1159,6 +1296,9 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 1159 |
if (c === "Recommended Stream") {
|
| 1160 |
return '<td><span class="tag ' + streamTag(String(v)) + '">' + esc(String(v)) + "</span></td>";
|
| 1161 |
}
|
|
|
|
|
|
|
|
|
|
| 1162 |
if (c === "Confidence") {
|
| 1163 |
return '<td><span class="tag ' + confTag(String(v)) + '">' + esc(String(v)) + "</span></td>";
|
| 1164 |
}
|
|
@@ -1170,6 +1310,7 @@ tbody tr:last-child td { border-bottom: none; }
|
|
| 1170 |
}
|
| 1171 |
$("search").addEventListener("input", filter);
|
| 1172 |
$("streamFilter").addEventListener("change", filter);
|
|
|
|
| 1173 |
async function runCsv() {
|
| 1174 |
if (!file) return;
|
| 1175 |
load(true);
|
|
|
|
| 966 |
<div class="panel-b"><div class="dist" id="dist"></div></div>
|
| 967 |
</div>
|
| 968 |
|
| 969 |
+
<div class="panel">
|
| 970 |
+
<div class="panel-h">
|
| 971 |
+
<h3>CLASS ALLOCATION ROSTERS (4A · 4B · 4C · 4D · SASTERA)</h3>
|
| 972 |
+
</div>
|
| 973 |
+
<div class="panel-b">
|
| 974 |
+
<div class="dist" style="grid-template-columns: repeat(5, 1fr);" id="classDist">
|
| 975 |
+
<!-- Class counters rendered here -->
|
| 976 |
+
</div>
|
| 977 |
+
<div id="classTables" style="display: flex; flex-direction: column; gap: 20px; margin-top: 16px;">
|
| 978 |
+
<!-- Tables for 4A, 4B, 4C, 4D, SASTERA will be injected here -->
|
| 979 |
+
</div>
|
| 980 |
+
</div>
|
| 981 |
+
</div>
|
| 982 |
+
|
| 983 |
<div class="panel">
|
| 984 |
<div class="tools">
|
| 985 |
<div class="search"><input type="search" id="search" placeholder="filter rows…"></div>
|
|
|
|
| 990 |
<option>Account</option>
|
| 991 |
<option>Art / Other</option>
|
| 992 |
</select>
|
| 993 |
+
<select class="select" id="classFilter">
|
| 994 |
+
<option value="">ALL CLASSES</option>
|
| 995 |
+
<option value="4A">4A (Science Top)</option>
|
| 996 |
+
<option value="4B">4B (Science)</option>
|
| 997 |
+
<option value="4C">4C (Account)</option>
|
| 998 |
+
<option value="4D">4D (Reka Cipta)</option>
|
| 999 |
+
<option value="SASTERA">SASTERA</option>
|
| 1000 |
+
</select>
|
| 1001 |
<a class="link hidden" id="sheetLink" href="#" target="_blank" rel="noopener">OPEN SHEET →</a>
|
| 1002 |
</div>
|
| 1003 |
<div class="table-wrap">
|
|
|
|
| 1126 |
if (x.includes("account")) return "a";
|
| 1127 |
return "o";
|
| 1128 |
}
|
| 1129 |
+
|
| 1130 |
+
function assignClasses(list) {
|
| 1131 |
+
// Collect Science students and sort by Science Probability descending & Confidence
|
| 1132 |
+
const scienceStudents = [];
|
| 1133 |
+
const confRank = { "high": 3, "medium": 2, "borderline": 1 };
|
| 1134 |
+
|
| 1135 |
+
list.forEach((r) => {
|
| 1136 |
+
const stream = (r["Recommended Stream"] || "").trim();
|
| 1137 |
+
const prob = parseFloat(r["Prob Science %"]) || 0;
|
| 1138 |
+
const conf = (r["Confidence"] || "").toLowerCase().trim();
|
| 1139 |
+
|
| 1140 |
+
if (stream === "Science") {
|
| 1141 |
+
scienceStudents.push({ row: r, prob, confScore: confRank[conf] || 0 });
|
| 1142 |
+
} else if (stream === "Account") {
|
| 1143 |
+
r["Assigned Class"] = "4C";
|
| 1144 |
+
} else if (stream === "Reka Cipta") {
|
| 1145 |
+
r["Assigned Class"] = "4D";
|
| 1146 |
+
} else {
|
| 1147 |
+
r["Assigned Class"] = "SASTERA";
|
| 1148 |
+
}
|
| 1149 |
+
});
|
| 1150 |
+
|
| 1151 |
+
// Sort science candidates: Highest probability and highest confidence first
|
| 1152 |
+
scienceStudents.sort((a, b) => {
|
| 1153 |
+
if (b.prob !== a.prob) return b.prob - a.prob;
|
| 1154 |
+
return b.confScore - a.confScore;
|
| 1155 |
+
});
|
| 1156 |
+
|
| 1157 |
+
// Equally distribute Science candidates between 4A (Top) and 4B
|
| 1158 |
+
const half = Math.ceil(scienceStudents.length / 2);
|
| 1159 |
+
scienceStudents.forEach((item, idx) => {
|
| 1160 |
+
item.row["Assigned Class"] = idx < half ? "4A" : "4B";
|
| 1161 |
+
});
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
function renderClassRosters() {
|
| 1165 |
+
const classNames = ["4A", "4B", "4C", "4D", "SASTERA"];
|
| 1166 |
+
const classMeta = {
|
| 1167 |
+
"4A": { title: "Class 4A — Science (High Confidence / Top)", badge: "tag-s" },
|
| 1168 |
+
"4B": { title: "Class 4B — Science", badge: "tag-s" },
|
| 1169 |
+
"4C": { title: "Class 4C — Account", badge: "tag-a" },
|
| 1170 |
+
"4D": { title: "Class 4D — Reka Cipta", badge: "tag-r" },
|
| 1171 |
+
"SASTERA": { title: "Class SASTERA — Art / Humanities", badge: "tag-o" }
|
| 1172 |
+
};
|
| 1173 |
+
|
| 1174 |
+
const counts = { "4A": 0, "4B": 0, "4C": 0, "4D": 0, "SASTERA": 0 };
|
| 1175 |
+
rows.forEach(r => {
|
| 1176 |
+
const c = r["Assigned Class"];
|
| 1177 |
+
if (counts[c] !== undefined) counts[c]++;
|
| 1178 |
+
});
|
| 1179 |
+
|
| 1180 |
+
// Render KPI bars for class distribution
|
| 1181 |
+
$("classDist").innerHTML = classNames.map(c => {
|
| 1182 |
+
const cnt = counts[c];
|
| 1183 |
+
const pct = rows.length ? Math.round((cnt / rows.length) * 100) : 0;
|
| 1184 |
+
return '<div class="dist-item"><div class="name">' + c + ' <b>' + cnt + '</b></div>' +
|
| 1185 |
+
'<div class="dist-bar"><i class="' + barClass(c === "4A" || c === "4B" ? "Science" : (c === "4C" ? "Account" : (c === "4D" ? "Reka" : "Art"))) + '" style="width:' + pct + '%"></i></div></div>';
|
| 1186 |
+
}).join("");
|
| 1187 |
+
|
| 1188 |
+
// Render 5 tables
|
| 1189 |
+
$("classTables").innerHTML = classNames.map(c => {
|
| 1190 |
+
const members = rows.filter(r => r["Assigned Class"] === c);
|
| 1191 |
+
const meta = classMeta[c];
|
| 1192 |
+
|
| 1193 |
+
let rowsHtml = members.map((m, idx) => `
|
| 1194 |
+
<tr>
|
| 1195 |
+
<td>${idx + 1}</td>
|
| 1196 |
+
<td><b>${esc(m["Name"] || m["Nama"] || "Student " + (idx + 1))}</b></td>
|
| 1197 |
+
<td><span class="tag ${streamTag(m["Recommended Stream"])}">${esc(m["Recommended Stream"] || "")}</span></td>
|
| 1198 |
+
<td><span class="tag ${confTag(m["Confidence"])}">${esc(m["Confidence"] || "N/A")}</span></td>
|
| 1199 |
+
<td>${esc(m["Prob Science %"] ? m["Prob Science %"] + "%" : "-")}</td>
|
| 1200 |
+
</tr>
|
| 1201 |
+
`).join("");
|
| 1202 |
+
|
| 1203 |
+
if (!members.length) {
|
| 1204 |
+
rowsHtml = `<tr><td colspan="5" style="text-align:center; color: var(--text-3); padding: 12px;">No students assigned to ${c}</td></tr>`;
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
return `
|
| 1208 |
+
<div style="border: 1px solid var(--line); background: var(--panel-2);">
|
| 1209 |
+
<div style="padding: 10px 14px; border-bottom: 1px solid var(--line); display:flex; justify-content:space-between; align-items:center;">
|
| 1210 |
+
<strong style="font-family: var(--mono); font-size: 12px; color: var(--cyan); letter-spacing: 0.08em;">${meta.title}</strong>
|
| 1211 |
+
<span class="tag ${meta.badge}">${members.length} Students</span>
|
| 1212 |
+
</div>
|
| 1213 |
+
<div style="max-height: 220px; overflow-y: auto;">
|
| 1214 |
+
<table>
|
| 1215 |
+
<thead>
|
| 1216 |
+
<tr>
|
| 1217 |
+
<th style="width: 40px;">#</th>
|
| 1218 |
+
<th>Student Name</th>
|
| 1219 |
+
<th>Stream</th>
|
| 1220 |
+
<th>Confidence</th>
|
| 1221 |
+
<th>Science %</th>
|
| 1222 |
+
</tr>
|
| 1223 |
+
</thead>
|
| 1224 |
+
<tbody>
|
| 1225 |
+
${rowsHtml}
|
| 1226 |
+
</tbody>
|
| 1227 |
+
</table>
|
| 1228 |
+
</div>
|
| 1229 |
+
</div>
|
| 1230 |
+
`;
|
| 1231 |
+
}).join("");
|
| 1232 |
+
}
|
| 1233 |
+
|
| 1234 |
function show(data) {
|
| 1235 |
$("empty").classList.add("hidden");
|
| 1236 |
$("results").classList.remove("hidden");
|
| 1237 |
const list = data.rows || data.rows_preview || [];
|
| 1238 |
rows = list;
|
| 1239 |
+
|
| 1240 |
+
// Apply 5-class assignment logic
|
| 1241 |
+
assignClasses(rows);
|
| 1242 |
+
|
| 1243 |
$("kpiTotal").textContent = data.total ?? list.length;
|
| 1244 |
$("kpiOk").textContent = data.ok ?? 0;
|
| 1245 |
$("kpiFail").textContent = data.failed ?? 0;
|
|
|
|
| 1262 |
$("sheetLink").href = data.sheet_url;
|
| 1263 |
$("sheetLink").classList.remove("hidden");
|
| 1264 |
} else $("sheetLink").classList.add("hidden");
|
| 1265 |
+
|
| 1266 |
+
renderClassRosters();
|
| 1267 |
+
|
| 1268 |
+
const prefer = ["Name","Nama","Class","Kelas","Assigned Class","Recommended Stream","Second Stream","Confidence","Prob Science %","Prob Reka Cipta %","Prob Account %","Prob Art / Other %","Key Subjects","Status"];
|
| 1269 |
let all = data.columns || (list[0] ? Object.keys(list[0]) : []);
|
| 1270 |
const ordered = [];
|
| 1271 |
prefer.forEach((c) => {
|
|
|
|
| 1273 |
if (real && !ordered.includes(real)) ordered.push(real);
|
| 1274 |
});
|
| 1275 |
all.forEach((c) => { if (!ordered.includes(c)) ordered.push(c); });
|
| 1276 |
+
cols = ordered.slice(0, 13);
|
| 1277 |
filter();
|
| 1278 |
}
|
| 1279 |
+
|
| 1280 |
function filter() {
|
| 1281 |
const q = ($("search").value || "").toLowerCase().trim();
|
| 1282 |
const stream = $("streamFilter").value;
|
| 1283 |
+
const assignedClass = $("classFilter") ? $("classFilter").value : "";
|
| 1284 |
let list = rows;
|
| 1285 |
if (stream) list = list.filter((r) => (r["Recommended Stream"] || "") === stream);
|
| 1286 |
+
if (assignedClass) list = list.filter((r) => (r["Assigned Class"] || "") === assignedClass);
|
| 1287 |
if (q) list = list.filter((r) => cols.some((c) => String(r[c] ?? "").toLowerCase().includes(q)));
|
| 1288 |
$("thead").innerHTML = "<tr>" + cols.map((c) => "<th>" + esc(c) + "</th>").join("") + "</tr>";
|
| 1289 |
$("tbody").innerHTML = list.length ? list.map((r) => {
|
|
|
|
| 1296 |
if (c === "Recommended Stream") {
|
| 1297 |
return '<td><span class="tag ' + streamTag(String(v)) + '">' + esc(String(v)) + "</span></td>";
|
| 1298 |
}
|
| 1299 |
+
if (c === "Assigned Class") {
|
| 1300 |
+
return '<td><span class="tag tag-ok" style="font-weight:700;">' + esc(String(v)) + "</span></td>";
|
| 1301 |
+
}
|
| 1302 |
if (c === "Confidence") {
|
| 1303 |
return '<td><span class="tag ' + confTag(String(v)) + '">' + esc(String(v)) + "</span></td>";
|
| 1304 |
}
|
|
|
|
| 1310 |
}
|
| 1311 |
$("search").addEventListener("input", filter);
|
| 1312 |
$("streamFilter").addEventListener("change", filter);
|
| 1313 |
+
if ($("classFilter")) $("classFilter").addEventListener("change", filter);
|
| 1314 |
async function runCsv() {
|
| 1315 |
if (!file) return;
|
| 1316 |
load(true);
|