feat: sort and paginate usage history
This commit is contained in:
109
app.js
109
app.js
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user