refactor(frontend): split large reimbursement and audit modules
This commit is contained in:
422
web/src/views/scripts/useTravelReimbursementReviewDrawer.js
Normal file
422
web/src/views/scripts/useTravelReimbursementReviewDrawer.js
Normal file
@@ -0,0 +1,422 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
DATE_INPUT_FORMAT,
|
||||
REVIEW_CATEGORY_PRESET_OPTIONS,
|
||||
REVIEW_OTHER_CATEGORY_OPTIONS,
|
||||
REVIEW_SCENE_OTHER_OPTION,
|
||||
buildInlineReviewChangedLines,
|
||||
buildInlineReviewState,
|
||||
buildReviewCategoryOptions,
|
||||
buildReviewDocumentDrafts,
|
||||
buildReviewDocumentSummaries,
|
||||
buildReviewPanelConfidence,
|
||||
buildReviewRecognitionNotes,
|
||||
buildReviewRecognizedLines,
|
||||
cloneReviewDocumentDrafts,
|
||||
cloneReviewEditFields,
|
||||
createEmptyInlineReviewState,
|
||||
extractAmountInputValue,
|
||||
formatConfidenceLabel,
|
||||
isValidIsoDateString,
|
||||
normalizeAmountValue,
|
||||
normalizeReviewDocumentComparableValue,
|
||||
resolveReviewCategoryConfidenceScore
|
||||
} from './travelReimbursementReviewModel.js'
|
||||
|
||||
export function useTravelReimbursementReviewDrawer({
|
||||
activeReviewPayload,
|
||||
reviewFilePreviews,
|
||||
flowSteps,
|
||||
submitting,
|
||||
reviewActionBusy,
|
||||
triggerFileUpload,
|
||||
resolveDocumentPreview,
|
||||
buildReviewFactCards,
|
||||
buildReviewRiskItems,
|
||||
buildReviewRiskSummary,
|
||||
buildReviewIntentText,
|
||||
resolveReviewRiskBriefs,
|
||||
reviewDrawerMode: externalReviewDrawerMode,
|
||||
REVIEW_DRAWER_MODE_REVIEW,
|
||||
REVIEW_DRAWER_MODE_DOCUMENTS,
|
||||
REVIEW_DRAWER_MODE_RISK,
|
||||
REVIEW_DRAWER_MODE_FLOW
|
||||
}) {
|
||||
const reviewInlineForm = ref(createEmptyInlineReviewState())
|
||||
const reviewInlineBaseForm = ref(createEmptyInlineReviewState())
|
||||
const reviewInlineBaseFields = ref([])
|
||||
const reviewInlinePendingFiles = ref([])
|
||||
const reviewInlineEditorKey = ref('')
|
||||
const reviewInlineErrors = ref({})
|
||||
const reviewOtherCategoryOpen = ref(false)
|
||||
const reviewDocumentDrafts = ref([])
|
||||
const reviewDocumentBaseDrafts = ref([])
|
||||
const activeReviewDocumentIndex = ref(0)
|
||||
const reviewDrawerMode = externalReviewDrawerMode || ref(REVIEW_DRAWER_MODE_REVIEW)
|
||||
const documentPreviewDialog = ref({
|
||||
open: false,
|
||||
filename: '',
|
||||
kind: 'file',
|
||||
url: ''
|
||||
})
|
||||
|
||||
const activeReviewFilePreviews = computed(() => reviewFilePreviews.value)
|
||||
const reviewIntentText = computed(() => buildReviewIntentText(activeReviewPayload.value))
|
||||
const reviewFactCards = computed(() => buildReviewFactCards(activeReviewPayload.value, reviewInlineForm.value))
|
||||
const reviewCategoryOptions = computed(() =>
|
||||
buildReviewCategoryOptions(activeReviewPayload.value, reviewInlineForm.value.expense_type, reviewInlineForm.value)
|
||||
)
|
||||
const reviewOtherCategoryOptions = computed(() =>
|
||||
REVIEW_OTHER_CATEGORY_OPTIONS.map((item) => ({
|
||||
...item,
|
||||
confidenceLabel: formatConfidenceLabel(
|
||||
resolveReviewCategoryConfidenceScore(activeReviewPayload.value, item.label, reviewInlineForm.value)
|
||||
)
|
||||
}))
|
||||
)
|
||||
const reviewSelectedOtherCategory = computed(() => {
|
||||
const presetLabels = REVIEW_CATEGORY_PRESET_OPTIONS.filter((item) => !item.is_other).map((item) => item.label)
|
||||
return presetLabels.includes(reviewInlineForm.value.expense_type) ? '' : reviewInlineForm.value.expense_type
|
||||
})
|
||||
const reviewInlineDirty = computed(
|
||||
() =>
|
||||
buildInlineReviewChangedLines(
|
||||
reviewInlineBaseForm.value,
|
||||
reviewInlineForm.value,
|
||||
reviewInlinePendingFiles.value
|
||||
).length > 0
|
||||
)
|
||||
const reviewPanelConfidence = computed(() => buildReviewPanelConfidence(activeReviewPayload.value, reviewInlineForm.value))
|
||||
const reviewRiskSummary = computed(() => buildReviewRiskSummary(activeReviewPayload.value))
|
||||
const reviewRiskItems = computed(() => buildReviewRiskItems(activeReviewPayload.value))
|
||||
const reviewRiskEmpty = computed(() => !reviewRiskItems.value.length)
|
||||
const reviewDocumentCount = computed(() => reviewDocumentDrafts.value.length)
|
||||
const reviewDocumentDrawerAvailable = computed(() => reviewDocumentCount.value > 0)
|
||||
const reviewRiskDrawerAvailable = computed(() => !reviewRiskEmpty.value)
|
||||
const reviewFlowDrawerAvailable = computed(() => flowSteps.value.length > 0)
|
||||
const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value))
|
||||
const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value))
|
||||
const reviewDocumentSummaries = computed(() => buildReviewDocumentSummaries(activeReviewPayload.value))
|
||||
const isReviewDocumentDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS)
|
||||
const isReviewRiskDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK)
|
||||
const isReviewFlowDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW)
|
||||
const reviewDrawerTitle = computed(() => (
|
||||
isReviewDocumentDrawer.value
|
||||
? '绁ㄦ嵁璇嗗埆缁撴灉'
|
||||
: isReviewRiskDrawer.value
|
||||
? '椋庨櫓鎻愮ず'
|
||||
: isReviewFlowDrawer.value
|
||||
? '璋冪敤娴佺▼'
|
||||
: '鎶ラ攢璇嗗埆鏍稿'
|
||||
))
|
||||
const reviewDocumentDrawerLabel = computed(() => (
|
||||
'鍗曟嵁璇嗗埆'
|
||||
))
|
||||
const reviewDocumentDrawerIcon = computed(() => (
|
||||
isReviewDocumentDrawer.value
|
||||
? 'mdi mdi-file-document-multiple'
|
||||
: 'mdi mdi-file-document-multiple-outline'
|
||||
))
|
||||
const reviewRiskDrawerLabel = computed(() => (
|
||||
'鏄剧ず椋庨櫓'
|
||||
))
|
||||
const reviewRiskDrawerIcon = computed(() => (
|
||||
isReviewRiskDrawer.value
|
||||
? 'mdi mdi-shield-alert'
|
||||
: 'mdi mdi-shield-alert-outline'
|
||||
))
|
||||
const reviewFlowDrawerLabel = computed(() => (
|
||||
'璋冪敤娴佺▼'
|
||||
))
|
||||
const reviewFlowDrawerIcon = computed(() => (
|
||||
isReviewFlowDrawer.value
|
||||
? 'mdi mdi-timeline-clock'
|
||||
: 'mdi mdi-timeline-clock-outline'
|
||||
))
|
||||
const activeReviewDocument = computed(() => reviewDocumentDrafts.value[activeReviewDocumentIndex.value] ?? null)
|
||||
const activeReviewDocumentPreview = computed(() =>
|
||||
activeReviewDocument.value
|
||||
? (
|
||||
resolveDocumentPreview(activeReviewFilePreviews.value, activeReviewDocument.value.filename)
|
||||
|| (
|
||||
activeReviewDocument.value.preview_kind === 'image' && activeReviewDocument.value.preview_data_url
|
||||
? {
|
||||
filename: activeReviewDocument.value.filename,
|
||||
kind: activeReviewDocument.value.preview_kind,
|
||||
url: activeReviewDocument.value.preview_data_url
|
||||
}
|
||||
: null
|
||||
)
|
||||
)
|
||||
: null
|
||||
)
|
||||
const canPreviewActiveReviewDocument = computed(() => Boolean(activeReviewDocumentPreview.value?.url))
|
||||
const reviewDocumentDirty = computed(() => {
|
||||
const baseValue = JSON.stringify(reviewDocumentBaseDrafts.value.map(normalizeReviewDocumentComparableValue))
|
||||
const nextValue = JSON.stringify(reviewDocumentDrafts.value.map(normalizeReviewDocumentComparableValue))
|
||||
return baseValue !== nextValue
|
||||
})
|
||||
const reviewHasUnsavedChanges = computed(() => reviewInlineDirty.value || reviewDocumentDirty.value)
|
||||
|
||||
function resetReviewDrawerFromPayload(payload) {
|
||||
const normalizedInlineState = buildInlineReviewState(payload)
|
||||
reviewInlineForm.value = { ...normalizedInlineState }
|
||||
reviewInlineBaseForm.value = { ...normalizedInlineState }
|
||||
reviewInlineBaseFields.value = cloneReviewEditFields(payload?.edit_fields)
|
||||
const nextDocumentDrafts = buildReviewDocumentDrafts(payload)
|
||||
reviewDocumentDrafts.value = cloneReviewDocumentDrafts(nextDocumentDrafts)
|
||||
reviewDocumentBaseDrafts.value = cloneReviewDocumentDrafts(nextDocumentDrafts)
|
||||
activeReviewDocumentIndex.value = nextDocumentDrafts.length
|
||||
? Math.min(activeReviewDocumentIndex.value, nextDocumentDrafts.length - 1)
|
||||
: 0
|
||||
reviewDrawerMode.value = resolveReviewRiskBriefs(payload).length
|
||||
? REVIEW_DRAWER_MODE_RISK
|
||||
: REVIEW_DRAWER_MODE_REVIEW
|
||||
reviewInlinePendingFiles.value = []
|
||||
reviewInlineEditorKey.value = ''
|
||||
reviewInlineErrors.value = {}
|
||||
reviewOtherCategoryOpen.value = false
|
||||
}
|
||||
|
||||
function setInlineReviewFieldError(key, message) {
|
||||
reviewInlineErrors.value = {
|
||||
...reviewInlineErrors.value,
|
||||
[key]: String(message || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
function clearInlineReviewFieldError(key) {
|
||||
if (!reviewInlineErrors.value[key]) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextErrors = { ...reviewInlineErrors.value }
|
||||
delete nextErrors[key]
|
||||
reviewInlineErrors.value = nextErrors
|
||||
}
|
||||
|
||||
function openInlineReviewEditor(key) {
|
||||
if (!activeReviewPayload.value || submitting.value || reviewActionBusy.value) return
|
||||
if (key === 'attachments') {
|
||||
triggerFileUpload('inline-review')
|
||||
return
|
||||
}
|
||||
|
||||
if (reviewInlineEditorKey.value && reviewInlineEditorKey.value !== key && !commitInlineReviewEditor()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (reviewInlineEditorKey.value === key) {
|
||||
commitInlineReviewEditor()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'amount') {
|
||||
reviewInlineForm.value = {
|
||||
...reviewInlineForm.value,
|
||||
amount: extractAmountInputValue(reviewInlineForm.value.amount)
|
||||
}
|
||||
}
|
||||
|
||||
clearInlineReviewFieldError(key)
|
||||
reviewInlineEditorKey.value = key
|
||||
if (key !== 'expense_type') {
|
||||
reviewOtherCategoryOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeInlineReviewEditor() {
|
||||
reviewInlineEditorKey.value = ''
|
||||
reviewOtherCategoryOpen.value = false
|
||||
}
|
||||
|
||||
function commitInlineReviewEditor() {
|
||||
const activeEditorKey = reviewInlineEditorKey.value
|
||||
const nextForm = {
|
||||
...reviewInlineForm.value,
|
||||
occurred_date: String(reviewInlineForm.value.occurred_date || '').trim(),
|
||||
amount: String(reviewInlineForm.value.amount || '').trim(),
|
||||
transport_type: String(reviewInlineForm.value.transport_type || '').trim(),
|
||||
customer_name: String(reviewInlineForm.value.customer_name || '').trim(),
|
||||
location: String(reviewInlineForm.value.location || '').trim(),
|
||||
merchant_name: String(reviewInlineForm.value.merchant_name || '').trim(),
|
||||
participants: String(reviewInlineForm.value.participants || '').trim(),
|
||||
scene_label: String(reviewInlineForm.value.scene_label || '').trim(),
|
||||
reason_value: String(reviewInlineForm.value.reason_value || reviewInlineForm.value.scene_label || '').trim(),
|
||||
expense_type: String(reviewInlineForm.value.expense_type || '').trim()
|
||||
}
|
||||
|
||||
if (
|
||||
activeEditorKey === 'scene' &&
|
||||
nextForm.scene_label === REVIEW_SCENE_OTHER_OPTION
|
||||
) {
|
||||
nextForm.reason_value = String(reviewInlineForm.value.reason_value || '').trim()
|
||||
if (!nextForm.reason_value) {
|
||||
setInlineReviewFieldError('scene', '璇烽€夋嫨鈥滃叾浠栧満鏅€濆悗锛岃琛ュ厖鍏蜂綋浜嬬敱')
|
||||
reviewInlineForm.value = nextForm
|
||||
return false
|
||||
}
|
||||
} else if (activeEditorKey === 'scene') {
|
||||
nextForm.reason_value = nextForm.scene_label
|
||||
}
|
||||
|
||||
if (activeEditorKey === 'occurred_date' && nextForm.occurred_date && !isValidIsoDateString(nextForm.occurred_date)) {
|
||||
setInlineReviewFieldError('occurred_date', `璇疯緭鍏ユ纭殑鏃堕棿鏍煎紡锛?{DATE_INPUT_FORMAT}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (activeEditorKey === 'amount' && nextForm.amount) {
|
||||
const normalizedAmount = normalizeAmountValue(nextForm.amount)
|
||||
if (!normalizedAmount) {
|
||||
setInlineReviewFieldError('amount', '璇疯緭鍏ユ纭殑鏁板瓧閲戦锛屼緥濡?200 鎴?200.50')
|
||||
return false
|
||||
}
|
||||
nextForm.amount = normalizedAmount
|
||||
}
|
||||
|
||||
if (activeEditorKey) {
|
||||
clearInlineReviewFieldError(activeEditorKey)
|
||||
}
|
||||
|
||||
reviewInlineForm.value = nextForm
|
||||
reviewInlineEditorKey.value = ''
|
||||
return true
|
||||
}
|
||||
|
||||
function selectInlineScene(scene) {
|
||||
const normalizedScene = String(scene || '').trim()
|
||||
reviewInlineForm.value = {
|
||||
...reviewInlineForm.value,
|
||||
scene_label: normalizedScene,
|
||||
reason_value:
|
||||
normalizedScene === REVIEW_SCENE_OTHER_OPTION
|
||||
? ''
|
||||
: normalizedScene
|
||||
}
|
||||
clearInlineReviewFieldError('scene')
|
||||
if (normalizedScene !== REVIEW_SCENE_OTHER_OPTION) {
|
||||
reviewInlineEditorKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function selectReviewCategory(option) {
|
||||
if (!option) return
|
||||
if (option.is_other) {
|
||||
reviewOtherCategoryOpen.value = !reviewOtherCategoryOpen.value
|
||||
return
|
||||
}
|
||||
|
||||
reviewInlineForm.value = {
|
||||
...reviewInlineForm.value,
|
||||
expense_type: option.label
|
||||
}
|
||||
reviewOtherCategoryOpen.value = false
|
||||
}
|
||||
|
||||
function selectReviewOtherCategory(option) {
|
||||
if (!option) return
|
||||
reviewInlineForm.value = {
|
||||
...reviewInlineForm.value,
|
||||
expense_type: option.label
|
||||
}
|
||||
reviewOtherCategoryOpen.value = false
|
||||
}
|
||||
|
||||
function goReviewDocument(direction) {
|
||||
const total = reviewDocumentCount.value
|
||||
if (!total) return
|
||||
const nextIndex = activeReviewDocumentIndex.value + Number(direction || 0)
|
||||
activeReviewDocumentIndex.value = Math.max(0, Math.min(total - 1, nextIndex))
|
||||
}
|
||||
|
||||
function openActiveReviewDocumentPreview() {
|
||||
if (!activeReviewDocument.value || !activeReviewDocumentPreview.value?.url) return
|
||||
documentPreviewDialog.value = {
|
||||
open: true,
|
||||
filename: activeReviewDocument.value.filename,
|
||||
kind: activeReviewDocumentPreview.value.kind,
|
||||
url: activeReviewDocumentPreview.value.url
|
||||
}
|
||||
}
|
||||
|
||||
function closeDocumentPreview() {
|
||||
documentPreviewDialog.value = {
|
||||
...documentPreviewDialog.value,
|
||||
open: false
|
||||
}
|
||||
}
|
||||
|
||||
function enforceReviewDrawerAvailability() {
|
||||
if (!reviewDocumentDrawerAvailable.value && reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
if (!reviewRiskDrawerAvailable.value && reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
if (!reviewFlowDrawerAvailable.value && reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reviewInlineForm,
|
||||
reviewInlineBaseForm,
|
||||
reviewInlineBaseFields,
|
||||
reviewInlinePendingFiles,
|
||||
reviewInlineEditorKey,
|
||||
reviewInlineErrors,
|
||||
reviewOtherCategoryOpen,
|
||||
reviewDocumentDrafts,
|
||||
reviewDocumentBaseDrafts,
|
||||
activeReviewDocumentIndex,
|
||||
reviewDrawerMode,
|
||||
documentPreviewDialog,
|
||||
activeReviewFilePreviews,
|
||||
reviewIntentText,
|
||||
reviewFactCards,
|
||||
reviewCategoryOptions,
|
||||
reviewOtherCategoryOptions,
|
||||
reviewSelectedOtherCategory,
|
||||
reviewInlineDirty,
|
||||
reviewPanelConfidence,
|
||||
reviewRiskSummary,
|
||||
reviewRiskItems,
|
||||
reviewRiskEmpty,
|
||||
reviewDocumentDrawerAvailable,
|
||||
reviewRiskDrawerAvailable,
|
||||
reviewFlowDrawerAvailable,
|
||||
recognizedNarratives,
|
||||
reviewRecognitionNotes,
|
||||
reviewDocumentSummaries,
|
||||
reviewDocumentCount,
|
||||
isReviewDocumentDrawer,
|
||||
isReviewRiskDrawer,
|
||||
isReviewFlowDrawer,
|
||||
reviewDrawerTitle,
|
||||
reviewDocumentDrawerLabel,
|
||||
reviewDocumentDrawerIcon,
|
||||
reviewRiskDrawerLabel,
|
||||
reviewRiskDrawerIcon,
|
||||
reviewFlowDrawerLabel,
|
||||
reviewFlowDrawerIcon,
|
||||
activeReviewDocument,
|
||||
activeReviewDocumentPreview,
|
||||
canPreviewActiveReviewDocument,
|
||||
reviewDocumentDirty,
|
||||
reviewHasUnsavedChanges,
|
||||
setInlineReviewFieldError,
|
||||
clearInlineReviewFieldError,
|
||||
resetReviewDrawerFromPayload,
|
||||
openInlineReviewEditor,
|
||||
closeInlineReviewEditor,
|
||||
commitInlineReviewEditor,
|
||||
selectInlineScene,
|
||||
selectReviewCategory,
|
||||
selectReviewOtherCategory,
|
||||
goReviewDocument,
|
||||
openActiveReviewDocumentPreview,
|
||||
closeDocumentPreview,
|
||||
enforceReviewDrawerAvailability
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user