219 lines
9.4 KiB
JavaScript
219 lines
9.4 KiB
JavaScript
(async function () {
|
|
const root = document.getElementById("app");
|
|
const user = await bootstrapAuth({ requireAuth: true, forbidIfMustChange: true });
|
|
if (!user) return;
|
|
|
|
const state = { competitions: [], users: [] };
|
|
|
|
async function loadCompetitions() {
|
|
state.competitions = await API.listCompetitions();
|
|
}
|
|
async function loadUsers() {
|
|
if (user.is_system_admin) state.users = await API.listUsers();
|
|
}
|
|
|
|
async function openCompetitionModal() {
|
|
const backdrop = el("div", { class: "modal-backdrop",
|
|
onclick: (e) => { if (e.target === backdrop) backdrop.remove(); } });
|
|
const nameInput = el("input", { type: "text" });
|
|
const allowInput = el("input", { type: "checkbox" });
|
|
let langs = I18N_AVAILABLE;
|
|
try {
|
|
const fromApi = await API.listRuleLanguages();
|
|
if (fromApi && fromApi.length) langs = [...fromApi].sort();
|
|
} catch (e) {}
|
|
const defaultLang = langs.includes(user.language) ? user.language : "en";
|
|
const langInput = el("select", null,
|
|
...langs.map((l) => el("option", { value: l, selected: l === defaultLang },
|
|
I18N_NAMES[l] || l))
|
|
);
|
|
backdrop.appendChild(el("div", { class: "modal" },
|
|
el("h3", null, t("new_competition")),
|
|
el("div", { class: "field" }, el("label", null, t("competition_name")), nameInput),
|
|
el("div", { class: "field" }, el("label", null, t("rules_language")), langInput,
|
|
el("div", { class: "muted small" }, t("rules_language_hint"))),
|
|
el("label", { class: "row" }, allowInput, " " + t("allow_any_scorer_edit")),
|
|
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
|
|
el("button", { onclick: () => backdrop.remove() }, t("cancel")),
|
|
el("button", { class: "primary", onclick: async () => {
|
|
if (!nameInput.value.trim()) return;
|
|
try {
|
|
await API.createCompetition({
|
|
name: nameInput.value.trim(),
|
|
allow_any_scorer_edit: allowInput.checked,
|
|
rules_language: langInput.value,
|
|
});
|
|
backdrop.remove();
|
|
await loadCompetitions();
|
|
render();
|
|
} catch (e) { alert(e.message); }
|
|
} }, t("create")),
|
|
),
|
|
));
|
|
document.body.appendChild(backdrop);
|
|
}
|
|
|
|
function openUserModal() {
|
|
const backdrop = el("div", { class: "modal-backdrop",
|
|
onclick: (e) => { if (e.target === backdrop) backdrop.remove(); } });
|
|
const username = el("input", { type: "text",
|
|
oninput: (e) => { e.target.value = e.target.value.toLowerCase(); } });
|
|
const password = el("input", { type: "password" });
|
|
const displayName = el("input", { type: "text" });
|
|
const langSelect = el("select", null,
|
|
...I18N_AVAILABLE.map((l) => el("option", { value: l }, I18N_NAMES[l]))
|
|
);
|
|
const isAdmin = el("input", { type: "checkbox" });
|
|
backdrop.appendChild(el("div", { class: "modal" },
|
|
el("h3", null, t("add_user")),
|
|
el("div", { class: "field" }, el("label", null, t("username")), username),
|
|
el("div", { class: "field" }, el("label", null, t("password")), password),
|
|
el("div", { class: "field" }, el("label", null, t("display_name")), displayName),
|
|
el("div", { class: "field" }, el("label", null, t("language")), langSelect),
|
|
el("label", { class: "row" }, isAdmin, " " + t("is_admin")),
|
|
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
|
|
el("button", { onclick: () => backdrop.remove() }, t("cancel")),
|
|
el("button", { class: "primary", onclick: async () => {
|
|
if (!username.value.trim() || !password.value) return;
|
|
try {
|
|
await API.createUser({
|
|
username: username.value.trim().toLowerCase(),
|
|
password: password.value,
|
|
display_name: displayName.value,
|
|
language: langSelect.value,
|
|
is_system_admin: isAdmin.checked,
|
|
});
|
|
backdrop.remove();
|
|
await loadUsers();
|
|
render();
|
|
} catch (err) { alert(err.message); }
|
|
} }, t("create")),
|
|
),
|
|
));
|
|
document.body.appendChild(backdrop);
|
|
}
|
|
|
|
function renderUsersAdmin() {
|
|
const card = el("div", { class: "card" });
|
|
card.appendChild(el("div", { class: "row", style: { justifyContent: "space-between", marginBottom: "0.5rem" } },
|
|
el("h2", { style: { margin: 0 } }, t("users")),
|
|
el("button", { onclick: openUserModal }, t("add_user")),
|
|
));
|
|
if (state.users.length === 0) {
|
|
card.appendChild(el("div", { class: "muted" }, "—"));
|
|
return card;
|
|
}
|
|
const table = el("table");
|
|
table.appendChild(el("thead", null,
|
|
el("tr", null,
|
|
el("th", null, t("username")),
|
|
el("th", null, t("display_name")),
|
|
el("th", null, t("language")),
|
|
el("th", null, t("is_admin")),
|
|
el("th", null, t("must_change_password")),
|
|
el("th", null, t("actions")),
|
|
)
|
|
));
|
|
const tbody = el("tbody");
|
|
for (const u of state.users) {
|
|
const row = el("tr", null,
|
|
el("td", null, u.username),
|
|
el("td", null, u.display_name),
|
|
el("td", null, I18N_NAMES[u.language] || u.language),
|
|
el("td", null, u.is_system_admin ? t("yes") : t("no")),
|
|
el("td", null, u.must_change_password ? el("span", { class: "badge warn" }, t("yes")) : t("no")),
|
|
el("td", null,
|
|
el("button", { class: "action-btn", onclick: () => openEditUserModal(u) }, t("edit")),
|
|
!u.must_change_password && el("button", { class: "action-btn", onclick: async () => {
|
|
if (!confirm(t("confirm_force_password"))) return;
|
|
await API.updateUser(u.id, { must_change_password: true });
|
|
await loadUsers();
|
|
render();
|
|
} }, t("force_password_change")),
|
|
u.id !== user.id && el("button", { class: "action-btn danger", onclick: async () => {
|
|
if (!confirm(t("confirm_delete"))) return;
|
|
await API.deleteUser(u.id);
|
|
await loadUsers();
|
|
render();
|
|
} }, t("delete")),
|
|
),
|
|
);
|
|
tbody.appendChild(row);
|
|
}
|
|
table.appendChild(tbody);
|
|
card.appendChild(el("div", { class: "table-wrap" }, table));
|
|
return card;
|
|
}
|
|
|
|
function openEditUserModal(u) {
|
|
const backdrop = el("div", { class: "modal-backdrop",
|
|
onclick: (e) => { if (e.target === backdrop) backdrop.remove(); } });
|
|
const username = el("input", { type: "text", value: u.username,
|
|
oninput: (e) => { e.target.value = e.target.value.toLowerCase(); } });
|
|
const displayName = el("input", { type: "text", value: u.display_name || "" });
|
|
const password = el("input", { type: "password", placeholder: t("leave_blank_keep") });
|
|
const isAdmin = el("input", { type: "checkbox", checked: !!u.is_system_admin });
|
|
backdrop.appendChild(el("div", { class: "modal" },
|
|
el("h3", null, t("edit") + ": " + u.username),
|
|
el("div", { class: "field" }, el("label", null, t("username")), username),
|
|
el("div", { class: "field" }, el("label", null, t("display_name")), displayName),
|
|
el("div", { class: "field" }, el("label", null, t("new_password")), password),
|
|
u.id !== user.id && el("label", { class: "row" }, isAdmin, " " + t("is_admin")),
|
|
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
|
|
el("button", { onclick: () => backdrop.remove() }, t("cancel")),
|
|
el("button", { class: "primary", onclick: async () => {
|
|
const body = {};
|
|
const newUsername = username.value.trim().toLowerCase();
|
|
if (newUsername && newUsername !== u.username) body.username = newUsername;
|
|
if (displayName.value !== (u.display_name || "")) body.display_name = displayName.value;
|
|
if (password.value) body.password = password.value;
|
|
if (u.id !== user.id && isAdmin.checked !== !!u.is_system_admin) body.is_system_admin = isAdmin.checked;
|
|
if (Object.keys(body).length === 0) { backdrop.remove(); return; }
|
|
try {
|
|
await API.updateUser(u.id, body);
|
|
backdrop.remove();
|
|
await loadUsers();
|
|
render();
|
|
} catch (e) { alert((e.data && e.data.error) || e.message); }
|
|
} }, t("save")),
|
|
),
|
|
));
|
|
document.body.appendChild(backdrop);
|
|
}
|
|
|
|
function render() {
|
|
clearNode(root);
|
|
root.appendChild(renderTopbar(user));
|
|
const container = el("div", { class: "container" });
|
|
container.appendChild(el("div", { class: "row", style: { justifyContent: "space-between", marginBottom: "0.75rem" } },
|
|
el("h2", { style: { margin: 0 } }, t("competitions")),
|
|
user.is_system_admin && el("button", { class: "primary", onclick: openCompetitionModal }, t("new_competition")),
|
|
));
|
|
|
|
if (state.competitions.length === 0) {
|
|
container.appendChild(el("div", { class: "muted" }, t("no_competitions")));
|
|
} else {
|
|
const grid = el("div", { class: "grid" });
|
|
for (const c of state.competitions) {
|
|
grid.appendChild(el("div", { class: "card" },
|
|
el("h2", null, c.name),
|
|
el("div", { class: "muted", style: { marginBottom: "0.5rem" } },
|
|
el("span", { class: "badge accent" }, t(c.role)),
|
|
),
|
|
el("button", { class: "primary", onclick: () => navigate("competition", { id: c.id }) }, t("open")),
|
|
));
|
|
}
|
|
container.appendChild(grid);
|
|
}
|
|
|
|
if (user.is_system_admin) {
|
|
container.appendChild(renderUsersAdmin());
|
|
}
|
|
root.appendChild(container);
|
|
}
|
|
|
|
await loadCompetitions();
|
|
await loadUsers();
|
|
render();
|
|
})();
|