Files
X-Financial/web/src/composables/useDocumentCenterInbox.js

200 lines
5.1 KiB
JavaScript
Raw Normal View History

import { computed, ref } from 'vue'
import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims, fetchExpenseClaims } from '../services/reimbursements.js'
import {
DOCUMENT_VIEWED_KEYS_CHANGE_EVENT,
countNewDocuments,
readViewedDocumentKeys,
resolveDocumentNewKey
} from '../utils/documentCenterNewState.js'
import { mapExpenseClaimToRequest } from './useRequests.js'
const SOURCE_PRIORITY = {
owned: 1,
approval: 2,
archive: 3
}
const documentRows = ref([])
const viewedDocumentKeys = ref(readViewedDocumentKeys())
const loading = ref(false)
const INBOX_CACHE_TTL_MS = 30000
let refreshTimer = null
let refreshPromise = null
let lastRefreshAt = 0
let viewedKeysListenerAttached = false
function normalizeClaimText(...values) {
for (const value of values) {
const normalized = String(value || '').trim()
if (normalized) {
return normalized
}
}
return ''
}
function buildDocumentInboxRow(claim, source) {
const request = mapExpenseClaimToRequest(claim)
const claimId = normalizeClaimText(request.claimId, request.id, claim?.id, claim?.claim_id)
const documentNo = normalizeClaimText(request.documentNo, request.claimNo, request.id, claim?.claim_no)
const documentKey = normalizeClaimText(claimId, documentNo)
return documentKey
? {
source,
claimId: claimId || documentKey,
documentNo,
documentKey: `${source}:${documentKey}`
}
: null
}
function sourcePriority(row) {
return SOURCE_PRIORITY[row?.source] || 0
}
function mergeNonArchivedRows(rows) {
const rowMap = new Map()
rows.filter(Boolean).forEach((row) => {
const key = normalizeClaimText(row.claimId, row.documentNo, row.documentKey)
if (!key) {
return
}
const current = rowMap.get(key)
if (!current || sourcePriority(row) >= sourcePriority(current)) {
rowMap.set(key, row)
}
})
return Array.from(rowMap.values())
}
function uniqueRowsByNewKey(rows) {
const seenKeys = new Set()
return rows.filter((row) => {
const key = resolveDocumentNewKey(row)
if (!key || seenKeys.has(key)) {
return false
}
seenKeys.add(key)
return true
})
}
function mapClaimsToRows(claims, source) {
return Array.isArray(claims)
? claims.map((claim) => buildDocumentInboxRow(claim, source)).filter(Boolean)
: []
}
export function buildDocumentInboxRows({ ownedClaims = [], approvalClaims = [], archivedClaims = [] } = {}) {
const ownedRows = mapClaimsToRows(ownedClaims, 'owned')
const approvalRows = mapClaimsToRows(approvalClaims, 'approval')
const archiveRows = mapClaimsToRows(archivedClaims, 'archive')
return uniqueRowsByNewKey([
...mergeNonArchivedRows([...ownedRows, ...approvalRows]),
...archiveRows
])
}
function refreshViewedDocumentKeys() {
viewedDocumentKeys.value = readViewedDocumentKeys()
}
function attachViewedKeysListener() {
if (typeof window === 'undefined' || viewedKeysListenerAttached) {
return
}
window.addEventListener(DOCUMENT_VIEWED_KEYS_CHANGE_EVENT, refreshViewedDocumentKeys)
viewedKeysListenerAttached = true
}
async function readClaimList(fetcher) {
const result = await fetcher()
return Array.isArray(result) ? result : []
}
export function useDocumentCenterInbox() {
attachViewedKeysListener()
const unreadCount = computed(() => countNewDocuments(documentRows.value, viewedDocumentKeys.value))
const hasUnread = computed(() => unreadCount.value > 0)
async function refreshDocumentInbox(options = {}) {
const force = Boolean(options.force)
const now = Date.now()
if (refreshPromise) {
return refreshPromise
}
if (!force && lastRefreshAt && now - lastRefreshAt < INBOX_CACHE_TTL_MS) {
refreshViewedDocumentKeys()
return documentRows.value
}
loading.value = true
refreshPromise = (async () => {
const [ownedResult, approvalResult, archiveResult] = await Promise.allSettled([
readClaimList(fetchExpenseClaims),
readClaimList(fetchApprovalExpenseClaims),
readClaimList(fetchArchivedExpenseClaims)
])
documentRows.value = buildDocumentInboxRows({
ownedClaims: ownedResult.status === 'fulfilled' ? ownedResult.value : [],
approvalClaims: approvalResult.status === 'fulfilled' ? approvalResult.value : [],
archivedClaims: archiveResult.status === 'fulfilled' ? archiveResult.value : []
})
lastRefreshAt = Date.now()
refreshViewedDocumentKeys()
return documentRows.value
})()
try {
return await refreshPromise
} finally {
loading.value = false
refreshPromise = null
}
}
function startDocumentInboxPolling(intervalMs = 120000) {
stopDocumentInboxPolling()
if (typeof window === 'undefined') {
return
}
refreshTimer = window.setInterval(() => {
void refreshDocumentInbox()
}, intervalMs)
}
function stopDocumentInboxPolling() {
if (refreshTimer && typeof window !== 'undefined') {
window.clearInterval(refreshTimer)
refreshTimer = null
}
}
return {
hasUnread,
loading,
refreshDocumentInbox,
startDocumentInboxPolling,
stopDocumentInboxPolling,
unreadCount
}
}