Files
X-Financial/web/src/services/reimbursements.js
2026-07-16 15:34:58 +08:00

285 lines
9.5 KiB
JavaScript

import { apiRequest } from './api.js'
export const REIMBURSEMENT_LIST_PREVIEW_PARAMS = Object.freeze({ page: 1, pageSize: 100 })
export const REIMBURSEMENT_LIST_FULL_PAGE_SIZE = 100
const REIMBURSEMENT_LIST_MAX_PAGES = 200
function buildListQuery(params = {}) {
const search = new URLSearchParams()
const page = params.page
const pageSize = params.pageSize || params.page_size
if (page) {
search.set('page', String(page))
}
if (pageSize) {
search.set('page_size', String(pageSize))
}
const query = search.toString()
return query ? `?${query}` : ''
}
export function extractExpenseClaimItems(payload) {
if (Array.isArray(payload)) {
return payload
}
return Array.isArray(payload?.items) ? payload.items : []
}
function isPaginatedExpenseClaimPayload(payload) {
return Boolean(
payload
&& typeof payload === 'object'
&& Array.isArray(payload.items)
&& Number.isFinite(Number(payload.page))
&& Number.isFinite(Number(payload.page_size))
)
}
async function fetchAllExpenseClaimPages(fetchPage, params = {}) {
const pageSize = Number(params.pageSize || params.page_size || REIMBURSEMENT_LIST_FULL_PAGE_SIZE)
const normalizedPageSize = Number.isFinite(pageSize) && pageSize > 0
? Math.min(REIMBURSEMENT_LIST_FULL_PAGE_SIZE, Math.floor(pageSize))
: REIMBURSEMENT_LIST_FULL_PAGE_SIZE
let page = Math.max(1, Math.floor(Number(params.page || 1) || 1))
const items = []
for (let index = 0; index < REIMBURSEMENT_LIST_MAX_PAGES; index += 1) {
const payload = await fetchPage({
...params,
page,
pageSize: normalizedPageSize
})
if (!isPaginatedExpenseClaimPayload(payload)) {
return extractExpenseClaimItems(payload)
}
items.push(...extractExpenseClaimItems(payload))
const total = Number(payload.total || 0)
const totalPages = Number(payload.total_pages || 0)
const hasNext = Boolean(payload.has_next)
if (!hasNext || (totalPages && page >= totalPages) || (total && items.length >= total)) {
break
}
page += 1
}
return items
}
export function fetchExpenseClaims(params = {}, options = {}) {
return apiRequest(`/reimbursements/claims${buildListQuery(params)}`, options)
}
export function fetchAllExpenseClaims(params = {}) {
return fetchAllExpenseClaimPages(fetchExpenseClaims, params)
}
export function fetchApprovalExpenseClaims(params = {}, options = {}) {
return apiRequest(`/reimbursements/claims/approvals${buildListQuery(params)}`, options)
}
export function fetchAllApprovalExpenseClaims(params = {}) {
return fetchAllExpenseClaimPages(fetchApprovalExpenseClaims, params)
}
export function fetchArchivedExpenseClaims(params = {}, options = {}) {
return apiRequest(`/reimbursements/claims/archives${buildListQuery(params)}`, options)
}
export function fetchAllArchivedExpenseClaims(params = {}) {
return fetchAllExpenseClaimPages(fetchArchivedExpenseClaims, params)
}
export function fetchExpenseClaimDetail(claimId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}`)
}
export function fetchExpenseClaimBudgetAnalysis(claimId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/budget-analysis`)
}
export function fetchEmployeeLatestProfile(employeeId, params = {}) {
const query = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
const normalized = String(value ?? '').trim()
if (normalized) {
query.set(key, normalized)
}
})
const suffix = query.toString() ? `?${query.toString()}` : ''
return apiRequest(`/employee-profiles/${encodeURIComponent(String(employeeId || '').trim())}/latest${suffix}`)
}
export function fetchCurrentEmployeeLatestProfile(params = {}) {
const query = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
const normalized = String(value ?? '').trim()
if (normalized) {
query.set(key, normalized)
}
})
const suffix = query.toString() ? `?${query.toString()}` : ''
return apiRequest(`/employee-profiles/me/latest${suffix}`)
}
export function updateExpenseClaim(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}`, {
method: 'PATCH',
body: JSON.stringify(payload)
})
}
export function acceptExpenseClaimStandardAdjustment(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/standard-adjustment`, {
method: 'POST',
body: JSON.stringify(payload)
})
}
export function calculateTravelReimbursement(payload = {}) {
return apiRequest('/reimbursements/travel-calculator', {
method: 'POST',
body: JSON.stringify(payload)
})
}
export function createExpenseClaimItem(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items`, {
method: 'POST',
body: JSON.stringify(payload)
})
}
export function deleteExpenseClaimItem(claimId, itemId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}`, {
method: 'DELETE'
})
}
export function uploadExpenseClaimItemAttachment(claimId, itemId, file) {
const formData = new FormData()
formData.append('file', file)
if (file?.receiptId) {
formData.append('receipt_id', String(file.receiptId))
}
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}/attachment`, {
method: 'POST',
body: formData,
contentType: null
})
}
export function fetchExpenseClaimItemAttachmentMeta(claimId, itemId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}/attachment/meta`)
}
export function fetchExpenseClaimItemAttachmentPreview(claimId, itemId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}/attachment/preview`, {
responseType: 'blob'
})
}
export function fetchExpenseClaimItemAttachment(claimId, itemId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}/attachment`, {
responseType: 'blob'
})
}
export async function fetchExpenseClaimAttachmentAsset(pathOrUrl) {
const target = String(pathOrUrl || '').trim()
if (!target) {
throw new Error('预览地址为空。')
}
if (/^https?:\/\//i.test(target)) {
const response = await fetch(target)
if (!response.ok) {
throw new Error(`预览加载失败(${response.status})。`)
}
return response.blob()
}
return apiRequest(target, {
responseType: 'blob'
})
}
export function deleteExpenseClaimItemAttachment(claimId, itemId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}/attachment`, {
method: 'DELETE'
})
}
export function updateExpenseClaimItem(claimId, itemId, payload) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/items/${encodeURIComponent(String(itemId || '').trim())}`, {
method: 'PATCH',
body: JSON.stringify(payload)
})
}
function createPreReviewRequestId() {
if (typeof globalThis.crypto?.randomUUID === 'function') {
return `pre-review:${globalThis.crypto.randomUUID()}`
}
return `pre-review:${Date.now()}:${Math.random().toString(16).slice(2)}`
}
export function createExpenseClaimActionRequestId(action, claimId) {
const normalizedAction = String(action || 'action').trim() || 'action'
const normalizedClaimId = String(claimId || 'claim').trim() || 'claim'
if (typeof globalThis.crypto?.randomUUID === 'function') {
return `${normalizedAction}:${normalizedClaimId}:${globalThis.crypto.randomUUID()}`
}
return `${normalizedAction}:${normalizedClaimId}:${Date.now()}:${Math.random().toString(16).slice(2)}`
}
export function preReviewExpenseClaim(claimId, requestId = '') {
const normalizedRequestId = String(requestId || '').trim() || createPreReviewRequestId()
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/pre-review`, {
method: 'POST',
headers: { 'X-Request-ID': normalizedRequestId },
body: JSON.stringify({})
})
}
export function submitExpenseClaim(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/submit`, {
method: 'POST',
body: JSON.stringify(payload || {})
})
}
export function returnExpenseClaim(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/return`, {
method: 'POST',
body: JSON.stringify(payload)
})
}
export function approveExpenseClaim(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/approve`, {
method: 'POST',
body: JSON.stringify(payload)
})
}
export function payExpenseClaim(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/pay`, {
method: 'POST',
body: JSON.stringify(payload)
})
}
export function deleteExpenseClaim(claimId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}`, {
method: 'DELETE'
})
}