423 lines
15 KiB
JavaScript
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
|
|
}
|
|
}
|