import { computed, nextTick, ref } from 'vue' export function useTravelReimbursementComposerTools({ currentUser, activeReviewPayload, reviewInlineForm, latestReviewMessage, currentInsight, messages, composerDraft, composerTextareaRef, adjustComposerTextareaHeight, scrollToBottom, toast, calculateTravelReimbursement, createMessage, buildReviewSlotMap, isValidIsoDateString, buildLocallySyncedReviewPayload, formatDateInputValue }) { const composerDatePickerOpen = ref(false) const composerDateMode = ref('single') const composerSingleDate = ref(formatDateInputValue()) const composerRangeStartDate = ref(formatDateInputValue()) const composerRangeEndDate = ref(formatDateInputValue()) const composerBusinessTimeTags = ref([]) const composerBusinessTimeDraftTouched = ref(false) const travelCalculatorOpen = ref(false) const travelCalculatorBusy = ref(false) const travelCalculatorError = ref('') const travelCalculatorResult = ref(null) const travelCalculatorForm = ref({ days: '1', location: '' }) const composerCanApplyDateSelection = computed(() => { if (composerDateMode.value === 'single') { return Boolean(composerSingleDate.value) } return Boolean( composerRangeStartDate.value && composerRangeEndDate.value && composerRangeStartDate.value <= composerRangeEndDate.value ) }) const travelCalculatorCanSubmit = computed(() => !travelCalculatorBusy.value && Number(travelCalculatorForm.value.days) >= 1 && Boolean(String(travelCalculatorForm.value.location || '').trim()) ) function buildComposerBusinessTimeLabel() { if (composerDateMode.value === 'single') { return `业务发生时间:${composerSingleDate.value}` } if (composerRangeStartDate.value === composerRangeEndDate.value) { return `业务发生时间:${composerRangeStartDate.value}` } return `业务发生时间:${composerRangeStartDate.value} 至 ${composerRangeEndDate.value}` } function hasComposerBusinessTimeSelection() { return composerBusinessTimeTags.value.length > 0 || composerBusinessTimeDraftTouched.value } function buildComposerBusinessTimeContext() { if (!hasComposerBusinessTimeSelection()) { return null } const mode = composerDateMode.value === 'range' ? 'range' : 'single' const startDate = String(mode === 'range' ? composerRangeStartDate.value : composerSingleDate.value).trim() const endDate = String(mode === 'range' ? composerRangeEndDate.value : startDate).trim() if (!isValidIsoDateString(startDate) || !isValidIsoDateString(endDate) || startDate > endDate) { return null } const displayValue = mode === 'range' && startDate !== endDate ? `${startDate} 至 ${endDate}` : startDate return { mode, start_date: startDate, end_date: endDate, occurred_date: startDate, time_range: displayValue, business_time: displayValue, time_range_raw: buildComposerBusinessTimeLabel() } } function mergeBusinessTimeIntoExtraContext(extraContext, businessTimeContext) { if (!businessTimeContext) { return extraContext } const baseReviewFormValues = extraContext.review_form_values && typeof extraContext.review_form_values === 'object' ? extraContext.review_form_values : {} return { ...extraContext, occurred_date: businessTimeContext.occurred_date, business_time: businessTimeContext.business_time, business_time_context: { mode: businessTimeContext.mode, start_date: businessTimeContext.start_date, end_date: businessTimeContext.end_date, display_value: businessTimeContext.business_time }, review_form_values: { ...baseReviewFormValues, occurred_date: businessTimeContext.occurred_date, time_range: businessTimeContext.time_range, business_time: businessTimeContext.business_time, time_range_raw: businessTimeContext.time_range_raw } } } function syncComposerBusinessTimeToReviewCard(businessTimeContext) { if (!businessTimeContext || !activeReviewPayload.value) { return } const nextInlineState = { ...reviewInlineForm.value, occurred_date: businessTimeContext.occurred_date } const nextReviewPayload = buildLocallySyncedReviewPayload(activeReviewPayload.value, nextInlineState) reviewInlineForm.value = nextInlineState if (latestReviewMessage.value) { latestReviewMessage.value.reviewPayload = nextReviewPayload } if (currentInsight.value?.agent) { currentInsight.value = { ...currentInsight.value, agent: { ...currentInsight.value.agent, reviewPayload: nextReviewPayload } } } } function resolveComposerSubmitText(explicitRawText) { const draftPart = String(explicitRawText ?? composerDraft.value).trim() const tagPart = composerBusinessTimeTags.value.map((item) => item.label).join(',') if (!tagPart) { return draftPart } if (!draftPart) { return tagPart } return `${tagPart},${draftPart}` } function toggleComposerDatePicker() { composerDatePickerOpen.value = !composerDatePickerOpen.value if (composerDatePickerOpen.value) { travelCalculatorOpen.value = false } } function closeComposerDatePicker() { composerDatePickerOpen.value = false } function setComposerDateMode(mode) { composerDateMode.value = mode === 'range' ? 'range' : 'single' } function handleComposerDateInputChange() { composerBusinessTimeDraftTouched.value = true syncComposerBusinessTimeToReviewCard(buildComposerBusinessTimeContext()) } function removeComposerBusinessTimeTag(tagId) { composerBusinessTimeTags.value = composerBusinessTimeTags.value.filter((item) => item.id !== tagId) if (!composerBusinessTimeTags.value.length) { composerBusinessTimeDraftTouched.value = false } } function handleComposerDatePickerOutside(event) { if (!composerDatePickerOpen.value && !travelCalculatorOpen.value) { return } if (event.target instanceof Element && event.target.closest('.composer-date-anchor')) { return } if (event.target instanceof Element && event.target.closest('.travel-calculator-anchor')) { return } if (composerDatePickerOpen.value) { composerDatePickerOpen.value = false } if (travelCalculatorOpen.value && !travelCalculatorBusy.value) { travelCalculatorOpen.value = false } } async function applyComposerDateSelection() { if (!composerCanApplyDateSelection.value) { return } composerBusinessTimeDraftTouched.value = true composerBusinessTimeTags.value = [ { id: `biz-time-${Date.now()}`, label: buildComposerBusinessTimeLabel() } ] syncComposerBusinessTimeToReviewCard(buildComposerBusinessTimeContext()) composerDatePickerOpen.value = false await nextTick() adjustComposerTextareaHeight() composerTextareaRef.value?.focus() } function resolveTravelCalculatorInitialDays() { const businessTimeContext = buildComposerBusinessTimeContext() if (!businessTimeContext) { return 1 } const startDate = businessTimeContext.start_date const endDate = businessTimeContext.end_date || startDate if (!isValidIsoDateString(startDate) || !isValidIsoDateString(endDate) || startDate > endDate) { return 1 } const startAt = Date.parse(`${startDate}T00:00:00Z`) const endAt = Date.parse(`${endDate}T00:00:00Z`) if (!Number.isFinite(startAt) || !Number.isFinite(endAt)) { return 1 } return Math.max(1, Math.round((endAt - startAt) / 86400000) + 1) } function resolveTravelCalculatorInitialLocation() { const slotMap = buildReviewSlotMap(activeReviewPayload.value) const candidates = [ reviewInlineForm.value.location, slotMap.business_location?.normalized_value, slotMap.business_location?.value, slotMap.location?.normalized_value, slotMap.location?.value, currentUser.value?.location ] return String(candidates.find((item) => String(item || '').trim()) || '').trim() } function openTravelCalculator() { closeComposerDatePicker() travelCalculatorError.value = '' travelCalculatorResult.value = null travelCalculatorForm.value = { days: String(resolveTravelCalculatorInitialDays()), location: resolveTravelCalculatorInitialLocation() } travelCalculatorOpen.value = true } function toggleTravelCalculator() { if (travelCalculatorOpen.value) { closeTravelCalculator() return } openTravelCalculator() } function closeTravelCalculator() { if (travelCalculatorBusy.value) { return } travelCalculatorOpen.value = false } function formatTravelCalculatorMoney(value) { const amount = Number(value) if (!Number.isFinite(amount)) { return String(value || '0') } return amount.toFixed(2) } function buildTravelCalculatorResultText(result) { const days = Number(result?.days) || 1 const location = String(result?.location || '').trim() || '未填写地点' const matchedCity = String(result?.matched_city || location).trim() const grade = String(result?.grade || '').trim() || '当前职级' const gradeBandLabel = String(result?.grade_band_label || result?.grade_band || '').trim() || '对应档位' const allowanceRegion = String(result?.allowance_region || '').trim() || '默认区域' const ruleName = String(result?.rule_name || '').trim() || '公司差旅费报销规则' const ruleVersion = String(result?.rule_version || '').trim() const hotelRate = formatTravelCalculatorMoney(result?.hotel_rate) const hotelAmount = formatTravelCalculatorMoney(result?.hotel_amount) const mealRate = formatTravelCalculatorMoney(result?.meal_allowance_rate) const basicRate = formatTravelCalculatorMoney(result?.basic_allowance_rate) const allowanceRate = formatTravelCalculatorMoney(result?.total_allowance_rate) const allowanceAmount = formatTravelCalculatorMoney(result?.allowance_amount) const totalAmount = formatTravelCalculatorMoney(result?.total_amount) const ruleVersionText = ruleVersion ? `(${ruleVersion})` : '' const user = currentUser.value || {} const displayName = String(user.name || user.display_name || user.username || '').trim() const greeting = displayName ? `您好,${displayName},` : '您好,' return [ `${greeting}根据您输入的地点和天数,我匹配到您要出差的地区为:**${matchedCity}**,出差天数为:**${days} 天**,我根据公司的报销文件给您预估金额如下:`, '', `**参考可报销合计:${totalAmount} 元**`, '', '| 项目 | 标准口径 | 天数 | 小计 |', '| --- | --- | ---: | ---: |', `| 住宿费 | ${matchedCity} / ${grade}(${gradeBandLabel})标准:${hotelRate} 元/天 | ${days} | ${hotelAmount} 元 |`, `| 出差补贴 | ${allowanceRegion}:伙食 ${mealRate} 元 + 基本 ${basicRate} 元 = ${allowanceRate} 元/天 | ${days} | ${allowanceAmount} 元 |`, '', '**计算过程**', `1. 住宿费:${hotelRate} × ${days} = ${hotelAmount} 元`, `2. 出差补贴:(${mealRate} + ${basicRate}) × ${days} = ${allowanceRate} × ${days} = ${allowanceAmount} 元`, `3. 合计:${hotelAmount} + ${allowanceAmount} = ${totalAmount} 元`, '', `**规则依据**:${ruleName}${ruleVersionText}。出差地点“${location}”匹配为“${matchedCity}”,当前职级“${grade}”匹配“${gradeBandLabel}”档。`, '', '这个结果是提交前的规则测算参考,最终仍以实际票据、审批意见和财务复核口径为准。' ].join('\n') } async function submitTravelCalculator() { if (!travelCalculatorCanSubmit.value) { travelCalculatorError.value = '请填写出差天数和地点后再计算。' return } travelCalculatorBusy.value = true travelCalculatorError.value = '' try { const user = currentUser.value || {} const payload = await calculateTravelReimbursement({ days: Math.max(1, Number.parseInt(String(travelCalculatorForm.value.days || '1'), 10) || 1), location: String(travelCalculatorForm.value.location || '').trim(), grade: String(user.grade || '').trim() }) travelCalculatorResult.value = payload messages.value.push(createMessage('assistant', buildTravelCalculatorResultText(payload), [], { meta: ['差旅计算器'], metaTone: 'low' })) travelCalculatorOpen.value = false nextTick(scrollToBottom) } catch (error) { travelCalculatorError.value = error?.message || '差旅金额测算失败,请稍后重试。' } finally { travelCalculatorBusy.value = false } } return { composerDatePickerOpen, composerDateMode, composerSingleDate, composerRangeStartDate, composerRangeEndDate, composerBusinessTimeTags, composerBusinessTimeDraftTouched, composerCanApplyDateSelection, travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, buildComposerBusinessTimeLabel, hasComposerBusinessTimeSelection, buildComposerBusinessTimeContext, mergeBusinessTimeIntoExtraContext, syncComposerBusinessTimeToReviewCard, resolveComposerSubmitText, toggleComposerDatePicker, closeComposerDatePicker, setComposerDateMode, handleComposerDateInputChange, removeComposerBusinessTimeTag, handleComposerDatePickerOutside, applyComposerDateSelection, resolveTravelCalculatorInitialDays, resolveTravelCalculatorInitialLocation, openTravelCalculator, toggleTravelCalculator, closeTravelCalculator, formatTravelCalculatorMoney, buildTravelCalculatorResultText, submitTravelCalculator } }