mirror of
https://github.com/napnap75/multiarch-docker-images.git
synced 2026-11-14 09:51:47 +01:00
feat(dashboard) review UI: static pages, Beszel-styled CSS and vanilla JS (US-30, US-32..36)
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
"use strict";
|
||||
|
||||
// Document list page (US-32, US-33): server-side sort, per-column typed
|
||||
// filters, status tabs, pagination, column chooser persisted in the
|
||||
// browser, bulk single-field edit through one standard PATCH per
|
||||
// document.
|
||||
|
||||
var state = {
|
||||
config: null,
|
||||
meta: [],
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
sort: null,
|
||||
columns: [],
|
||||
statusTab: "",
|
||||
filters: {},
|
||||
selected: {},
|
||||
docs: [],
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
counts: {}
|
||||
};
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function esc(v) { return String(v).replace(/'/g, "\\'"); }
|
||||
|
||||
function store(key, val) {
|
||||
if (val === undefined) {
|
||||
try {
|
||||
var raw = localStorage.getItem(key);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
|
||||
}
|
||||
|
||||
function fixedMeta() {
|
||||
return [
|
||||
{ name: "status", type: "status", fixed: true },
|
||||
{ name: "file_key", type: "text", fixed: true },
|
||||
{ name: "created", type: "date", fixed: true },
|
||||
{ name: "updated", type: "date", fixed: true }
|
||||
];
|
||||
}
|
||||
|
||||
function fieldMeta(name) {
|
||||
for (var i = 0; i < state.meta.length; i++) {
|
||||
if (state.meta[i].name === name) return state.meta[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
var r = await api("GET", "/api/archivist/config");
|
||||
if (r.status !== 200) return;
|
||||
state.config = r.data;
|
||||
state.meta = state.config.fields.map(function (f) {
|
||||
return { name: f.name, type: f.type, mandatory: !!f.mandatory };
|
||||
}).concat(fixedMeta());
|
||||
|
||||
var defaults = state.config.fields.filter(function (f) { return f.mandatory; })
|
||||
.map(function (f) { return f.name; }).concat(["status", "file_key"]);
|
||||
var savedCols = store("archivist.columns");
|
||||
state.columns = Array.isArray(savedCols) ? savedCols : defaults;
|
||||
var savedSort = store("archivist.sort");
|
||||
state.sort = savedSort && savedSort.field ? savedSort : null;
|
||||
|
||||
initTheme("themeBtn");
|
||||
buildChooser();
|
||||
bindBulk();
|
||||
renderTabs();
|
||||
await loadCounts();
|
||||
await loadDocs();
|
||||
}
|
||||
|
||||
function buildChooser() {
|
||||
var grid = $("colgrid");
|
||||
grid.innerHTML = "";
|
||||
state.meta.forEach(function (m) {
|
||||
var label = document.createElement("label");
|
||||
var cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = state.columns.indexOf(m.name) >= 0;
|
||||
cb.addEventListener("change", function () {
|
||||
var i = state.columns.indexOf(m.name);
|
||||
if (cb.checked && i < 0) state.columns.push(m.name);
|
||||
if (!cb.checked && i >= 0) state.columns.splice(i, 1);
|
||||
store("archivist.columns", state.columns);
|
||||
state.page = 1;
|
||||
loadDocs();
|
||||
});
|
||||
label.appendChild(cb);
|
||||
label.appendChild(document.createTextNode(" " + m.name));
|
||||
grid.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCounts() {
|
||||
var statuses = state.config.statuses || [];
|
||||
var results = await Promise.all(statuses.map(function (st) {
|
||||
return api("GET", "/api/collections/documents/records?perPage=1&fields=status&filter=" +
|
||||
encodeURIComponent("(status='" + esc(st) + "')"));
|
||||
}));
|
||||
statuses.forEach(function (st, i) {
|
||||
state.counts[st] = (results[i].data && results[i].data.totalItems) || 0;
|
||||
});
|
||||
renderTabs();
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
var tabs = $("tabs");
|
||||
tabs.innerHTML = "";
|
||||
var all = document.createElement("button");
|
||||
all.type = "button";
|
||||
all.className = "tab" + (state.statusTab === "" ? " active" : "");
|
||||
all.textContent = "Tous";
|
||||
all.addEventListener("click", function () {
|
||||
state.statusTab = "";
|
||||
state.page = 1;
|
||||
loadDocs();
|
||||
});
|
||||
tabs.appendChild(all);
|
||||
(state.config.statuses || []).forEach(function (st) {
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "tab" + (state.statusTab === st ? " active" : "");
|
||||
b.appendChild(document.createTextNode(st));
|
||||
var c = document.createElement("span");
|
||||
c.className = "count";
|
||||
c.textContent = state.counts[st] !== undefined ? String(state.counts[st]) : "";
|
||||
b.appendChild(c);
|
||||
b.addEventListener("click", function () {
|
||||
state.statusTab = st;
|
||||
state.page = 1;
|
||||
loadDocs();
|
||||
});
|
||||
tabs.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function buildFilterQuery() {
|
||||
var parts = [];
|
||||
if (state.statusTab) parts.push("(status='" + esc(state.statusTab) + "')");
|
||||
state.columns.forEach(function (name) {
|
||||
if (name === "status") return; // owned by the tabs
|
||||
var m = fieldMeta(name);
|
||||
var f = state.filters[name];
|
||||
if (!m || !f || m.type === "number") return;
|
||||
if (m.type === "date") {
|
||||
if (f.from) parts.push("(" + name + ">='" + esc(f.from) + "')");
|
||||
if (f.to) parts.push("(" + name + "<='" + esc(f.to) + "')");
|
||||
} else if (m.type === "boolean") {
|
||||
if (f.value === "true" || f.value === "false") parts.push("(" + name + "=" + f.value + ")");
|
||||
} else if (m.type === "enum") {
|
||||
if (f.value) parts.push("(" + name + "='" + esc(f.value) + "')");
|
||||
} else {
|
||||
if (f.value) parts.push("(" + name + "~'" + esc(f.value) + "')");
|
||||
}
|
||||
});
|
||||
if (!parts.length) return "";
|
||||
return "&filter=" + encodeURIComponent(parts.join(" && "));
|
||||
}
|
||||
|
||||
async function loadDocs() {
|
||||
var sort = state.sort
|
||||
? ("&sort=" + (state.sort.dir === "desc" ? "-" : "") + encodeURIComponent(state.sort.field))
|
||||
: "";
|
||||
var q = "/api/collections/documents/records?page=" + state.page +
|
||||
"&perPage=" + state.perPage + sort + buildFilterQuery();
|
||||
var r = await api("GET", q);
|
||||
if (r.status !== 200) return;
|
||||
state.docs = r.data.items || [];
|
||||
state.total = r.data.totalItems || 0;
|
||||
state.totalPages = r.data.totalPages || 1;
|
||||
renderHead();
|
||||
renderRows();
|
||||
renderPager();
|
||||
renderBulkbar();
|
||||
}
|
||||
|
||||
function renderHead() {
|
||||
var thead = $("doctable").tHead;
|
||||
thead.innerHTML = "";
|
||||
var trh = document.createElement("tr");
|
||||
var thAll = document.createElement("th");
|
||||
var cbAll = document.createElement("input");
|
||||
cbAll.type = "checkbox";
|
||||
cbAll.addEventListener("change", function () {
|
||||
state.docs.forEach(function (d) {
|
||||
if (cbAll.checked) state.selected[d.id] = true;
|
||||
else delete state.selected[d.id];
|
||||
});
|
||||
renderRows();
|
||||
renderBulkbar();
|
||||
});
|
||||
thAll.appendChild(cbAll);
|
||||
trh.appendChild(thAll);
|
||||
state.columns.forEach(function (name) {
|
||||
var th = document.createElement("th");
|
||||
th.textContent = name + (state.sort && state.sort.field === name
|
||||
? (state.sort.dir === "desc" ? " ↓" : " ↑") : "");
|
||||
th.addEventListener("click", function () {
|
||||
if (!state.sort || state.sort.field !== name) {
|
||||
state.sort = { field: name, dir: "asc" };
|
||||
} else if (state.sort.dir === "asc") {
|
||||
state.sort.dir = "desc";
|
||||
} else {
|
||||
state.sort = null;
|
||||
}
|
||||
store("archivist.sort", state.sort);
|
||||
state.page = 1;
|
||||
loadDocs();
|
||||
});
|
||||
trh.appendChild(th);
|
||||
});
|
||||
thead.appendChild(trh);
|
||||
|
||||
var trf = document.createElement("tr");
|
||||
trf.appendChild(document.createElement("td"));
|
||||
state.columns.forEach(function (name) {
|
||||
var td = document.createElement("td");
|
||||
if (name !== "status") {
|
||||
var m = fieldMeta(name);
|
||||
if (m && m.type !== "number") {
|
||||
td.appendChild(buildFilterControl(m));
|
||||
}
|
||||
}
|
||||
trf.appendChild(td);
|
||||
});
|
||||
thead.appendChild(trf);
|
||||
}
|
||||
|
||||
function buildFilterControl(m) {
|
||||
var f = state.filters[m.name] || (state.filters[m.name] = {});
|
||||
var wrap = document.createElement("span");
|
||||
if (m.type === "date") {
|
||||
var from = document.createElement("input");
|
||||
from.type = "date";
|
||||
from.value = f.from || "";
|
||||
var to = document.createElement("input");
|
||||
to.type = "date";
|
||||
to.value = f.to || "";
|
||||
from.addEventListener("change", function () { f.from = from.value; state.page = 1; loadDocs(); });
|
||||
to.addEventListener("change", function () { f.to = to.value; state.page = 1; loadDocs(); });
|
||||
wrap.appendChild(from);
|
||||
wrap.appendChild(to);
|
||||
} else if (m.type === "boolean") {
|
||||
var sel = document.createElement("select");
|
||||
addOption(sel, "", "—");
|
||||
addOption(sel, "true", "oui");
|
||||
addOption(sel, "false", "non");
|
||||
sel.value = f.value || "";
|
||||
sel.addEventListener("change", function () { f.value = sel.value; state.page = 1; loadDocs(); });
|
||||
wrap.appendChild(sel);
|
||||
} else if (m.type === "enum") {
|
||||
var es = document.createElement("select");
|
||||
addOption(es, "", "—");
|
||||
((state.config.enums || {})[m.name] || []).forEach(function (v) { addOption(es, v, v); });
|
||||
es.value = f.value || "";
|
||||
es.addEventListener("change", function () { f.value = es.value; state.page = 1; loadDocs(); });
|
||||
wrap.appendChild(es);
|
||||
} else {
|
||||
var inp = document.createElement("input");
|
||||
inp.type = "text";
|
||||
inp.placeholder = "contient…";
|
||||
inp.value = f.value || "";
|
||||
var timer = null;
|
||||
inp.addEventListener("input", function () {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(function () { f.value = inp.value; state.page = 1; loadDocs(); }, 300);
|
||||
});
|
||||
wrap.appendChild(inp);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderRows() {
|
||||
var tbody = $("doctable").tBodies[0];
|
||||
tbody.innerHTML = "";
|
||||
state.docs.forEach(function (doc) {
|
||||
var tr = document.createElement("tr");
|
||||
tr.className = "clickable";
|
||||
var tdcb = document.createElement("td");
|
||||
var cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = !!state.selected[doc.id];
|
||||
cb.addEventListener("click", function (ev) { ev.stopPropagation(); });
|
||||
cb.addEventListener("change", function () {
|
||||
if (cb.checked) state.selected[doc.id] = true;
|
||||
else delete state.selected[doc.id];
|
||||
renderBulkbar();
|
||||
});
|
||||
tdcb.appendChild(cb);
|
||||
tr.appendChild(tdcb);
|
||||
state.columns.forEach(function (name) {
|
||||
var m = fieldMeta(name);
|
||||
var td = document.createElement("td");
|
||||
if (name === "status") {
|
||||
var b = document.createElement("span");
|
||||
b.className = "badge st-" + doc.status;
|
||||
b.textContent = doc.status;
|
||||
td.appendChild(b);
|
||||
} else if (m && m.type === "date") {
|
||||
td.textContent = fmtDate(doc[name]);
|
||||
} else {
|
||||
td.textContent = fmtVal(doc[name]);
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tr.addEventListener("click", function (ev) {
|
||||
if (ev.target.tagName === "INPUT") return;
|
||||
window.location.href = "/document.html?id=" + doc.id;
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPager() {
|
||||
var pager = $("pager");
|
||||
pager.innerHTML = "";
|
||||
var prev = document.createElement("button");
|
||||
prev.type = "button";
|
||||
prev.className = "btn secondary";
|
||||
prev.textContent = "‹ Précédent";
|
||||
prev.disabled = state.page <= 1;
|
||||
prev.addEventListener("click", function () { state.page--; loadDocs(); });
|
||||
var info = document.createElement("span");
|
||||
info.textContent = state.total + " document(s) — page " + state.page + " / " + Math.max(1, state.totalPages);
|
||||
var next = document.createElement("button");
|
||||
next.type = "button";
|
||||
next.className = "btn secondary";
|
||||
next.textContent = "Suivant ›";
|
||||
next.disabled = state.page >= state.totalPages;
|
||||
next.addEventListener("click", function () { state.page++; loadDocs(); });
|
||||
pager.appendChild(prev);
|
||||
pager.appendChild(info);
|
||||
pager.appendChild(next);
|
||||
}
|
||||
|
||||
function bindBulk() {
|
||||
var sel = $("bulkfield");
|
||||
sel.innerHTML = "";
|
||||
state.config.fields.forEach(function (f) { addOption(sel, f.name, f.name); });
|
||||
sel.addEventListener("change", rebuildBulkValue);
|
||||
$("bulkapply").addEventListener("click", applyBulk);
|
||||
$("bulkclear").addEventListener("click", function () {
|
||||
state.selected = {};
|
||||
renderRows();
|
||||
renderBulkbar();
|
||||
});
|
||||
$("bulkall").addEventListener("click", selectAllFiltered);
|
||||
rebuildBulkValue();
|
||||
}
|
||||
|
||||
function bulkField() {
|
||||
var name = $("bulkfield").value;
|
||||
for (var i = 0; i < state.config.fields.length; i++) {
|
||||
if (state.config.fields[i].name === name) return state.config.fields[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rebuildBulkValue() {
|
||||
var holder = $("bulkvalue");
|
||||
holder.innerHTML = "";
|
||||
var f = bulkField();
|
||||
if (!f) return;
|
||||
holder.appendChild(buildValueControl(f, (state.config.enums || {})[f.name] || [], null));
|
||||
}
|
||||
|
||||
async function selectAllFiltered() {
|
||||
var page = 1;
|
||||
for (;;) {
|
||||
var q = "/api/collections/documents/records?page=" + page + "&perPage=200&fields=id" + buildFilterQuery();
|
||||
var r = await api("GET", q);
|
||||
if (r.status !== 200) break;
|
||||
(r.data.items || []).forEach(function (d) { state.selected[d.id] = true; });
|
||||
if (page >= (r.data.totalPages || 1)) break;
|
||||
page++;
|
||||
}
|
||||
renderRows();
|
||||
renderBulkbar();
|
||||
}
|
||||
|
||||
async function applyBulk() {
|
||||
var f = bulkField();
|
||||
if (!f) { alert("Choisissez un champ."); return; }
|
||||
var el = $("bulkvalue").firstChild;
|
||||
var value;
|
||||
try {
|
||||
var r = readValueControl(f, el);
|
||||
if (r.clear) {
|
||||
if (f.mandatory) { alert("Ce champ est obligatoire : une valeur est requise."); return; }
|
||||
value = null;
|
||||
} else {
|
||||
value = r.value;
|
||||
}
|
||||
} catch (e) { alert(e.message); return; }
|
||||
var ids = Object.keys(state.selected);
|
||||
if (!ids.length) { alert("Aucun document sélectionné."); return; }
|
||||
$("bulkresult").textContent = "…";
|
||||
var ok = 0;
|
||||
var failed = [];
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var body = {};
|
||||
body[f.name] = value;
|
||||
var res = await api("PATCH", "/api/collections/documents/records/" + ids[i], body);
|
||||
if (res.status === 200) ok++;
|
||||
else failed.push(ids[i] + " : " + errMsg(res));
|
||||
}
|
||||
$("bulkresult").textContent = ok + " document(s) modifié(s)" +
|
||||
(failed.length ? ", " + failed.length + " échec(s) — " + failed[0] : "");
|
||||
state.selected = {};
|
||||
await loadCounts();
|
||||
await loadDocs();
|
||||
}
|
||||
|
||||
function renderBulkbar() {
|
||||
var n = Object.keys(state.selected).length;
|
||||
$("bulkbar").hidden = n === 0;
|
||||
$("bulkcount").textContent = n + " document(s) sélectionné(s)";
|
||||
}
|
||||
|
||||
init();
|
||||
Reference in New Issue
Block a user