From 8c2f301d85e417cbf3c29a2188e0e76de3a7babe Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Wed, 3 Jun 2026 16:52:49 +0800 Subject: [PATCH] fix(documents): sort newest rows first --- web/src/utils/documentCenterSort.js | 24 ++++++++++++++ web/src/views/DocumentsCenterView.vue | 13 +++++--- web/tests/document-center-sort.test.mjs | 33 +++++++++++++++++++ .../documents-center-status-filter.test.mjs | 18 ++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 web/src/utils/documentCenterSort.js create mode 100644 web/tests/document-center-sort.test.mjs diff --git a/web/src/utils/documentCenterSort.js b/web/src/utils/documentCenterSort.js new file mode 100644 index 0000000..cf50272 --- /dev/null +++ b/web/src/utils/documentCenterSort.js @@ -0,0 +1,24 @@ +function normalizeSortTime(value) { + const time = Number(value) + return Number.isFinite(time) ? time : 0 +} + +export function compareDocumentRowsByLatestTime(left, right) { + const latestDiff = normalizeSortTime(right?.sortTime) - normalizeSortTime(left?.sortTime) + if (latestDiff !== 0) { + return latestDiff + } + + const createdDiff = normalizeSortTime(right?.createdSortTime) - normalizeSortTime(left?.createdSortTime) + if (createdDiff !== 0) { + return createdDiff + } + + const rightKey = String(right?.documentKey || right?.documentNo || '').trim() + const leftKey = String(left?.documentKey || left?.documentNo || '').trim() + return rightKey.localeCompare(leftKey, 'zh-CN') +} + +export function sortDocumentRowsByLatestTime(rows) { + return (Array.isArray(rows) ? [...rows] : []).sort(compareDocumentRowsByLatestTime) +} diff --git a/web/src/views/DocumentsCenterView.vue b/web/src/views/DocumentsCenterView.vue index cbd0107..cec15f4 100644 --- a/web/src/views/DocumentsCenterView.vue +++ b/web/src/views/DocumentsCenterView.vue @@ -250,6 +250,7 @@ import { useMinimumVisibleState } from '../composables/useMinimumVisibleState.js import { mapExpenseClaimToRequest } from '../composables/useRequests.js' import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims } from '../services/reimbursements.js' import { countNewDocuments, isNewDocument, markDocumentViewed, markDocumentsViewed, readDocumentScope, readViewedDocumentKeys, writeDocumentScope } from '../utils/documentCenterNewState.js' +import { sortDocumentRowsByLatestTime } from '../utils/documentCenterSort.js' import { extractDateText, formatDocumentListTime, resolveDocumentSortTime, resolveDocumentStayTimeDisplay } from '../utils/documentCenterTime.js' import { excludeArchivedDocumentRows, filterApplicationScopeNewRows, isArchivedDocumentRow, prepareApplicationScopeRows } from '../utils/documentCenterRows.js' import { normalizeRequestForUi } from '../utils/requestViewModel.js' @@ -535,7 +536,7 @@ const statusFilterLabel = computed(() => const filteredRows = computed(() => { const keyword = listKeyword.value.trim().toLowerCase() - return activeScopeRows.value.filter((row) => { + return sortDocumentRowsByLatestTime(activeScopeRows.value.filter((row) => { const matchesKeyword = !keyword || [ row.documentNo, row.documentTypeLabel, @@ -556,7 +557,7 @@ const filteredRows = computed(() => { const matchesDateRange = matchesAppliedDateRange(row) return matchesKeyword && matchesDocumentType && matchesScene && matchesStatus && matchesDateRange - }) + })) }) const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize.value))) @@ -653,6 +654,8 @@ function buildDocumentRow(request, options = {}) { const claimId = normalized.claimId || normalized.id || documentNo const createdAtSource = normalized.createdAt || normalized.submittedAt || normalized.applyTime || normalized.updatedAt const updatedAtSource = normalized.updatedAt || normalized.submittedAt || normalized.createdAt || normalized.applyTime + const createdSortTime = resolveDocumentSortTime(createdAtSource) + const updatedSortTime = resolveDocumentSortTime(updatedAtSource) const documentTypeCode = normalized.documentTypeCode || DOCUMENT_TYPE_REIMBURSEMENT const documentTypeLabel = normalized.documentTypeLabel @@ -689,7 +692,9 @@ function buildDocumentRow(request, options = {}) { ? false : isNewDocument({ ...normalized, source: options.source || 'owned', claimId, documentNo }, viewedDocumentKeys.value), updatedAtDisplay: formatDocumentListTime(updatedAtSource), - sortTime: resolveDocumentSortTime(updatedAtSource) + createdSortTime, + updatedSortTime, + sortTime: Math.max(createdSortTime, updatedSortTime) } } @@ -751,7 +756,7 @@ function mergeDocumentRows(rows) { } }) - return Array.from(rowMap.values()).sort((left, right) => right.sortTime - left.sortTime) + return sortDocumentRowsByLatestTime(Array.from(rowMap.values())) } function resolveSourcePriority(row) { diff --git a/web/tests/document-center-sort.test.mjs b/web/tests/document-center-sort.test.mjs new file mode 100644 index 0000000..d406055 --- /dev/null +++ b/web/tests/document-center-sort.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + compareDocumentRowsByLatestTime, + sortDocumentRowsByLatestTime +} from '../src/utils/documentCenterSort.js' + +test('document center sorts newest document rows first without mutating input', () => { + const rows = [ + { documentNo: 'AP-001', sortTime: 1000 }, + { documentNo: 'AP-003', sortTime: 3000 }, + { documentNo: 'AP-002', sortTime: 2000 } + ] + + const sortedRows = sortDocumentRowsByLatestTime(rows) + + assert.deepEqual(sortedRows.map((row) => row.documentNo), ['AP-003', 'AP-002', 'AP-001']) + assert.deepEqual(rows.map((row) => row.documentNo), ['AP-001', 'AP-003', 'AP-002']) +}) + +test('document center sort falls back to created time and stable document keys', () => { + const rows = [ + { documentKey: 'owned:AP-001', documentNo: 'AP-001', sortTime: 1000, createdSortTime: 1000 }, + { documentKey: 'owned:AP-002', documentNo: 'AP-002', sortTime: 1000, createdSortTime: 2000 }, + { documentKey: 'owned:AP-003', documentNo: 'AP-003', sortTime: 1000, createdSortTime: 2000 } + ] + + const sortedRows = sortDocumentRowsByLatestTime(rows) + + assert.deepEqual(sortedRows.map((row) => row.documentNo), ['AP-003', 'AP-002', 'AP-001']) + assert.equal(compareDocumentRowsByLatestTime(rows[1], rows[2]) > 0, true) +}) diff --git a/web/tests/documents-center-status-filter.test.mjs b/web/tests/documents-center-status-filter.test.mjs index 6095e21..2377522 100644 --- a/web/tests/documents-center-status-filter.test.mjs +++ b/web/tests/documents-center-status-filter.test.mjs @@ -68,6 +68,7 @@ test('documents center category tabs map to the intended row sources', () => { assert.match(documentsCenterView, /excludeArchivedDocumentRows/) assert.match(documentsCenterView, /approvalRows\.value = excludeArchivedDocumentRows/) assert.match(documentsCenterView, /const nonArchivedRows = computed\(\(\) => mergeDocumentRows\(\[\.\.\.ownedRows\.value, \.\.\.approvalRows\.value\]\)\)/) + assert.match(documentsCenterView, /import \{ sortDocumentRowsByLatestTime \} from '..\/utils\/documentCenterSort\.js'/) assert.match(documentsCenterView, /activeScopeTab\.value !== DOCUMENT_SCOPE_ARCHIVE && isArchivedDocumentRow\(row\)/) assert.match( documentsCenterView, @@ -92,6 +93,23 @@ test('documents center category tabs map to the intended row sources', () => { assert.match(documentsCenterView, /return nonArchivedRows\.value/) }) +test('documents center sorts every filtered scope by latest document time before pagination', () => { + assert.match( + documentsCenterView, + /return sortDocumentRowsByLatestTime\(activeScopeRows\.value\.filter\(\(row\) => \{[\s\S]*matchesKeyword && matchesDocumentType && matchesScene && matchesStatus && matchesDateRange[\s\S]*\}\)\)/ + ) + assert.match( + documentsCenterView, + /const createdSortTime = resolveDocumentSortTime\(createdAtSource\)[\s\S]*const updatedSortTime = resolveDocumentSortTime\(updatedAtSource\)/ + ) + assert.match( + documentsCenterView, + /createdSortTime,[\s\S]*updatedSortTime,[\s\S]*sortTime: Math\.max\(createdSortTime, updatedSortTime\)/ + ) + assert.match(documentsCenterView, /return sortDocumentRowsByLatestTime\(Array\.from\(rowMap\.values\(\)\)\)/) + assert.doesNotMatch(documentsCenterView, /right\.sortTime - left\.sortTime/) +}) + test('documents center preserves application document type from mapped requests', () => { assert.match( documentsCenterView,