291 lines
11 KiB
JavaScript
291 lines
11 KiB
JavaScript
|
|
const MARKETING_CAMPAIGN_PAGE_SIZE = 100;
|
||
|
|
|
||
|
|
let marketingCampaignPage = 1;
|
||
|
|
let marketingCampaignTotalRows = 0;
|
||
|
|
let marketingCampaignLastQuery = null;
|
||
|
|
let marketingCampaignLoading = false;
|
||
|
|
|
||
|
|
function setMarketingCampaignState(message, mode = "") {
|
||
|
|
if (!marketingCampaignState) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
marketingCampaignState.textContent = message;
|
||
|
|
marketingCampaignState.classList.toggle("hidden", !message);
|
||
|
|
marketingCampaignState.classList.toggle("loading", mode === "loading");
|
||
|
|
marketingCampaignState.classList.toggle("error", mode === "error");
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearMarketingCampaignRows() {
|
||
|
|
if (marketingCampaignRows) {
|
||
|
|
marketingCampaignRows.innerHTML = "";
|
||
|
|
}
|
||
|
|
if (marketingCampaignCards) {
|
||
|
|
marketingCampaignCards.innerHTML = "";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function marketingBigInt(value) {
|
||
|
|
try {
|
||
|
|
return BigInt(String(value || "0"));
|
||
|
|
} catch (_) {
|
||
|
|
return 0n;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatMarketingInteger(value) {
|
||
|
|
const text = marketingBigInt(value).toString();
|
||
|
|
return text.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatMarketingCents(value) {
|
||
|
|
const cents = marketingBigInt(value);
|
||
|
|
const sign = cents < 0n ? "-" : "";
|
||
|
|
const absolute = cents < 0n ? -cents : cents;
|
||
|
|
const whole = absolute / 100n;
|
||
|
|
const fraction = String(absolute % 100n).padStart(2, "0");
|
||
|
|
return `${sign}${formatMarketingInteger(whole)}.${fraction}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatMarketingAverage(totalValue, count) {
|
||
|
|
const total = marketingBigInt(totalValue);
|
||
|
|
const divisor = marketingBigInt(count);
|
||
|
|
if (divisor === 0n) {
|
||
|
|
return "--";
|
||
|
|
}
|
||
|
|
return formatMarketingCents(total / divisor);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setMarketingCampaignSummary(data = null) {
|
||
|
|
const totalRecords = marketingBigInt(data?.total_records);
|
||
|
|
const totalImpressions = marketingBigInt(data?.total_impressions);
|
||
|
|
const totalClicks = marketingBigInt(data?.total_clicks);
|
||
|
|
const totalImpressionValue = marketingBigInt(data?.total_impression_value);
|
||
|
|
const totalClickValue = marketingBigInt(data?.total_click_value);
|
||
|
|
const totalValue = totalImpressionValue + totalClickValue;
|
||
|
|
|
||
|
|
if (marketingTotalRecords) {
|
||
|
|
marketingTotalRecords.textContent = data ? formatMarketingInteger(totalRecords) : "--";
|
||
|
|
}
|
||
|
|
if (marketingTotalImpressions) {
|
||
|
|
marketingTotalImpressions.textContent = data ? formatMarketingInteger(totalImpressions) : "--";
|
||
|
|
}
|
||
|
|
if (marketingTotalClicks) {
|
||
|
|
marketingTotalClicks.textContent = data ? formatMarketingInteger(totalClicks) : "--";
|
||
|
|
}
|
||
|
|
if (marketingAvgImpressionValue) {
|
||
|
|
marketingAvgImpressionValue.textContent = data
|
||
|
|
? formatMarketingAverage(totalImpressionValue, totalImpressions)
|
||
|
|
: "--";
|
||
|
|
}
|
||
|
|
if (marketingAvgClickValue) {
|
||
|
|
marketingAvgClickValue.textContent = data
|
||
|
|
? formatMarketingAverage(totalClickValue, totalClicks)
|
||
|
|
: "--";
|
||
|
|
}
|
||
|
|
if (marketingTotalValue) {
|
||
|
|
marketingTotalValue.textContent = data ? formatMarketingCents(totalValue) : "--";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function setMarketingCampaignFooter(visibleCount = 0, totalCount = 0, page = 1) {
|
||
|
|
const totalPages = Math.max(1, Math.ceil(totalCount / MARKETING_CAMPAIGN_PAGE_SIZE));
|
||
|
|
const pageStart = totalCount && visibleCount ? (page - 1) * MARKETING_CAMPAIGN_PAGE_SIZE : 0;
|
||
|
|
if (marketingCampaignCount) {
|
||
|
|
marketingCampaignCount.textContent = totalCount
|
||
|
|
? `Showing ${formatMarketingInteger(pageStart + 1)}-${formatMarketingInteger(pageStart + visibleCount)} of ${formatMarketingInteger(totalCount)}`
|
||
|
|
: "";
|
||
|
|
}
|
||
|
|
if (marketingCampaignPagination) {
|
||
|
|
marketingCampaignPagination.hidden = totalPages <= 1;
|
||
|
|
marketingCampaignPagination.querySelectorAll("[data-marketing-campaign-page]").forEach((button) => {
|
||
|
|
const action = button.dataset.marketingCampaignPage;
|
||
|
|
button.disabled = action === "first" || action === "previous"
|
||
|
|
? page <= 1
|
||
|
|
: page >= totalPages;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (marketingCampaignPageInput) {
|
||
|
|
marketingCampaignPageInput.value = String(page);
|
||
|
|
marketingCampaignPageInput.max = String(totalPages);
|
||
|
|
marketingCampaignPageInput.disabled = totalPages <= 1;
|
||
|
|
}
|
||
|
|
if (marketingCampaignPageTotal) {
|
||
|
|
marketingCampaignPageTotal.textContent = formatNumber(totalPages);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function marketingStatusClass(status) {
|
||
|
|
return String(status || "").toLowerCase() === "pending" ? "pending" : "confirmed";
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderMarketingCampaignRows(rows = []) {
|
||
|
|
clearMarketingCampaignRows();
|
||
|
|
const tableFragment = document.createDocumentFragment();
|
||
|
|
const cardFragment = document.createDocumentFragment();
|
||
|
|
|
||
|
|
rows.forEach((row) => {
|
||
|
|
const tr = document.createElement("tr");
|
||
|
|
tr.innerHTML = `
|
||
|
|
<td>${formatHistoryDate(row.time)}</td>
|
||
|
|
<td>${htmlEscape(row.campaign)}</td>
|
||
|
|
<td>${htmlEscape(row.ad_type)}</td>
|
||
|
|
<td>${htmlEscape(row.keyword)}</td>
|
||
|
|
<td class="marketing-displayed-cell" title="${htmlEscape(row.displayed)}">${htmlEscape(row.displayed)}</td>
|
||
|
|
<td>${formatMarketingInteger(row.impression || 0)}</td>
|
||
|
|
<td>${formatMarketingInteger(row.click || 0)}</td>
|
||
|
|
<td>${formatMarketingCents(row.impression_value || 0)}</td>
|
||
|
|
<td>${formatMarketingCents(row.click_value || 0)}</td>
|
||
|
|
<td><span class="table-status ${marketingStatusClass(row.status)}">${htmlEscape(row.status)}</span></td>
|
||
|
|
`;
|
||
|
|
tableFragment.appendChild(tr);
|
||
|
|
|
||
|
|
const card = document.createElement("article");
|
||
|
|
card.className = "history-card";
|
||
|
|
card.innerHTML = `
|
||
|
|
<div><span>Time</span><b>${formatHistoryDate(row.time)}</b></div>
|
||
|
|
<div><span>Campaign</span><b>${htmlEscape(row.campaign)}</b></div>
|
||
|
|
<div><span>Ad Type</span><b>${htmlEscape(row.ad_type)}</b></div>
|
||
|
|
<div><span>Keyword</span><b>${htmlEscape(row.keyword)}</b></div>
|
||
|
|
<div><span>Displayed</span><b>${htmlEscape(row.displayed)}</b></div>
|
||
|
|
<div><span>Impressions</span><b>${formatMarketingInteger(row.impression || 0)}</b></div>
|
||
|
|
<div><span>Clicks</span><b>${formatMarketingInteger(row.click || 0)}</b></div>
|
||
|
|
<div><span>Status</span><b>${htmlEscape(row.status)}</b></div>
|
||
|
|
`;
|
||
|
|
cardFragment.appendChild(card);
|
||
|
|
});
|
||
|
|
|
||
|
|
marketingCampaignRows?.appendChild(tableFragment);
|
||
|
|
marketingCampaignCards?.appendChild(cardFragment);
|
||
|
|
}
|
||
|
|
|
||
|
|
function validateMarketingCampaignQuery() {
|
||
|
|
const advertiser = String(marketingCampaignAdvertiserInput?.value || "").trim();
|
||
|
|
const campaign = String(marketingCampaignIdInput?.value || "").trim();
|
||
|
|
if (!walletLoaded) {
|
||
|
|
return { error: "Load a wallet before viewing campaign data." };
|
||
|
|
}
|
||
|
|
if (!activeBroadcastNode()) {
|
||
|
|
return { error: "Select a broadcast node before viewing campaign data." };
|
||
|
|
}
|
||
|
|
if (!advertiser) {
|
||
|
|
return { error: "Agency wallet address is required." };
|
||
|
|
}
|
||
|
|
if (!/^\d+$/.test(campaign)) {
|
||
|
|
return { error: "Campaign ID must be a whole number." };
|
||
|
|
}
|
||
|
|
return { advertiser, campaign };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadMarketingCampaignPage(page = marketingCampaignPage) {
|
||
|
|
if (marketingCampaignLoading) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const query = validateMarketingCampaignQuery();
|
||
|
|
if (query.error) {
|
||
|
|
clearMarketingCampaignRows();
|
||
|
|
setMarketingCampaignSummary(null);
|
||
|
|
setMarketingCampaignFooter(0, 0, 1);
|
||
|
|
setMarketingCampaignState(query.error, "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
if (!invoke) {
|
||
|
|
setMarketingCampaignState("Wallet bridge is not available.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
marketingCampaignLoading = true;
|
||
|
|
marketingCampaignLoadButton?.toggleAttribute("disabled", true);
|
||
|
|
marketingCampaignRefreshButton?.toggleAttribute("disabled", true);
|
||
|
|
setMarketingCampaignState("Loading campaign activity...", "loading");
|
||
|
|
|
||
|
|
try {
|
||
|
|
const normalizedPage = Math.max(1, Number.parseInt(page || 1, 10));
|
||
|
|
const skip = (normalizedPage - 1) * MARKETING_CAMPAIGN_PAGE_SIZE;
|
||
|
|
const data = await invoke("marketing_campaign_history", {
|
||
|
|
broadcastNode: activeBroadcastNode(),
|
||
|
|
advertiser: query.advertiser,
|
||
|
|
campaign: query.campaign,
|
||
|
|
skip,
|
||
|
|
limit: MARKETING_CAMPAIGN_PAGE_SIZE
|
||
|
|
});
|
||
|
|
|
||
|
|
marketingCampaignLastQuery = query;
|
||
|
|
marketingCampaignPage = normalizedPage;
|
||
|
|
marketingCampaignTotalRows = Number(marketingBigInt(data?.total_records));
|
||
|
|
setMarketingCampaignSummary(data);
|
||
|
|
renderMarketingCampaignRows(data?.rows || []);
|
||
|
|
setMarketingCampaignFooter((data?.rows || []).length, marketingCampaignTotalRows, marketingCampaignPage);
|
||
|
|
setMarketingCampaignState((data?.rows || []).length ? "" : "No campaign records found.");
|
||
|
|
} catch (error) {
|
||
|
|
setMarketingCampaignState(String(error), "error");
|
||
|
|
} finally {
|
||
|
|
marketingCampaignLoading = false;
|
||
|
|
marketingCampaignLoadButton?.toggleAttribute("disabled", false);
|
||
|
|
marketingCampaignRefreshButton?.toggleAttribute("disabled", false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function prepareMarketingCampaignPage() {
|
||
|
|
if (!marketingCampaignLastQuery) {
|
||
|
|
setMarketingCampaignSummary(null);
|
||
|
|
setMarketingCampaignFooter(0, 0, 1);
|
||
|
|
if (!marketingCampaignRows?.children.length) {
|
||
|
|
setMarketingCampaignState("Enter an agency wallet and campaign ID to load campaign activity.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetMarketingCampaignPage() {
|
||
|
|
marketingCampaignPage = 1;
|
||
|
|
marketingCampaignTotalRows = 0;
|
||
|
|
marketingCampaignLastQuery = null;
|
||
|
|
marketingCampaignLoading = false;
|
||
|
|
if (marketingCampaignAdvertiserInput) {
|
||
|
|
marketingCampaignAdvertiserInput.value = "";
|
||
|
|
}
|
||
|
|
if (marketingCampaignIdInput) {
|
||
|
|
marketingCampaignIdInput.value = "";
|
||
|
|
}
|
||
|
|
clearMarketingCampaignRows();
|
||
|
|
setMarketingCampaignSummary(null);
|
||
|
|
setMarketingCampaignFooter(0, 0, 1);
|
||
|
|
setMarketingCampaignState("Enter an agency wallet and campaign ID to load campaign activity.");
|
||
|
|
}
|
||
|
|
|
||
|
|
marketingCampaignForm?.addEventListener("submit", (event) => {
|
||
|
|
event.preventDefault();
|
||
|
|
marketingCampaignPage = 1;
|
||
|
|
loadMarketingCampaignPage(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
marketingCampaignRefreshButton?.addEventListener("click", () => {
|
||
|
|
loadMarketingCampaignPage(marketingCampaignPage);
|
||
|
|
});
|
||
|
|
|
||
|
|
marketingCampaignPagination?.querySelectorAll("[data-marketing-campaign-page]").forEach((button) => {
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
const action = button.dataset.marketingCampaignPage;
|
||
|
|
const totalPages = Math.max(1, Math.ceil(marketingCampaignTotalRows / MARKETING_CAMPAIGN_PAGE_SIZE));
|
||
|
|
if (action === "first") {
|
||
|
|
loadMarketingCampaignPage(1);
|
||
|
|
} else if (action === "previous") {
|
||
|
|
loadMarketingCampaignPage(Math.max(1, marketingCampaignPage - 1));
|
||
|
|
} else if (action === "next") {
|
||
|
|
loadMarketingCampaignPage(Math.min(totalPages, marketingCampaignPage + 1));
|
||
|
|
} else if (action === "last") {
|
||
|
|
loadMarketingCampaignPage(totalPages);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
marketingCampaignPageInput?.addEventListener("change", () => {
|
||
|
|
const totalPages = Math.max(1, Math.ceil(marketingCampaignTotalRows / MARKETING_CAMPAIGN_PAGE_SIZE));
|
||
|
|
const requestedPage = Math.min(
|
||
|
|
totalPages,
|
||
|
|
Math.max(1, Number.parseInt(marketingCampaignPageInput.value || "1", 10))
|
||
|
|
);
|
||
|
|
loadMarketingCampaignPage(requestedPage);
|
||
|
|
});
|