add filtering and sorting features; enhance events and articles pages with URL state persistence
This commit is contained in:
parent
bec6763191
commit
194442ec4c
8 changed files with 274 additions and 16 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
// articles page — list + keyword/status/source filtering + edit modal
|
// articles page — list + keyword/status/source filtering + edit modal
|
||||||
// depends on: app.js (api, toast, escapeHtml, PAGE, badgeHtml)
|
// filter state is persisted to the url query so reload/share keeps it.
|
||||||
|
// depends on: app.js (api, toast, escapeHtml, PAGE, badgeHtml, query*)
|
||||||
|
|
||||||
let articleOffset = 0;
|
let articleOffset = 0;
|
||||||
let currentArticle = null;
|
let currentArticle = null;
|
||||||
|
|
@ -8,12 +9,17 @@ let currentArticle = null;
|
||||||
async function loadSources() {
|
async function loadSources() {
|
||||||
const sources = await api("/admin/api/sources");
|
const sources = await api("/admin/api/sources");
|
||||||
const sel = document.getElementById("f-source");
|
const sel = document.getElementById("f-source");
|
||||||
|
|
||||||
|
const current = queryGet("source");
|
||||||
|
|
||||||
sources.forEach(s => {
|
sources.forEach(s => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = s;
|
opt.value = s;
|
||||||
opt.textContent = s;
|
opt.textContent = s;
|
||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (current) sel.value = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -28,6 +34,16 @@ function getFilters() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// sync current filters + offset into url so the state is bookmarkable
|
||||||
|
function syncUrl() {
|
||||||
|
const f = getFilters();
|
||||||
|
queryWrite({
|
||||||
|
...f,
|
||||||
|
offset: articleOffset > 0 ? articleOffset : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadArticles() {
|
async function loadArticles() {
|
||||||
const f = getFilters();
|
const f = getFilters();
|
||||||
const params = new URLSearchParams({ limit: PAGE, offset: articleOffset });
|
const params = new URLSearchParams({ limit: PAGE, offset: articleOffset });
|
||||||
|
|
@ -88,14 +104,42 @@ async function openArticle(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
|
||||||
document.getElementById("searchBtn").onclick = () => { articleOffset = 0; loadArticles(); };
|
// restore filter inputs from the current url
|
||||||
document.getElementById("prevBtn").onclick = () => { articleOffset = Math.max(0, articleOffset - PAGE); loadArticles(); };
|
queryApplyToInputs({
|
||||||
document.getElementById("nextBtn").onclick = () => { articleOffset += PAGE; loadArticles(); };
|
"f-keyword": "keyword",
|
||||||
|
"f-status": "content_status",
|
||||||
|
"f-from": "from",
|
||||||
|
"f-to": "to",
|
||||||
|
});
|
||||||
|
articleOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("searchBtn").onclick = () => {
|
||||||
|
articleOffset = 0;
|
||||||
|
syncUrl();
|
||||||
|
loadArticles();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("prevBtn").onclick = () => {
|
||||||
|
articleOffset = Math.max(0, articleOffset - PAGE);
|
||||||
|
syncUrl();
|
||||||
|
loadArticles();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("nextBtn").onclick = () => {
|
||||||
|
articleOffset += PAGE;
|
||||||
|
syncUrl();
|
||||||
|
loadArticles();
|
||||||
|
};
|
||||||
|
|
||||||
document.querySelector(".filters").addEventListener("keydown", e => {
|
document.querySelector(".filters").addEventListener("keydown", e => {
|
||||||
if (e.key === "Enter") { articleOffset = 0; loadArticles(); }
|
if (e.key === "Enter") {
|
||||||
|
articleOffset = 0;
|
||||||
|
syncUrl();
|
||||||
|
loadArticles();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -140,6 +184,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSources();
|
await loadSources(); // wait for dropdown to render the saved source selection
|
||||||
loadArticles();
|
loadArticles();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,43 @@
|
||||||
// events page — list + title edit + detach-and-delete
|
// events page — list + title edit + detach-and-delete, with filters
|
||||||
|
// filter/sort/pagination state persists to url query params
|
||||||
// depends on: app.js
|
// depends on: app.js
|
||||||
|
|
||||||
let eventOffset = 0;
|
let eventOffset = 0;
|
||||||
let currentEvent = null;
|
let currentEvent = null;
|
||||||
|
|
||||||
|
|
||||||
|
function getEventFilters() {
|
||||||
|
return {
|
||||||
|
keyword: document.getElementById("ef-keyword").value.trim(),
|
||||||
|
min_articles: document.getElementById("ef-min").value.trim(),
|
||||||
|
from: document.getElementById("ef-from").value,
|
||||||
|
to: document.getElementById("ef-to").value,
|
||||||
|
sort: document.getElementById("ef-sort").value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function syncUrl() {
|
||||||
|
const f = getEventFilters();
|
||||||
|
queryWrite({
|
||||||
|
...f,
|
||||||
|
// default sort isn't worth surfacing in the url
|
||||||
|
sort: f.sort && f.sort !== "created_desc" ? f.sort : "",
|
||||||
|
offset: eventOffset > 0 ? eventOffset : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadEvents() {
|
async function loadEvents() {
|
||||||
const data = await api(`/admin/api/events?limit=${PAGE}&offset=${eventOffset}`);
|
const f = getEventFilters();
|
||||||
|
const params = new URLSearchParams({ limit: PAGE, offset: eventOffset });
|
||||||
|
if (f.keyword) params.set("keyword", f.keyword);
|
||||||
|
if (f.min_articles) params.set("min_articles", f.min_articles);
|
||||||
|
if (f.from) params.set("from", f.from + "T00:00:00");
|
||||||
|
if (f.to) params.set("to", f.to + "T23:59:59");
|
||||||
|
if (f.sort) params.set("sort", f.sort);
|
||||||
|
|
||||||
|
const data = await api(`/admin/api/events?${params}`);
|
||||||
const tbody = document.getElementById("eventTable");
|
const tbody = document.getElementById("eventTable");
|
||||||
|
|
||||||
tbody.innerHTML = data.rows.map(r => `
|
tbody.innerHTML = data.rows.map(r => `
|
||||||
|
|
@ -36,8 +67,41 @@ function openEvent(id, title) {
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
document.getElementById("ePrevBtn").onclick = () => { eventOffset = Math.max(0, eventOffset - PAGE); loadEvents(); };
|
// restore filter state from url
|
||||||
document.getElementById("eNextBtn").onclick = () => { eventOffset += PAGE; loadEvents(); };
|
queryApplyToInputs({
|
||||||
|
"ef-keyword": "keyword",
|
||||||
|
"ef-min": "min_articles",
|
||||||
|
"ef-from": "from",
|
||||||
|
"ef-to": "to",
|
||||||
|
"ef-sort": "sort",
|
||||||
|
});
|
||||||
|
eventOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("eSearchBtn").onclick = () => {
|
||||||
|
eventOffset = 0;
|
||||||
|
syncUrl();
|
||||||
|
loadEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("ePrevBtn").onclick = () => {
|
||||||
|
eventOffset = Math.max(0, eventOffset - PAGE);
|
||||||
|
syncUrl();
|
||||||
|
loadEvents();
|
||||||
|
};
|
||||||
|
document.getElementById("eNextBtn").onclick = () => {
|
||||||
|
eventOffset += PAGE;
|
||||||
|
syncUrl();
|
||||||
|
loadEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector(".filters").addEventListener("keydown", e => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
eventOffset = 0;
|
||||||
|
syncUrl();
|
||||||
|
loadEvents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("eCancelBtn").onclick = () =>
|
document.getElementById("eCancelBtn").onclick = () =>
|
||||||
document.getElementById("eventOverlay").classList.remove("open");
|
document.getElementById("eventOverlay").classList.remove("open");
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,14 @@ let graphSearchTerm = "";
|
||||||
let graphSelectedId = null;
|
let graphSelectedId = null;
|
||||||
|
|
||||||
|
|
||||||
|
function syncGraphUrl() {
|
||||||
|
queryWrite({
|
||||||
|
q: graphSearchTerm,
|
||||||
|
type: graphFilterType && graphFilterType !== "all" ? graphFilterType : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadIntelGraph() {
|
async function loadIntelGraph() {
|
||||||
const data = await api("/admin/api/intelligence/graph");
|
const data = await api("/admin/api/intelligence/graph");
|
||||||
|
|
||||||
|
|
@ -547,8 +555,24 @@ async function toggleEvidenceRow(row) {
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
// restore search/type from url
|
||||||
|
const urlSearch = queryGet("q");
|
||||||
|
const urlType = queryGet("type") || "all";
|
||||||
|
|
||||||
|
if (urlSearch) {
|
||||||
|
document.getElementById("graph-search").value = urlSearch;
|
||||||
|
graphSearchTerm = urlSearch.toLowerCase();
|
||||||
|
}
|
||||||
|
if (urlType && urlType !== "all") {
|
||||||
|
graphFilterType = urlType;
|
||||||
|
document.querySelectorAll(".graph-chip").forEach(c =>
|
||||||
|
c.classList.toggle("active", c.dataset.type === urlType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("graph-search").addEventListener("input", ev => {
|
document.getElementById("graph-search").addEventListener("input", ev => {
|
||||||
graphSearchTerm = ev.target.value.trim().toLowerCase();
|
graphSearchTerm = ev.target.value.trim().toLowerCase();
|
||||||
|
syncGraphUrl();
|
||||||
applyGraphFilters();
|
applyGraphFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -558,6 +582,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||||
document.querySelectorAll(".graph-chip").forEach(c => c.classList.remove("active"));
|
document.querySelectorAll(".graph-chip").forEach(c => c.classList.remove("active"));
|
||||||
btn.classList.add("active");
|
btn.classList.add("active");
|
||||||
graphFilterType = btn.dataset.type;
|
graphFilterType = btn.dataset.type;
|
||||||
|
syncGraphUrl();
|
||||||
applyGraphFilters();
|
applyGraphFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
// intelligence → knowledge table
|
// intelligence → knowledge table
|
||||||
|
// filter/sort/pagination persisted via url query params
|
||||||
// depends on: app.js, intel-shared.js
|
// depends on: app.js, intel-shared.js
|
||||||
|
|
||||||
let knowledgeOffset = 0;
|
let knowledgeOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function syncUrl() {
|
||||||
|
const companyId = document.getElementById("i-company").value;
|
||||||
|
const type = document.getElementById("i-type").value;
|
||||||
|
const sort = document.getElementById("i-sort").value;
|
||||||
|
queryWrite({
|
||||||
|
company_id: companyId,
|
||||||
|
type: type,
|
||||||
|
sort: sort && sort !== "id" ? sort : "",
|
||||||
|
offset: knowledgeOffset > 0 ? knowledgeOffset : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadKnowledge() {
|
async function loadKnowledge() {
|
||||||
const companyId = document.getElementById("i-company").value;
|
const companyId = document.getElementById("i-company").value;
|
||||||
const type = document.getElementById("i-type").value;
|
const type = document.getElementById("i-type").value;
|
||||||
|
|
@ -45,19 +59,32 @@ async function loadKnowledge() {
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
document.getElementById("iPrevBtn").onclick = () => {
|
document.getElementById("iPrevBtn").onclick = () => {
|
||||||
knowledgeOffset = Math.max(0, knowledgeOffset - PAGE);
|
knowledgeOffset = Math.max(0, knowledgeOffset - PAGE);
|
||||||
|
syncUrl();
|
||||||
loadKnowledge();
|
loadKnowledge();
|
||||||
};
|
};
|
||||||
document.getElementById("iNextBtn").onclick = () => {
|
document.getElementById("iNextBtn").onclick = () => {
|
||||||
knowledgeOffset += PAGE;
|
knowledgeOffset += PAGE;
|
||||||
|
syncUrl();
|
||||||
loadKnowledge();
|
loadKnowledge();
|
||||||
};
|
};
|
||||||
document.getElementById("i-filter-btn").onclick = () => {
|
document.getElementById("i-filter-btn").onclick = () => {
|
||||||
knowledgeOffset = 0;
|
knowledgeOffset = 0;
|
||||||
|
syncUrl();
|
||||||
loadKnowledge();
|
loadKnowledge();
|
||||||
};
|
};
|
||||||
|
|
||||||
const ok = await loadIntelStatsRow();
|
const ok = await loadIntelStatsRow();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
await loadIntelCompanies();
|
await loadIntelCompanies();
|
||||||
|
|
||||||
|
// restore from url (after dropdown is populated so company_id options exist)
|
||||||
|
queryApplyToInputs({
|
||||||
|
"i-company": "company_id",
|
||||||
|
"i-type": "type",
|
||||||
|
"i-sort": "sort",
|
||||||
|
});
|
||||||
|
knowledgeOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||||
|
|
||||||
loadKnowledge();
|
loadKnowledge();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
// intelligence → predictions table
|
// intelligence → predictions table
|
||||||
|
// filter/sort/pagination persisted via url query params
|
||||||
// depends on: app.js, intel-shared.js
|
// depends on: app.js, intel-shared.js
|
||||||
|
|
||||||
let predictionsOffset = 0;
|
let predictionsOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function syncUrl() {
|
||||||
|
const companyId = document.getElementById("i-company").value;
|
||||||
|
const sort = document.getElementById("i-sort").value;
|
||||||
|
queryWrite({
|
||||||
|
company_id: companyId,
|
||||||
|
sort: sort && sort !== "id" ? sort : "",
|
||||||
|
offset: predictionsOffset > 0 ? predictionsOffset : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadPredictions() {
|
async function loadPredictions() {
|
||||||
const companyId = document.getElementById("i-company").value;
|
const companyId = document.getElementById("i-company").value;
|
||||||
const sort = document.getElementById("i-sort").value;
|
const sort = document.getElementById("i-sort").value;
|
||||||
|
|
@ -43,19 +55,30 @@ async function loadPredictions() {
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
document.getElementById("iPrevBtn").onclick = () => {
|
document.getElementById("iPrevBtn").onclick = () => {
|
||||||
predictionsOffset = Math.max(0, predictionsOffset - PAGE);
|
predictionsOffset = Math.max(0, predictionsOffset - PAGE);
|
||||||
|
syncUrl();
|
||||||
loadPredictions();
|
loadPredictions();
|
||||||
};
|
};
|
||||||
document.getElementById("iNextBtn").onclick = () => {
|
document.getElementById("iNextBtn").onclick = () => {
|
||||||
predictionsOffset += PAGE;
|
predictionsOffset += PAGE;
|
||||||
|
syncUrl();
|
||||||
loadPredictions();
|
loadPredictions();
|
||||||
};
|
};
|
||||||
document.getElementById("i-filter-btn").onclick = () => {
|
document.getElementById("i-filter-btn").onclick = () => {
|
||||||
predictionsOffset = 0;
|
predictionsOffset = 0;
|
||||||
|
syncUrl();
|
||||||
loadPredictions();
|
loadPredictions();
|
||||||
};
|
};
|
||||||
|
|
||||||
const ok = await loadIntelStatsRow();
|
const ok = await loadIntelStatsRow();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
await loadIntelCompanies();
|
await loadIntelCompanies();
|
||||||
|
|
||||||
|
queryApplyToInputs({
|
||||||
|
"i-company": "company_id",
|
||||||
|
"i-sort": "sort",
|
||||||
|
});
|
||||||
|
predictionsOffset = parseInt(queryGet("offset"), 10) || 0;
|
||||||
|
|
||||||
loadPredictions();
|
loadPredictions();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,16 @@ async function runSql() {
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// restore selected db from url
|
||||||
|
const urlDb = queryGet("db");
|
||||||
|
if (urlDb === "archive" || urlDb === "intelligence") {
|
||||||
|
document.getElementById("sql-db").value = urlDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("sql-db").addEventListener("change", ev => {
|
||||||
|
queryWrite({ db: ev.target.value === "archive" ? "" : ev.target.value });
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("sql-run-btn").onclick = runSql;
|
document.getElementById("sql-run-btn").onclick = runSql;
|
||||||
|
|
||||||
document.getElementById("sql-input").addEventListener("keydown", e => {
|
document.getElementById("sql-input").addEventListener("keydown", e => {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,22 @@
|
||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<label>Keyword <input type="text" id="ef-keyword" placeholder="search title..." /></label>
|
||||||
|
<label>Min articles <input type="text" id="ef-min" placeholder="e.g. 2" style="min-width:90px" /></label>
|
||||||
|
<label>From <input type="date" id="ef-from" /></label>
|
||||||
|
<label>To <input type="date" id="ef-to" /></label>
|
||||||
|
<label>Sort
|
||||||
|
<select id="ef-sort">
|
||||||
|
<option value="created_desc">Newest first</option>
|
||||||
|
<option value="created_asc">Oldest first</option>
|
||||||
|
<option value="articles_desc">Most articles</option>
|
||||||
|
<option value="articles_asc">Fewest articles</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button class="primary" id="eSearchBtn" style="align-self:flex-end">Filter</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ async function adminRoutes(fastify) {
|
||||||
return rows.map(r => r.source);
|
return rows.map(r => r.source);
|
||||||
});
|
});
|
||||||
|
|
||||||
// events
|
// events — supports keyword / date range / min-article-count / sort
|
||||||
fastify.get('/admin/api/events', async (request, reply) => {
|
fastify.get('/admin/api/events', async (request, reply) => {
|
||||||
if (!checkAuth(request, reply)) return;
|
if (!checkAuth(request, reply)) return;
|
||||||
|
|
||||||
|
|
@ -232,17 +232,66 @@ async function adminRoutes(fastify) {
|
||||||
const limit = Math.min(parseInt(q.limit, 10) || 50, 200);
|
const limit = Math.min(parseInt(q.limit, 10) || 50, 200);
|
||||||
const offset = parseInt(q.offset, 10) || 0;
|
const offset = parseInt(q.offset, 10) || 0;
|
||||||
|
|
||||||
const total = db.prepare(`SELECT COUNT(*) as n FROM events`).get().n;
|
const where = [];
|
||||||
|
const whereParams = [];
|
||||||
|
|
||||||
|
if (q.keyword) {
|
||||||
|
where.push('e.title LIKE ?');
|
||||||
|
whereParams.push(`%${q.keyword}%`);
|
||||||
|
}
|
||||||
|
if (q.from) {
|
||||||
|
where.push('e.created_at >= ?');
|
||||||
|
whereParams.push(q.from);
|
||||||
|
}
|
||||||
|
if (q.to) {
|
||||||
|
where.push('e.created_at <= ?');
|
||||||
|
whereParams.push(q.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// having filters operate on the aggregate count
|
||||||
|
const having = [];
|
||||||
|
const havingParams = [];
|
||||||
|
if (q.min_articles) {
|
||||||
|
const n = parseInt(q.min_articles, 10);
|
||||||
|
if (!isNaN(n)) { having.push('article_count >= ?'); havingParams.push(n); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelist sort columns + direction so user input cant break the query
|
||||||
|
const sortMap = {
|
||||||
|
created_desc: 'e.created_at DESC',
|
||||||
|
created_asc: 'e.created_at ASC',
|
||||||
|
articles_desc: 'article_count DESC',
|
||||||
|
articles_asc: 'article_count ASC',
|
||||||
|
};
|
||||||
|
const orderBy = sortMap[q.sort] || sortMap.created_desc;
|
||||||
|
|
||||||
|
const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
||||||
|
const havingClause = having.length ? `HAVING ${having.join(' AND ')}` : '';
|
||||||
|
|
||||||
|
// total count has to respect the HAVING clause too, so wrap the grouped query
|
||||||
|
const totalRow = db.prepare(`
|
||||||
|
SELECT COUNT(*) as n FROM (
|
||||||
|
SELECT e.id, COUNT(a.id) as article_count
|
||||||
|
FROM events e
|
||||||
|
LEFT JOIN articles a ON a.event_id = e.id
|
||||||
|
${whereClause}
|
||||||
|
GROUP BY e.id
|
||||||
|
${havingClause}
|
||||||
|
)
|
||||||
|
`).get(...whereParams, ...havingParams);
|
||||||
|
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT e.id, e.title, e.created_at, COUNT(a.id) as article_count
|
SELECT e.id, e.title, e.created_at, COUNT(a.id) as article_count
|
||||||
FROM events e
|
FROM events e
|
||||||
LEFT JOIN articles a ON a.event_id = e.id
|
LEFT JOIN articles a ON a.event_id = e.id
|
||||||
|
${whereClause}
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.created_at DESC
|
${havingClause}
|
||||||
|
ORDER BY ${orderBy}
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`).all(limit, offset);
|
`).all(...whereParams, ...havingParams, limit, offset);
|
||||||
|
|
||||||
return { total, rows };
|
return { total: totalRow.n, rows };
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.delete('/admin/api/events/:id', async (request, reply) => {
|
fastify.delete('/admin/api/events/:id', async (request, reply) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue