Files
X-Financial/web/src/views/scripts/useTravelReimbursementReviewDrawer.js

423 lines
15 KiB
JavaScript

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
}
}