Fix review queue pagination and API fallback handling
This commit is contained in:
@@ -8,12 +8,22 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
|
||||
<form class="admin-login" autocomplete="off"><label>Административный токен<input name="token" type="password" required autocomplete="off" /></label><button type="submit">Открыть очередь</button></form>
|
||||
<p class="privacy">Токен остаётся только в памяти страницы. Исходная ссылка и происхождение сохраняются при публикации.</p>
|
||||
<div class="notice error" data-admin-error role="alert" hidden></div><div class="moderation-list" data-external-list aria-live="polite"></div>
|
||||
<nav data-pages aria-label="Страницы очереди" hidden>
|
||||
<button type="button" data-previous>Предыдущая</button>
|
||||
<span data-page-number aria-live="polite"></span>
|
||||
<button type="button" data-next>Следующая</button>
|
||||
</nav>
|
||||
</section>
|
||||
<script>
|
||||
const root = document.querySelector<HTMLElement>(".moderation-app");
|
||||
const login = document.querySelector<HTMLFormElement>(".admin-login");
|
||||
const list = document.querySelector<HTMLElement>("[data-external-list]");
|
||||
const error = document.querySelector<HTMLElement>("[data-admin-error]");
|
||||
const pages = document.querySelector<HTMLElement>("[data-pages]");
|
||||
const previous = document.querySelector<HTMLButtonElement>("[data-previous]");
|
||||
const next = document.querySelector<HTMLButtonElement>("[data-next]");
|
||||
const pageNumber = document.querySelector<HTMLElement>("[data-page-number]");
|
||||
let offset = 0;
|
||||
let token = ""; let fishes: Record<string, string>[] = []; let waters: Record<string, string>[] = [];
|
||||
const esc = (value: unknown) => String(value ?? "—").replace(/[&<>'"]/g, char => ({"&":"&","<":"<",">":">","'":"'",'"':"""}[char] ?? char));
|
||||
const fail = (message: string) => { if (error) { error.textContent = message; error.hidden = false; } };
|
||||
@@ -22,12 +32,28 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
|
||||
async function loadQueue() {
|
||||
if (!root || !list) return; error?.setAttribute("hidden", ""); list.innerHTML = '<div class="state"><h2>Загружаем staging…</h2></div>';
|
||||
[fishes, waters] = await Promise.all([json(`${root.dataset.apiUrl}/api/v1/fishes`), json(`${root.dataset.apiUrl}/api/v1/waterbodies`)]);
|
||||
const rows = await json(`${root.dataset.apiUrl}/api/v1/admin/external-observations?limit=200`, {headers:{Authorization:`Bearer ${token}`}});
|
||||
const pending = rows.filter((row: Record<string, unknown>) => !["published", "rejected"].includes(String(row.status)));
|
||||
pages?.setAttribute("hidden", "");
|
||||
const rows = await json(`${root.dataset.apiUrl}/api/v1/admin/external-observations?status=review&limit=51&offset=${offset}`, {headers:{Authorization:`Bearer ${token}`}});
|
||||
if (!rows.length && offset > 0) { offset = 0; return loadQueue(); }
|
||||
const pending = rows.slice(0, 50);
|
||||
if (pages) pages.hidden = !pending.length;
|
||||
if (previous) previous.disabled = offset === 0;
|
||||
if (next) next.disabled = rows.length <= 50;
|
||||
if (pageNumber) pageNumber.textContent = `Страница ${offset / 50 + 1}`;
|
||||
if (!pending.length) { list.innerHTML = '<div class="state"><h2>Очередь пуста</h2><p>Все внешние записи обработаны.</p></div>'; return; }
|
||||
list.innerHTML = pending.map((row: Record<string, unknown>) => { const complete = row.x != null && row.y != null && row.weight_g != null; return `<article class="moderation-card" data-observation-id="${esc(row.id)}"><div class="moderation-summary"><span class="activity-pill"><i></i>${esc(row.source_system)} · ${esc(row.status)}</span><h2>${esc(row.fish_name)}</h2><p>${esc(row.waterbody_name)}</p><dl><div><dt>Координаты</dt><dd>${row.x == null || row.y == null ? "нет" : `${esc(row.x)}:${esc(row.y)}`}</dd></div><div><dt>Вес</dt><dd>${row.weight_g == null ? "нет" : `${esc(row.weight_g)} г`}</dd></div><div><dt>ID источника</dt><dd>${esc(row.source_external_id)}</dd></div><div><dt>Состояние</dt><dd>${complete ? "полная запись" : "неполная"}</dd></div></dl><a href="${esc(row.source_url)}" target="_blank" rel="noreferrer">Открыть первоисточник</a></div><div class="moderation-actions"><label>Каноническая рыба<select name="fish" required><option value="">Выберите…</option>${options(fishes, row.fish_slug)}</select></label><label>Канонический водоём<select name="waterbody" required><option value="">Выберите…</option>${options(waters, row.waterbody_slug)}</select></label><label>Примечание<textarea name="note" rows="2" maxlength="1000">${esc(row.review_note ?? "")}</textarea></label><p data-alias-message role="status"></p><div><button type="button" data-suggest>Подсказать соответствия</button><button type="button" data-map>Сопоставить</button><button type="button" data-publish ${complete && row.status === "ready" ? "" : "disabled"}>Опубликовать</button><button class="reject" type="button" data-reject>Отклонить</button></div></div></article>`; }).join("");
|
||||
}
|
||||
login?.addEventListener("submit", async event => { event.preventDefault(); token = String(new FormData(login).get("token") || ""); try { await loadQueue(); login.hidden = true; } catch (cause) { fail(cause instanceof Error ? cause.message : "Ошибка загрузки."); } });
|
||||
async function changePage(delta: number) {
|
||||
const oldOffset = offset;
|
||||
offset = Math.max(0, offset + delta);
|
||||
if (previous) previous.disabled = true;
|
||||
if (next) next.disabled = true;
|
||||
try { await loadQueue(); }
|
||||
catch { offset = oldOffset; fail("Не удалось загрузить страницу. Повторите вход в очередь."); if (login) login.hidden = false; }
|
||||
}
|
||||
previous?.addEventListener("click", () => changePage(-50));
|
||||
next?.addEventListener("click", () => changePage(50));
|
||||
login?.addEventListener("submit", async event => { event.preventDefault(); offset = 0; token = String(new FormData(login).get("token") || ""); try { await loadQueue(); login.hidden = true; } catch (cause) { fail(cause instanceof Error ? cause.message : "Ошибка загрузки."); } });
|
||||
list?.addEventListener("click", async event => { const button = (event.target as HTMLElement).closest<HTMLButtonElement>("button"); const card = button?.closest<HTMLElement>("[data-observation-id]"); if (!button || !card || !root) return; button.disabled = true; const base = `${root.dataset.apiUrl}/api/v1/admin/external-observations/${card.dataset.observationId}`; const headers = {Authorization:`Bearer ${token}`,"Content-Type":"application/json"}; try {
|
||||
if (button.hasAttribute("data-suggest")) {
|
||||
const suggestion = await json(`${base}/alias-suggestions`, {headers});
|
||||
|
||||
Reference in New Issue
Block a user