feat: sort and paginate usage history

This commit is contained in:
Wyndham ARR
2026-08-03 17:06:58 +08:00
parent 84443d5dba
commit a9348b8d3e
4 changed files with 138 additions and 20 deletions

109
app.js
View File

@@ -63,10 +63,12 @@ const LANGUAGE_CONFIG = Object.freeze({
});
const SUPPORTED_LANGUAGES = Object.freeze(Object.keys(LANGUAGE_CONFIG));
const OWNER_PAGE_SIZE = 50;
const USAGE_PAGE_SIZE = 30;
let detailOwnerId = null;
let activePage = "dashboard";
let activeOwnerView = "accounts";
let ownerPage = 1;
let usagePage = 1;
let stayReturnOwnerId = null;
let stayOwnerLocked = false;
let toastTimer;
@@ -83,22 +85,51 @@ let sessionErrorTimer = null;
let loginErrorKey = null;
let sessionErrorKey = null;
function dateOnlyMs(value) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ""))) return Number.NaN;
const [year, month, day] = String(value).split("-").map(Number);
const timestamp = Date.UTC(year, month - 1, day);
const date = new Date(timestamp);
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
? timestamp
: Number.NaN;
}
function todayDateOnlyMs() {
const now = new Date();
return Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
}
function usageDateDistance(record, todayMs) {
const checkinMs = dateOnlyMs(record.checkin);
const checkoutMs = dateOnlyMs(record.checkout);
if (!Number.isFinite(checkinMs)) return Number.MAX_SAFE_INTEGER;
const startMs = Math.min(checkinMs, Number.isFinite(checkoutMs) ? checkoutMs : checkinMs);
const endMs = Math.max(checkinMs, Number.isFinite(checkoutMs) ? checkoutMs : checkinMs);
if (todayMs < startMs) return startMs - todayMs;
if (todayMs > endMs) return todayMs - endMs;
return 0;
}
function compareDateDescending(aValue, bValue) {
const aMs = dateOnlyMs(aValue);
const bMs = dateOnlyMs(bValue);
if (Number.isFinite(aMs) && Number.isFinite(bMs)) return bMs - aMs;
if (Number.isFinite(aMs)) return -1;
if (Number.isFinite(bMs)) return 1;
return 0;
}
function usageRecordsInDisplayOrder(records) {
const todayMs = todayDateOnlyMs();
return [...records].sort((a, b) => {
if (a.recordSource === "api" || b.recordSource === "api") {
const createdDifference = String(b.enteredAt || "").localeCompare(String(a.enteredAt || ""));
return createdDifference || String(b.id || "").localeCompare(String(a.id || ""));
}
const aIsManual = a.recordSource === "manual";
const bIsManual = b.recordSource === "manual";
if (aIsManual !== bIsManual) return aIsManual ? -1 : 1;
if (aIsManual) {
const sequenceDifference = (b.entrySequence || 0) - (a.entrySequence || 0);
return sequenceDifference || String(b.enteredAt || "").localeCompare(String(a.enteredAt || ""));
}
return (a.importOrder ?? Number.MAX_SAFE_INTEGER) - (b.importOrder ?? Number.MAX_SAFE_INTEGER);
const distanceDifference = usageDateDistance(a, todayMs) - usageDateDistance(b, todayMs);
if (distanceDifference) return distanceDifference;
const checkinDifference = compareDateDescending(a.checkin, b.checkin);
if (checkinDifference) return checkinDifference;
const checkoutDifference = compareDateDescending(a.checkout, b.checkout);
if (checkoutDifference) return checkoutDifference;
return String(b.id || "").localeCompare(String(a.id || ""));
});
}
@@ -266,6 +297,8 @@ const I18N = {
nextPage: "Next page",
goToPage: ({ page }) => `Go to page ${formatNumber(page)}`,
ownerPageStatus: ({ from, to, total, page, totalPages }) => `${formatNumber(from)}-${formatNumber(to)} of ${formatNumber(total)} · Page ${formatNumber(page)} of ${formatNumber(totalPages)}`,
usagePaginationAria: "Usage history pages",
usagePageStatus: ({ from, to, total, page, totalPages }) => `${formatNumber(from)}-${formatNumber(to)} of ${formatNumber(total)} records · Page ${formatNumber(page)} of ${formatNumber(totalPages)}`,
recordsShown: ({ count }) => `${formatNumber(count)} record${count === 1 ? "" : "s"} shown`,
viewAccount: ({ name }) => `View ${name} account`,
openAccount: ({ name }) => `Open ${name} account`,
@@ -483,6 +516,8 @@ const I18N = {
nextPage: "下一页",
goToPage: ({ page }) => `转到第 ${formatNumber(page)}`,
ownerPageStatus: ({ from, to, total, page, totalPages }) => `${formatNumber(page)} / ${formatNumber(totalPages)} 页 · 显示 ${formatNumber(from)}-${formatNumber(to)},共 ${formatNumber(total)} 个账户`,
usagePaginationAria: "使用记录分页",
usagePageStatus: ({ from, to, total, page, totalPages }) => `${formatNumber(page)} / ${formatNumber(totalPages)} 页 · 显示 ${formatNumber(from)}-${formatNumber(to)},共 ${formatNumber(total)} 条记录`,
recordsShown: ({ count }) => `显示 ${formatNumber(count)} 条记录`,
viewAccount: ({ name }) => `查看 ${name} 的账户`,
openAccount: ({ name }) => `打开 ${name} 的账户`,
@@ -700,6 +735,8 @@ const I18N = {
nextPage: "หน้าถัดไป",
goToPage: ({ page }) => `ไปหน้าที่ ${formatNumber(page)}`,
ownerPageStatus: ({ from, to, total, page, totalPages }) => `หน้า ${formatNumber(page)} จาก ${formatNumber(totalPages)} · แสดง ${formatNumber(from)}-${formatNumber(to)} จาก ${formatNumber(total)} บัญชี`,
usagePaginationAria: "หน้าประวัติการใช้สิทธิ์",
usagePageStatus: ({ from, to, total, page, totalPages }) => `หน้า ${formatNumber(page)} จาก ${formatNumber(totalPages)} · แสดง ${formatNumber(from)}-${formatNumber(to)} จาก ${formatNumber(total)} รายการ`,
recordsShown: ({ count }) => `แสดง ${formatNumber(count)} รายการ`,
viewAccount: ({ name }) => `ดูบัญชีของ ${name}`,
openAccount: ({ name }) => `เปิดบัญชีของ ${name}`,
@@ -1386,11 +1423,17 @@ function renderStays() {
const haystack = `${record.confirmation} ${owner.name} ${owner.room} ${owner.member} ${owner.unit} ${record.usedType} ${record.remark || ""}`.toLowerCase();
return haystack.includes(query) && (!type || record.usedType === type);
}));
const totalPages = Math.max(1, Math.ceil(filtered.length / USAGE_PAGE_SIZE));
usagePage = Math.min(Math.max(usagePage, 1), totalPages);
const pageOffset = (usagePage - 1) * USAGE_PAGE_SIZE;
const visibleStays = filtered.slice(pageOffset, pageOffset + USAGE_PAGE_SIZE);
const pageFrom = filtered.length ? pageOffset + 1 : 0;
const pageTo = Math.min(pageOffset + USAGE_PAGE_SIZE, filtered.length);
$("#stayCount").textContent = t("recordsShown", { count: filtered.length });
$("#stayCount").textContent = t("recordsShown", { count: visibleStays.length });
$("#usageTabCount").textContent = formatNumber(stayRecords.length);
const databaseEmpty = IS_API_MODE && connectionState === "ready" && stayRecords.length === 0;
$("#stayTableBody").innerHTML = filtered.length ? filtered.map(record => {
$("#stayTableBody").innerHTML = visibleStays.length ? visibleStays.map(record => {
const owner = ownerFor(record.ownerId);
return `<tr class="clickable-row" data-owner-id="${owner.id}" tabindex="0">
<td data-label="${t("confirmationNo")}"><strong class="mono">${record.confirmation}</strong></td>
@@ -1409,6 +1452,30 @@ function renderStays() {
</tr>`;
}).join("") : `<tr class="empty-table-row"><td colspan="10"><div class="table-empty"><strong>${t(databaseEmpty ? "noDatabaseUsage" : "noUsageRecordsFound")}</strong><span>${t(databaseEmpty ? "noDatabaseUsageDescription" : "tryUsageFilter")}</span></div></td></tr>`;
const pagination = $("#stayPagination");
if (pagination) {
pagination.hidden = totalPages <= 1;
$("#stayPageStatus").textContent = filtered.length
? t("usagePageStatus", { from: pageFrom, to: pageTo, total: filtered.length, page: usagePage, totalPages })
: "";
const pageNumbers = $("#stayPageNumbers");
if (pageNumbers) {
pageNumbers.hidden = totalPages <= 1;
pageNumbers.innerHTML = totalPages > 1
? Array.from({ length: totalPages }, (_, index) => {
const page = index + 1;
const current = page === usagePage;
return `<button class="pagination-number" type="button" data-usage-page="${page}" aria-label="${escapeHtml(t("goToPage", { page }))}"${current ? ' aria-current="page"' : ""}>${formatNumber(page)}</button>`;
}).join("")
: "";
$$("#stayPageNumbers [data-usage-page]").forEach(button => button.addEventListener("click", () => {
usagePage = Number(button.dataset.usagePage);
renderStays();
}));
}
}
$("#stayTableWrap").scrollTop = 0;
$$("#stayTableBody tr[data-owner-id]").forEach(row => {
bindTableRowActivation(row, () => openOwnerDrawer(row.dataset.ownerId));
});
@@ -2081,8 +2148,14 @@ function bindEvents() {
ownerPage = 1;
renderOwners();
});
$("#staySearch").addEventListener("input", renderStays);
$("#stayTypeFilter").addEventListener("change", renderStays);
$("#staySearch").addEventListener("input", () => {
usagePage = 1;
renderStays();
});
$("#stayTypeFilter").addEventListener("change", () => {
usagePage = 1;
renderStays();
});
document.addEventListener("keydown", event => {
if (event.key === "Escape") {
if ($("#confirmDialog").classList.contains("open")) closeConfirmDialog(false);