diff --git a/app.js b/app.js index 8884d23..8d0b744 100644 --- a/app.js +++ b/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 `
| Confirmation No. | Owner / room | Check-in | Check-out | Night | Use | Balance | Used Room Type | Remark | Actions |
|---|