593 lines
27 KiB
TypeScript
593 lines
27 KiB
TypeScript
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
|
|
// version: 6
|
|
|
|
import DataTable from "datatables.net-bs5";
|
|
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
|
import { Modal } from "bootstrap";
|
|
import ResizeObserver from "resize-observer-polyfill";
|
|
import "simplebar";
|
|
import type { ShellStatusDto } from "./bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts";
|
|
import type { StoreRuntimeStatusDto } from "./bindings/ksp_app_store_desk/dto_common/StoreRuntimeStatusDto.ts";
|
|
import type { StoreTransactionDetailDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts";
|
|
import type { StoreTransactionDetailRequestDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailRequestDto.ts";
|
|
import type { StoreTransactionQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryRequestDto.ts";
|
|
import type { StoreTransactionQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionQueryResponseDto.ts";
|
|
import type { StoreTransactionRowDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionRowDto.ts";
|
|
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
|
import { invokeKsp } from "./invoke";
|
|
|
|
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
|
installFrontendConsoleBridge("main");
|
|
|
|
type ViewId = "overview" | "transactions" | "accounts" | "diagnostics";
|
|
|
|
interface DataTablesPageRequest {
|
|
draw: number;
|
|
length: number;
|
|
start: number;
|
|
}
|
|
|
|
interface DataTablesPageResponse<T> {
|
|
data: T[];
|
|
draw: number;
|
|
recordsFiltered: number;
|
|
recordsTotal: number;
|
|
}
|
|
|
|
interface StoreDataTable {
|
|
columns: { adjust(): void };
|
|
draw(resetPaging?: boolean): void;
|
|
on(event: string, callback: (...args: unknown[]) => void): StoreDataTable;
|
|
}
|
|
|
|
const viewTitles: Record<ViewId, string> = {
|
|
overview: "Overview",
|
|
transactions: "RAW Transactions",
|
|
accounts: "RAW Accounts",
|
|
diagnostics: "Diagnostics",
|
|
};
|
|
|
|
let transactionTable: StoreDataTable | null = null;
|
|
let accountTable: StoreDataTable | null = null;
|
|
|
|
const COPY_FEEDBACK_MILLISECONDS = 1200;
|
|
const LONG_TEXT_HEAD_CHARACTERS = 12;
|
|
const LONG_TEXT_TAIL_CHARACTERS = 8;
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replaceAll("&", "&")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|
|
|
|
function truncateLongText(value: string): string {
|
|
const threshold = LONG_TEXT_HEAD_CHARACTERS + LONG_TEXT_TAIL_CHARACTERS + 1;
|
|
if (value.length <= threshold) {
|
|
return value;
|
|
}
|
|
return `${value.slice(0, LONG_TEXT_HEAD_CHARACTERS)}…${value.slice(-LONG_TEXT_TAIL_CHARACTERS)}`;
|
|
}
|
|
|
|
function renderCopyableLongText(value: string, fieldId: string): string {
|
|
const escapedValue = escapeHtml(value);
|
|
const escapedVisible = escapeHtml(truncateLongText(value));
|
|
const escapedFieldId = escapeHtml(fieldId);
|
|
return `<span class="app-copyable-long-text"><span class="app-copyable-long-text__value font-monospace" title="${escapedValue}">${escapedVisible}</span><button class="btn btn-sm btn-outline-secondary app-copyable-long-text__copy" type="button" data-copy-long-text="${escapedValue}" data-copy-field="${escapedFieldId}" title="Copier la valeur complète" aria-label="Copier la valeur complète"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
|
|
}
|
|
|
|
function setCopyableLongText(elementId: string, value: string, fieldId: string): void {
|
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.replaceChildren();
|
|
const wrapper = document.createElement("span");
|
|
wrapper.className = "app-copyable-long-text";
|
|
const text = document.createElement("span");
|
|
text.className = "app-copyable-long-text__value font-monospace";
|
|
text.textContent = truncateLongText(value);
|
|
text.title = value;
|
|
const button = document.createElement("button");
|
|
button.className = "btn btn-sm btn-outline-secondary app-copyable-long-text__copy";
|
|
button.type = "button";
|
|
button.dataset.copyLongText = value;
|
|
button.dataset.copyField = fieldId;
|
|
button.title = "Copier la valeur complète";
|
|
button.setAttribute("aria-label", "Copier la valeur complète");
|
|
button.innerHTML = '<i class="fa-regular fa-copy" aria-hidden="true"></i>';
|
|
wrapper.append(text, button);
|
|
element.append(wrapper);
|
|
}
|
|
|
|
async function writeClipboardText(value: string): Promise<boolean> {
|
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
try {
|
|
await navigator.clipboard.writeText(value);
|
|
return true;
|
|
} catch {
|
|
// Fall back to the document copy command for WebViews without Clipboard API permission.
|
|
}
|
|
}
|
|
const textarea = document.createElement("textarea");
|
|
textarea.value = value;
|
|
textarea.setAttribute("readonly", "");
|
|
textarea.style.position = "fixed";
|
|
textarea.style.left = "-10000px";
|
|
textarea.style.top = "-10000px";
|
|
document.body.append(textarea);
|
|
textarea.select();
|
|
const copied = document.execCommand("copy");
|
|
textarea.remove();
|
|
return copied;
|
|
}
|
|
|
|
async function copyLongText(button: HTMLButtonElement, value: string, fieldId: string): Promise<void> {
|
|
frontendDebug("main", "Store Desk long text copy requested", { fieldId });
|
|
const copied = await writeClipboardText(value);
|
|
if (!copied) {
|
|
frontendError("main", "Store Desk long text copy failed", { fieldId });
|
|
return;
|
|
}
|
|
const icon = button.querySelector<HTMLElement>("i");
|
|
const previousClassName = icon?.className ?? "";
|
|
const previousTitle = button.title;
|
|
if (icon) {
|
|
icon.className = "fa-solid fa-check";
|
|
}
|
|
button.title = "Copié";
|
|
frontendDebug("main", "Store Desk long text copy completed", { fieldId });
|
|
window.setTimeout(() => {
|
|
if (icon) {
|
|
icon.className = previousClassName;
|
|
}
|
|
button.title = previousTitle;
|
|
}, COPY_FEEDBACK_MILLISECONDS);
|
|
}
|
|
|
|
function isViewId(value: string | undefined): value is ViewId {
|
|
return value === "overview" || value === "transactions" || value === "accounts" || value === "diagnostics";
|
|
}
|
|
|
|
function setText(elementId: string, value: string): void {
|
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
if (element) {
|
|
element.textContent = value;
|
|
}
|
|
}
|
|
|
|
function setVisible(elementId: string, visible: boolean): void {
|
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
if (element) {
|
|
element.hidden = !visible;
|
|
}
|
|
}
|
|
|
|
function renderDiagnostic(elementId: string, diagnostic: { domain: string; code: string; message: string } | null): void {
|
|
const element = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = diagnostic === null;
|
|
element.textContent = diagnostic ? `${diagnostic.domain}/${diagnostic.code}: ${diagnostic.message}` : "";
|
|
}
|
|
|
|
function activateView(viewId: ViewId): void {
|
|
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
|
|
panel.hidden = panel.dataset.viewPanel !== viewId;
|
|
});
|
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
|
button.classList.toggle("active", button.dataset.view === viewId);
|
|
});
|
|
const title = document.querySelector<HTMLElement>("#headerViewTitle");
|
|
if (title) {
|
|
title.textContent = viewTitles[viewId];
|
|
}
|
|
document.title = `Store Desk — ${viewTitles[viewId]}`;
|
|
if (viewId === "transactions") {
|
|
transactionTable?.columns.adjust();
|
|
}
|
|
if (viewId === "accounts") {
|
|
accountTable?.columns.adjust();
|
|
}
|
|
frontendDebug("main", "Store Desk view activated", { viewId });
|
|
}
|
|
|
|
function decimalCountToSafeNumber(value: string): number | null {
|
|
if (!/^\d+$/.test(value)) {
|
|
return null;
|
|
}
|
|
const exact = BigInt(value);
|
|
if (exact > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
return null;
|
|
}
|
|
return Number(exact);
|
|
}
|
|
|
|
function currentTransactionQuery(page: DataTablesPageRequest): StoreTransactionQueryRequestDto | null {
|
|
if (!Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) {
|
|
return null;
|
|
}
|
|
const slotMinElement = document.querySelector<HTMLInputElement>("#transactionSlotMin");
|
|
const slotMaxElement = document.querySelector<HTMLInputElement>("#transactionSlotMax");
|
|
const directionElement = document.querySelector<HTMLSelectElement>("#transactionDirection");
|
|
const slotMin = slotMinElement?.value.trim() ?? "";
|
|
const slotMax = slotMaxElement?.value.trim() ?? "";
|
|
return {
|
|
direction: directionElement?.value ?? "descending",
|
|
limit: page.length,
|
|
offset: page.start.toString(),
|
|
slotMax: slotMax === "" ? null : slotMax,
|
|
slotMin: slotMin === "" ? null : slotMin,
|
|
};
|
|
}
|
|
|
|
function emptyTransactionPage(draw: number): DataTablesPageResponse<StoreTransactionRowDto> {
|
|
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
|
|
}
|
|
|
|
function setTransactionQueryError(message: string | null): void {
|
|
const element = document.querySelector<HTMLElement>("#transactionsQueryError");
|
|
if (!element) {
|
|
return;
|
|
}
|
|
element.hidden = message === null;
|
|
element.textContent = message ?? "";
|
|
}
|
|
|
|
async function queryTransactions(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreTransactionRowDto>) => void): Promise<void> {
|
|
const draw = Number.isSafeInteger(page.draw) && page.draw >= 0 ? page.draw : 0;
|
|
const request = currentTransactionQuery(page);
|
|
if (!request) {
|
|
setTransactionQueryError("La pagination demandée n'est pas admise par Store Desk.");
|
|
callback(emptyTransactionPage(draw));
|
|
frontendError("main", "Store Desk transaction DataTables query rejected before IPC");
|
|
return;
|
|
}
|
|
setTransactionQueryError(null);
|
|
frontendDebug("main", "Store Desk transaction DataTables server-side query started", {
|
|
pageLength: request.limit,
|
|
slotFilterActive: request.slotMin !== null || request.slotMax !== null,
|
|
});
|
|
try {
|
|
const response = await invokeKsp<StoreTransactionQueryResponseDto>("main", "store_query_transactions", { request });
|
|
const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal);
|
|
const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal);
|
|
if (recordsTotal === null || recordsFiltered === null) {
|
|
setTransactionQueryError("Le volume Store dépasse le domaine entier sûr de l'interface.");
|
|
callback(emptyTransactionPage(draw));
|
|
frontendError("main", "Store Desk transaction DataTables exact count exceeds frontend safe integer domain");
|
|
return;
|
|
}
|
|
callback({ data: response.rows, draw, recordsFiltered, recordsTotal });
|
|
frontendDebug("main", "Store Desk transaction DataTables server-side query completed", { rowCount: response.rows.length });
|
|
} catch {
|
|
setTransactionQueryError("La requête RAW Transactions a échoué.");
|
|
callback(emptyTransactionPage(draw));
|
|
frontendError("main", "Store Desk transaction DataTables server-side query failed");
|
|
}
|
|
}
|
|
|
|
function retentionBadge(retentionState: string): string {
|
|
const label = retentionState === "full" ? "Full" : retentionState === "archived" ? "Archived" : retentionState === "purged" ? "Purged" : retentionState;
|
|
const className = retentionState === "full" ? "text-bg-success" : retentionState === "archived" ? "text-bg-info" : retentionState === "purged" ? "text-bg-secondary" : "text-bg-warning";
|
|
return `<span class="badge ${className}">${label}</span>`;
|
|
}
|
|
|
|
function initializeTransactionTable(): void {
|
|
transactionTable = new DataTable("#rawTransactionsTable", {
|
|
ajax: (data, callback) => {
|
|
const request = data as unknown as DataTablesPageRequest;
|
|
void queryTransactions(request, response => callback(response));
|
|
},
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: "signature", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-signature") : String(data)) },
|
|
{ className: "font-monospace", data: "slotDecimal" },
|
|
{ data: "blockTimeUnixMillisDecimal", defaultContent: "—" },
|
|
{ data: "formatId" },
|
|
{ data: "formatVersion" },
|
|
{ data: "payloadSizeDecimal", defaultContent: "—" },
|
|
{ data: "contentHash", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "transaction-content-hash") : String(data)) },
|
|
{ data: "retentionState", render: data => retentionBadge(String(data)) },
|
|
{
|
|
data: null,
|
|
defaultContent: "",
|
|
render: (_data, _type, row) => {
|
|
const transaction = row as StoreTransactionRowDto;
|
|
return `<button class="btn btn-sm btn-outline-primary" type="button" data-transaction-detail="${escapeHtml(transaction.signature)}"><i class="fa-solid fa-magnifying-glass me-1" aria-hidden="true"></i>Détail</button>`;
|
|
},
|
|
},
|
|
],
|
|
info: true,
|
|
lengthMenu: [25, 50, 100],
|
|
ordering: false,
|
|
pageLength: 25,
|
|
paging: true,
|
|
processing: true,
|
|
searching: false,
|
|
serverSide: true,
|
|
scrollX: true,
|
|
language: {
|
|
emptyTable: "Aucune transaction RAW ne correspond à la requête Store.",
|
|
info: "_START_ à _END_ sur _TOTAL_ transaction(s)",
|
|
infoEmpty: "0 transaction",
|
|
lengthMenu: "Afficher _MENU_",
|
|
loadingRecords: "Chargement Store…",
|
|
processing: "Inspection Store…",
|
|
zeroRecords: "Aucune transaction RAW correspondante.",
|
|
},
|
|
}) as unknown as StoreDataTable;
|
|
transactionTable.on("page", () => {
|
|
frontendDebug("main", "Store Desk transaction DataTables page changed");
|
|
});
|
|
transactionTable.on("length", () => {
|
|
frontendDebug("main", "Store Desk transaction DataTables page length changed");
|
|
});
|
|
transactionTable.on("draw", () => {
|
|
frontendTrace("main", "Store Desk transaction DataTables redraw completed");
|
|
});
|
|
frontendTrace("main", "Store Desk transaction DataTable initialized", { pagingOwner: "datatables", serverSide: true });
|
|
}
|
|
|
|
function initializeAccountSkeleton(): void {
|
|
accountTable = new DataTable("#rawAccountsTable", {
|
|
autoWidth: false,
|
|
data: [],
|
|
info: true,
|
|
lengthMenu: [25, 50, 100],
|
|
ordering: false,
|
|
pageLength: 25,
|
|
paging: true,
|
|
searching: false,
|
|
serverSide: false,
|
|
scrollX: true,
|
|
language: {
|
|
emptyTable: "Aucune donnée RAW Account n'est chargée avant la tranche pre.008.",
|
|
},
|
|
}) as unknown as StoreDataTable;
|
|
frontendTrace("main", "Store Desk account DataTable skeleton initialized", { pagingOwner: "datatables", serverSide: false });
|
|
}
|
|
|
|
function renderShellStatus(status: ShellStatusDto): void {
|
|
setText("appVersionBadge", status.applicationVersion);
|
|
setText("runtimeVersion", status.applicationVersion);
|
|
setText("runtimeShellPhase", status.shellPhase);
|
|
setText("runtimeConfigDocuments", status.configDocumentCount.toString());
|
|
setText("runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback");
|
|
setText("runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non");
|
|
renderDiagnostic("runtimeDiagnostic", status.startupDiagnostic);
|
|
frontendTrace("main", "Store Desk shell status rendered", {
|
|
fallbackLoggingActive: status.fallbackLoggingActive,
|
|
shellPhase: status.shellPhase,
|
|
});
|
|
}
|
|
|
|
function renderStoreRuntimeStatus(status: StoreRuntimeStatusDto): void {
|
|
const target = status.backendKind && status.network ? `${status.backendKind} / ${status.network}` : status.network ?? status.backendKind ?? "indisponible";
|
|
const migration = status.migrationVersionDecimal === null ? `inconnue · ${status.pendingMigrationCount} pending` : `v${status.migrationVersionDecimal} · ${status.pendingMigrationCount} pending`;
|
|
const pool = `${status.poolAvailable}/${status.poolSize}/${status.poolCapacity} · wait ${status.poolWaiting}`;
|
|
const profile = status.profileId ?? "indisponible";
|
|
setText("overviewStoreProfile", profile);
|
|
setText("overviewStoreTarget", target);
|
|
setText("overviewStoreHealth", status.healthState);
|
|
setText("overviewStorePool", pool);
|
|
setText("overviewStoreMigration", migration);
|
|
renderDiagnostic("overviewStoreDiagnostic", status.diagnostic);
|
|
setText("runtimeStoreProfile", profile);
|
|
setText("runtimeStoreTarget", target);
|
|
setText("runtimeStoreHealth", status.healthState);
|
|
setText("runtimeStoreMigration", migration);
|
|
setText("runtimeStorePool", pool);
|
|
renderDiagnostic("runtimeStoreDiagnostic", status.diagnostic);
|
|
frontendTrace("main", "Store Desk Store runtime status rendered", {
|
|
healthState: status.healthState,
|
|
pendingMigrationCount: status.pendingMigrationCount,
|
|
poolCapacity: status.poolCapacity,
|
|
poolSize: status.poolSize,
|
|
storeOpen: status.storeOpen,
|
|
});
|
|
}
|
|
|
|
async function refreshStoreRuntime(): Promise<void> {
|
|
frontendDebug("main", "Store Desk Store runtime refresh started");
|
|
try {
|
|
const status = await invokeKsp<StoreRuntimeStatusDto>("main", "store_runtime_status");
|
|
renderStoreRuntimeStatus(status);
|
|
frontendDebug("main", "Store Desk Store runtime refresh completed", { healthState: status.healthState, storeOpen: status.storeOpen });
|
|
} catch {
|
|
frontendError("main", "Store Desk Store runtime refresh failed");
|
|
}
|
|
}
|
|
|
|
async function refreshDiagnostics(): Promise<void> {
|
|
frontendDebug("main", "Store Desk diagnostics refresh started");
|
|
try {
|
|
const status = await invokeKsp<ShellStatusDto>("main", "get_shell_status");
|
|
renderShellStatus(status);
|
|
await refreshStoreRuntime();
|
|
frontendDebug("main", "Store Desk diagnostics refresh completed");
|
|
} catch {
|
|
frontendError("main", "Store Desk diagnostics refresh failed");
|
|
}
|
|
}
|
|
|
|
function clearTransactionDetail(): void {
|
|
for (const elementId of [
|
|
"transactionDetailSignature",
|
|
"transactionDetailSlot",
|
|
"transactionDetailBlockTime",
|
|
"transactionDetailFormat",
|
|
"transactionDetailContentHash",
|
|
"transactionDetailRetention",
|
|
"transactionDetailPayloadSize",
|
|
"transactionDetailPayloadPreview",
|
|
]) {
|
|
setText(elementId, "—");
|
|
}
|
|
setVisible("transactionDetailError", false);
|
|
setVisible("transactionDetailPreviewTruncated", false);
|
|
const payloadPreviewCopy = document.querySelector<HTMLButtonElement>("#transactionDetailPayloadPreviewCopy");
|
|
if (payloadPreviewCopy) {
|
|
delete payloadPreviewCopy.dataset.copyLongText;
|
|
delete payloadPreviewCopy.dataset.copyField;
|
|
payloadPreviewCopy.hidden = true;
|
|
}
|
|
}
|
|
|
|
function renderTransactionDetail(detail: StoreTransactionDetailDto): void {
|
|
setCopyableLongText("transactionDetailSignature", detail.signature, "transaction-detail-signature");
|
|
setText("transactionDetailSlot", detail.slotDecimal);
|
|
setText("transactionDetailBlockTime", detail.blockTimeUnixMillisDecimal ?? "—");
|
|
setText("transactionDetailFormat", `${detail.formatId} v${detail.formatVersion}`);
|
|
setCopyableLongText("transactionDetailContentHash", detail.contentHash, "transaction-detail-content-hash");
|
|
setText("transactionDetailRetention", detail.retentionState);
|
|
setText("transactionDetailPayloadSize", detail.payloadSizeDecimal ?? "—");
|
|
const payloadPreview = detail.payloadPreviewHex;
|
|
setText("transactionDetailPayloadPreview", payloadPreview ?? "Payload non disponible dans cet état de rétention.");
|
|
const payloadPreviewCopy = document.querySelector<HTMLButtonElement>("#transactionDetailPayloadPreviewCopy");
|
|
if (payloadPreviewCopy && payloadPreview) {
|
|
payloadPreviewCopy.dataset.copyLongText = payloadPreview;
|
|
payloadPreviewCopy.dataset.copyField = "transaction-payload-preview";
|
|
payloadPreviewCopy.hidden = false;
|
|
} else if (payloadPreviewCopy) {
|
|
delete payloadPreviewCopy.dataset.copyLongText;
|
|
delete payloadPreviewCopy.dataset.copyField;
|
|
payloadPreviewCopy.hidden = true;
|
|
}
|
|
setVisible("transactionDetailPreviewTruncated", detail.payloadPreviewTruncated);
|
|
}
|
|
|
|
async function openTransactionDetail(signature: string): Promise<void> {
|
|
const modalElement = document.querySelector<HTMLElement>("#transactionDetailModal");
|
|
if (!modalElement) {
|
|
return;
|
|
}
|
|
clearTransactionDetail();
|
|
setCopyableLongText("transactionDetailSignature", signature, "transaction-detail-signature");
|
|
frontendDebug("main", "Store Desk transaction detail load requested");
|
|
const modal = Modal.getOrCreateInstance(modalElement);
|
|
modal.show();
|
|
const request: StoreTransactionDetailRequestDto = { signature };
|
|
try {
|
|
const detail = await invokeKsp<StoreTransactionDetailDto>("main", "store_get_transaction_detail", { request });
|
|
renderTransactionDetail(detail);
|
|
frontendDebug("main", "Store Desk transaction detail opened", { payloadPreviewTruncated: detail.payloadPreviewTruncated, retentionState: detail.retentionState });
|
|
} catch {
|
|
setText("transactionDetailError", "Le détail de cette transaction n'a pas pu être chargé.");
|
|
setVisible("transactionDetailError", true);
|
|
frontendError("main", "Store Desk transaction detail load failed");
|
|
}
|
|
}
|
|
|
|
function redrawTransactions(resetPaging: boolean, reason: string): void {
|
|
frontendDebug("main", "Store Desk transaction DataTables redraw requested", { reason });
|
|
transactionTable?.draw(resetPaging);
|
|
}
|
|
|
|
function installInteractions(): void {
|
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
|
button.addEventListener("click", () => {
|
|
const viewId = button.dataset.view;
|
|
frontendDebug("main", "Store Desk navigation button clicked", { viewId: viewId ?? "missing" });
|
|
if (isViewId(viewId)) {
|
|
activateView(viewId);
|
|
}
|
|
});
|
|
});
|
|
const refreshOverview = document.querySelector<HTMLButtonElement>("#refreshOverview");
|
|
refreshOverview?.addEventListener("click", () => {
|
|
frontendDebug("main", "Store Desk Overview refresh button clicked");
|
|
void refreshStoreRuntime();
|
|
});
|
|
const refreshTransactions = document.querySelector<HTMLButtonElement>("#refreshTransactions");
|
|
refreshTransactions?.addEventListener("click", () => {
|
|
frontendDebug("main", "Store Desk RAW Transactions refresh button clicked");
|
|
redrawTransactions(false, "refresh");
|
|
});
|
|
for (const elementId of ["transactionSlotMin", "transactionSlotMax", "transactionDirection"]) {
|
|
const control = document.querySelector<HTMLElement>(`#${elementId}`);
|
|
control?.addEventListener("change", () => {
|
|
frontendDebug("main", "Store Desk transaction Store filter changed", { controlId: elementId });
|
|
redrawTransactions(true, "store-filter");
|
|
});
|
|
}
|
|
const refreshButton = document.querySelector<HTMLButtonElement>("#refreshDiagnostics");
|
|
refreshButton?.addEventListener("click", () => {
|
|
frontendDebug("main", "Store Desk diagnostics refresh button clicked");
|
|
void refreshDiagnostics();
|
|
});
|
|
document.addEventListener("click", event => {
|
|
const eventTarget = event.target;
|
|
if (!(eventTarget instanceof Element)) {
|
|
return;
|
|
}
|
|
const copyButton = eventTarget.closest<HTMLButtonElement>("[data-copy-long-text]");
|
|
if (copyButton) {
|
|
const value = copyButton.dataset.copyLongText;
|
|
const fieldId = copyButton.dataset.copyField ?? "long-text";
|
|
if (value) {
|
|
void copyLongText(copyButton, value, fieldId);
|
|
}
|
|
return;
|
|
}
|
|
const detailButton = eventTarget.closest<HTMLElement>("[data-transaction-detail]");
|
|
if (detailButton) {
|
|
const signature = detailButton.dataset.transactionDetail;
|
|
if (signature) {
|
|
void openTransactionDetail(signature);
|
|
}
|
|
return;
|
|
}
|
|
const control = eventTarget.closest<HTMLElement>("button, [role='tab'], [data-bs-toggle='tab']");
|
|
if (!control) {
|
|
return;
|
|
}
|
|
if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics" || control.id === "refreshOverview" || control.id === "refreshTransactions")) {
|
|
return;
|
|
}
|
|
const controlId = control.id || control.getAttribute("data-bs-target") || control.getAttribute("aria-controls") || "anonymous";
|
|
if (control.matches("[role='tab'], [data-bs-toggle='tab']")) {
|
|
frontendDebug("main", "Store Desk tab control clicked", { controlId });
|
|
return;
|
|
}
|
|
frontendDebug("main", "Store Desk generic button clicked", { buttonId: controlId });
|
|
});
|
|
document.addEventListener("shown.bs.tab", event => {
|
|
const target = event.target;
|
|
const tabId = target instanceof HTMLElement ? target.id || target.getAttribute("data-bs-target") || target.textContent?.trim() || "anonymous" : "unknown";
|
|
frontendDebug("main", "Store Desk tab activated", { tabId });
|
|
});
|
|
document.addEventListener("change", event => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement)) {
|
|
return;
|
|
}
|
|
frontendDebug("main", "Store Desk interactive control changed", {
|
|
controlId: target.id || target.getAttribute("name") || "anonymous",
|
|
controlType: target instanceof HTMLSelectElement ? "select" : target.type || "text",
|
|
});
|
|
});
|
|
const detailModal = document.querySelector<HTMLElement>("#transactionDetailModal");
|
|
detailModal?.addEventListener("hidden.bs.modal", () => {
|
|
clearTransactionDetail();
|
|
frontendDebug("main", "Store Desk transaction detail closed");
|
|
});
|
|
frontendTrace("main", "Store Desk frontend interactions installed", { buttons: true, controls: true, datatables: true, tabs: true });
|
|
}
|
|
|
|
async function initializeMain(): Promise<void> {
|
|
frontendInfo("main", "Store Desk main frontend loaded");
|
|
initializeTransactionTable();
|
|
initializeAccountSkeleton();
|
|
installInteractions();
|
|
activateView("overview");
|
|
await refreshDiagnostics();
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
void initializeMain();
|
|
});
|