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