Merge branch 'master' into kpi-statis
This commit is contained in:
@@ -84,3 +84,18 @@ export function orderExport(params: any) {
|
||||
export function searchPatients(params: any) {
|
||||
return request.get({ url: '/tcm.diagnosis/searchPatient', params })
|
||||
}
|
||||
|
||||
/** 单条订单操作日志(分页) */
|
||||
export function orderActionLogs(params: { order_id: number; page_no?: number; page_size?: number }) {
|
||||
return request.get({ url: '/order.order/actionLogs', params })
|
||||
}
|
||||
|
||||
/** 按管理员统计操作次数(时间范围,未传时默认近 7 天) */
|
||||
export function orderActionLogStats(params?: { start_time?: string; end_time?: string; limit?: number }) {
|
||||
return request.get({ url: '/order.order/actionLogStats', params })
|
||||
}
|
||||
|
||||
/** 批量将支付单指派给医助 */
|
||||
export function orderBatchAssignAssistant(params: { order_ids: number[]; assistant_id: number }) {
|
||||
return request.post({ url: '/order.order/assignAssistant', params })
|
||||
}
|
||||
|
||||
@@ -388,8 +388,8 @@ export function prescriptionOrderLinkPayOrder(params: {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/linkPayOrder', params })
|
||||
}
|
||||
|
||||
/** 将「已发货」且支付审核通过的订单标记为「已完成」 */
|
||||
export function prescriptionOrderComplete(params: { id: number }) {
|
||||
/** 将「已发货/已签收」且支付审核通过的订单结案(3=已完成 或 7-12 业务状态) */
|
||||
export function prescriptionOrderComplete(params: { id: number; fulfillment_status: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/complete', params })
|
||||
}
|
||||
|
||||
|
||||
@@ -784,8 +784,8 @@ const formData = reactive<PrescriptionForm>({
|
||||
dose_unit: '剂',
|
||||
usage_days: 7,
|
||||
times_per_day: 2,
|
||||
usage_instruction: '水煎服,一日二次',
|
||||
usage_time: '饭前',
|
||||
usage_instruction: '',
|
||||
usage_time: '饭后',
|
||||
usage_way: '温水送服',
|
||||
dietary_taboo: [],
|
||||
usage_notes: '',
|
||||
|
||||
@@ -38,6 +38,18 @@
|
||||
<el-radio-button label="not_passed">未通过</el-radio-button>
|
||||
<el-radio-button label="rejected">驳回</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="pl-toolbar__sep" aria-hidden="true" />
|
||||
<span class="pl-toolbar__k" title="与列表「来源」列一致:系统代开 / 手工">来源</span>
|
||||
<el-radio-group
|
||||
v-model="formData.source_filter"
|
||||
size="small"
|
||||
class="pl-toolbar__rg pl-toolbar__rg--source"
|
||||
@change="onSourceFilterChange"
|
||||
>
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button label="manual">手工</el-radio-button>
|
||||
<el-radio-button label="system">系统代开</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="pl-toolbar__line pl-toolbar__line--second">
|
||||
<el-form :model="formData" inline class="pl-toolbar__form">
|
||||
@@ -189,7 +201,7 @@
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!Number(row.has_prescription_order)"
|
||||
v-if="!Number(row.has_prescription_order) && Number(row.void_status) !== 1"
|
||||
type="success"
|
||||
link
|
||||
v-perms="['tcm.prescriptionOrder/create']"
|
||||
@@ -444,6 +456,15 @@
|
||||
class="mb-4"
|
||||
:title="editReviveHint"
|
||||
/>
|
||||
<div
|
||||
v-if="Number(editForm.diagnosis_id) > 0"
|
||||
class="mb-3 flex flex-wrap items-center gap-2 pl-[120px]"
|
||||
>
|
||||
<el-button type="primary" size="small" @click="openDiagnosisPatientDetail">
|
||||
查看患者诊单详情
|
||||
</el-button>
|
||||
<span class="text-xs text-gray-400">关联诊单 #{{ editForm.diagnosis_id }}</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="患者姓名" prop="patient_name">
|
||||
@@ -1234,6 +1255,9 @@
|
||||
<el-button type="danger" :loading="auditLoading" @click="submitAudit('reject')">驳回处方</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 与 tcm/diagnosis/edit 同一套诊单界面(只读 + 全部分页签) -->
|
||||
<TcmDiagnosisEditView ref="diagnosisViewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1259,9 +1283,11 @@ import feedback from '@/utils/feedback'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch, nextTick, defineAsyncComponent } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
|
||||
|
||||
/** 与 server/config/project.php prescription_audit_roles 保持一致 */
|
||||
const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3]
|
||||
|
||||
@@ -1289,6 +1315,8 @@ const formData = reactive({
|
||||
creator_ids: [] as number[],
|
||||
/** all | passed | not_passed | pending | rejected(all 表示不限,接口传参会转为空) */
|
||||
audit_filter: 'all' as string,
|
||||
/** all | manual | system — 与 is_system_auto:手工(0) / 系统代开(1) */
|
||||
source_filter: 'all' as string,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
})
|
||||
@@ -1621,13 +1649,13 @@ function resetCreateOrderForm() {
|
||||
createOrderFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
/** 仅按定金门槛抬升「总金额」,不跟已选支付单合计联动(由用户自填) */
|
||||
function syncCreateOrderAmountFromRules() {
|
||||
const min = createOrderDepositMin.value
|
||||
const paid = createOrderLinkedPaidTotal.value
|
||||
const floor = min > 0 ? Math.round(min * 100) / 100 : 0
|
||||
const need = min > 0 ? Math.max(paid, floor) : paid > 0 ? paid : 0
|
||||
if (need > 0 && Number(createOrderForm.amount) < need) {
|
||||
createOrderForm.amount = Math.round(need * 100) / 100
|
||||
if (min <= 0) return
|
||||
const need = Math.round(min * 100) / 100
|
||||
if (Number(createOrderForm.amount) < need) {
|
||||
createOrderForm.amount = need
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1664,15 +1692,11 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [...createOrderForm.pay_order_ids],
|
||||
() => {
|
||||
syncCreateOrderAmountFromRules()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
async function openCreateOrderFromPrescription(row: any) {
|
||||
if (Number(row?.void_status) === 1) {
|
||||
feedback.msgWarning('已作废处方不可创建业务订单')
|
||||
return
|
||||
}
|
||||
resetCreateOrderForm()
|
||||
createOrderPrescription.value = row
|
||||
const did = Number(row.diagnosis_id) || 0
|
||||
@@ -1889,6 +1913,19 @@ const editForm = reactive({
|
||||
times_per_day: 2
|
||||
})
|
||||
|
||||
const diagnosisViewRef = ref<{
|
||||
openViewOnly: (id: number) => Promise<void>
|
||||
} | null>(null)
|
||||
|
||||
function openDiagnosisPatientDetail() {
|
||||
const id = Number(editForm.diagnosis_id)
|
||||
if (!id) {
|
||||
feedback.msgWarning('本处方未关联诊单,无法查看诊单患者信息')
|
||||
return
|
||||
}
|
||||
void diagnosisViewRef.value?.openViewOnly(id)
|
||||
}
|
||||
|
||||
// 标志:是否正在加载数据(防止watch函数覆盖已加载的值)
|
||||
const isLoadingData = ref(false)
|
||||
|
||||
@@ -1940,10 +1977,12 @@ const rules: FormRules = {
|
||||
}
|
||||
|
||||
async function fetchPrescriptionLists(params: Record<string, unknown>) {
|
||||
const { audit_filter, creator_ids, ...rest } = params
|
||||
const { audit_filter, source_filter, creator_ids, ...rest } = params
|
||||
const payload: Record<string, unknown> = {
|
||||
...rest,
|
||||
audit_filter: audit_filter === 'all' || audit_filter === '' ? '' : audit_filter
|
||||
audit_filter: audit_filter === 'all' || audit_filter === '' ? '' : audit_filter,
|
||||
source_filter:
|
||||
source_filter === 'all' || source_filter === '' || source_filter === undefined ? '' : source_filter
|
||||
}
|
||||
if (Array.isArray(creator_ids) && creator_ids.length > 0) {
|
||||
payload.creator_ids = creator_ids
|
||||
@@ -1993,6 +2032,10 @@ function onAuditFilterChange() {
|
||||
resetPage()
|
||||
}
|
||||
|
||||
function onSourceFilterChange() {
|
||||
resetPage()
|
||||
}
|
||||
|
||||
let syncingDateTab = false
|
||||
watch(
|
||||
() => [formData.start_time, formData.end_time],
|
||||
@@ -2039,6 +2082,7 @@ function resetParams() {
|
||||
formData.patient_name = ''
|
||||
formData.creator_ids = []
|
||||
formData.audit_filter = 'all'
|
||||
formData.source_filter = 'all'
|
||||
formData.start_time = ''
|
||||
formData.end_time = ''
|
||||
syncingDateTab = false
|
||||
|
||||
@@ -62,7 +62,77 @@
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none po-filter-card shadow-sm" shadow="never">
|
||||
<div
|
||||
class="po-list-stats grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5"
|
||||
v-loading="pager.loading"
|
||||
>
|
||||
<div
|
||||
class="po-stat-surface po-stat-surface--order relative overflow-hidden rounded-xl border border-slate-200/90 px-5 py-4 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="po-stat-deco po-stat-deco--o pointer-events-none absolute -right-6 -top-8 h-28 w-28 rounded-full opacity-40"
|
||||
/>
|
||||
<div class="relative flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-slate-500 text-xs font-medium">
|
||||
<el-icon :size="16" class="text-sky-500"><Calendar /></el-icon>
|
||||
{{ listStats.orderHeading }}
|
||||
</div>
|
||||
<div class="mt-2 text-2xl sm:text-[26px] font-bold text-slate-800 tracking-tight tabular-nums">
|
||||
¥{{ listStats.orderStr }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat-icon-wrap h-10 w-10 shrink-0 rounded-lg bg-sky-500/10 flex items-center justify-center text-sky-600"
|
||||
>
|
||||
<el-icon :size="20"><Document /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<p class="relative mt-2 text-xs text-slate-500 leading-relaxed">
|
||||
{{ listStats.periodLine }}
|
||||
</p>
|
||||
<p class="relative mt-1.5 text-xs text-slate-400 leading-relaxed">
|
||||
{{ listStats.scopeHint }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="po-stat-surface po-stat-surface--pay relative overflow-hidden rounded-xl border border-slate-200/90 px-5 py-4 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="po-stat-deco po-stat-deco--p pointer-events-none absolute -right-6 -top-8 h-28 w-28 rounded-full opacity-40"
|
||||
/>
|
||||
<div class="relative flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-slate-500 text-xs font-medium">
|
||||
<el-icon :size="16" class="text-emerald-500"><LinkIcon /></el-icon>
|
||||
{{ listStats.payHeading }}
|
||||
</div>
|
||||
<div class="mt-2 text-2xl sm:text-[26px] font-bold text-slate-800 tracking-tight tabular-nums">
|
||||
¥{{ listStats.payStr }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat-icon-wrap h-10 w-10 shrink-0 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-600"
|
||||
>
|
||||
<el-icon :size="20"><Wallet /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<p class="relative mt-2 text-xs text-slate-500 leading-relaxed">
|
||||
{{ listStats.periodLine }}
|
||||
</p>
|
||||
<p class="relative mt-1.5 text-xs text-slate-400 leading-relaxed">
|
||||
所选时间内创建订单所关联的支付单金额(与「实付(关联)」含义一致;不含创建时间时默认今日)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-form class="ls-form" :model="queryParams" inline>
|
||||
<el-form-item class="w-[360px] min-w-[240px] max-w-full" label="创建时间">
|
||||
<DaterangePicker
|
||||
v-model:startTime="queryParams.start_time"
|
||||
v-model:endTime="queryParams.end_time"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[260px]" label="订单号">
|
||||
<el-input
|
||||
v-model="queryParams.order_no"
|
||||
@@ -406,12 +476,21 @@
|
||||
</el-step>
|
||||
<el-step title="发货与履约" :status="workflowFulfillStatus">
|
||||
<template #description>
|
||||
<span v-if="workflowFulfillStatus === 'process'" class="text-primary font-medium">等待发货/补充单号</span>
|
||||
<span
|
||||
v-if="workflowFulfillStatus === 'process' && Number(detailData?.fulfillment_status) === 7"
|
||||
class="text-amber-600 font-medium"
|
||||
>进行中</span>
|
||||
<span v-else-if="workflowFulfillStatus === 'process'" class="text-primary font-medium">等待发货/补充单号</span>
|
||||
<span v-else-if="workflowFulfillStatus === 'wait'">待转入履约</span>
|
||||
<span v-else-if="workflowFulfillStatus === 'success'" class="text-green-600">
|
||||
{{ Number(detailData?.fulfillment_status) === 6 ? '已签收' : '已发货' }}
|
||||
{{ fulfillmentText(detailData?.fulfillment_status) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="workflowFulfillStatus === 'error'"
|
||||
class="text-red-500 font-medium"
|
||||
>
|
||||
{{ fulfillmentText(detailData?.fulfillment_status) }}
|
||||
</span>
|
||||
<span v-else-if="workflowFulfillStatus === 'error'" class="text-red-500 font-medium">订单已取消</span>
|
||||
</template>
|
||||
</el-step>
|
||||
<el-step title="订单完成" />
|
||||
@@ -420,19 +499,30 @@
|
||||
|
||||
<!-- 顶部数据概览卡片 -->
|
||||
<el-row :gutter="16" class="mb-5">
|
||||
<el-col :xs="12" :sm="8">
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
|
||||
<div class="stat-title text-gray-500 text-xs mb-1">总金额</div>
|
||||
<div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
|
||||
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
|
||||
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
|
||||
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
|
||||
<div
|
||||
class="stat-value font-bold text-xl"
|
||||
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
|
||||
>
|
||||
¥{{ formatMoney(detailAgencyToCollect) }}
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
|
||||
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
|
||||
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} 笔</div>
|
||||
@@ -802,7 +892,11 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<el-card shadow="never" class="po-panel border-gray-100 mt-4">
|
||||
<el-card
|
||||
v-perms="['tcm.prescriptionOrder/logs']"
|
||||
shadow="never"
|
||||
class="po-panel border-gray-100 mt-4"
|
||||
>
|
||||
<template #header>
|
||||
<span class="font-medium text-[15px]">操作日志</span>
|
||||
</template>
|
||||
@@ -1314,6 +1408,35 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 完成订单:选择履约结案状态 -->
|
||||
<el-dialog
|
||||
v-model="completeOrderDialogVisible"
|
||||
title="完成订单"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="completeOrderId = 0"
|
||||
>
|
||||
<p class="text-sm text-gray-600 mb-3">请选择本单结案状态。选择「已完成」时,实付将按已关联的支付单金额汇总。</p>
|
||||
<el-select
|
||||
v-model="completeFulfillmentStatus"
|
||||
class="w-full"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in completeOrderStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<el-button @click="completeOrderDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="completeOrderSubmitting" @click="submitCompleteOrder">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 补齐支付单 Dialog -->
|
||||
<el-dialog
|
||||
v-model="addPayOrderVisible"
|
||||
@@ -1549,8 +1672,11 @@
|
||||
<span class="cf-slip-inline-gap" />
|
||||
服用方式: {{ prescriptionViewData.usage_way ? ' ' + prescriptionViewData.usage_way : '' }}
|
||||
</div>
|
||||
<div v-if="slipChuwanVisible">
|
||||
出丸:{{ slipPillGrams === null ? '—' : slipPillGrams }} 克
|
||||
<div v-if="slipChuwanVisible" class="space-y-0.5">
|
||||
<div>每次用量约:{{ slipPerBagDosageText }}</div>
|
||||
<div>
|
||||
出丸:{{ slipPillGrams === null ? '—' : slipPillGrams }} 克
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="slipDietaryText" class="cf-slip-dietary">忌口:{{ slipDietaryText }}</div>
|
||||
<div v-if="prescriptionViewData.usage_notes" class="cf-slip-notes">{{ prescriptionViewData.usage_notes }}</div>
|
||||
@@ -1722,7 +1848,8 @@
|
||||
|
||||
<script lang="ts" setup name="prescriptionOrderList">
|
||||
import { computed, onMounted, reactive, ref, nextTick, watch } from 'vue'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, Search } from '@element-plus/icons-vue'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import {
|
||||
prescriptionOrderAuditPayment,
|
||||
prescriptionOrderAuditPrescription,
|
||||
@@ -1878,7 +2005,13 @@ const fulfillmentStatusTabs = [
|
||||
{ label: '已发货', value: 5 },
|
||||
{ label: '已签收', value: 6 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '已取消', value: 4 }
|
||||
{ label: '已取消', value: 4 },
|
||||
{ label: '进行中', value: 7 },
|
||||
{ label: '暂不制药', value: 8 },
|
||||
{ label: '拒收', value: 9 },
|
||||
{ label: '退款', value: 10 },
|
||||
{ label: '保留药方', value: 11 },
|
||||
{ label: '制药缓发', value: 12 }
|
||||
]
|
||||
|
||||
function handleFulfillmentStatusTabClick(value: number | '') {
|
||||
@@ -1887,6 +2020,9 @@ function handleFulfillmentStatusTabClick(value: number | '') {
|
||||
}
|
||||
|
||||
const queryParams = reactive({
|
||||
/** 创建时间区间(与列表筛选一致;不影响「今日」统计) */
|
||||
start_time: '' as string,
|
||||
end_time: '' as string,
|
||||
order_no: '',
|
||||
prescription_id: '' as string | number,
|
||||
/** 诊单患者姓名/手机号(后台关联 diagnosis) */
|
||||
@@ -1950,6 +2086,10 @@ async function fetchLists(params: Record<string, unknown>) {
|
||||
}
|
||||
if (!p.order_no) delete p.order_no
|
||||
if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword
|
||||
if (!String(p.start_time || '').trim() || !String(p.end_time || '').trim()) {
|
||||
delete p.start_time
|
||||
delete p.end_time
|
||||
}
|
||||
return prescriptionOrderLists(p)
|
||||
}
|
||||
|
||||
@@ -1958,6 +2098,45 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const listStats = computed(() => {
|
||||
const ex = pager.extend as Record<string, unknown> | undefined
|
||||
const o = ex?.stats_order_amount ?? ex?.today_order_amount
|
||||
const pay = ex?.stats_linked_pay_amount ?? ex?.today_linked_pay_amount
|
||||
const n = (v: unknown) => {
|
||||
const x = Number(v)
|
||||
if (!Number.isFinite(x)) return 0
|
||||
return Math.round(x * 100) / 100
|
||||
}
|
||||
const fmt = (v: number) => v.toFixed(2)
|
||||
const t0 = String(ex?.stats_time_start ?? '').trim()
|
||||
const t1 = String(ex?.stats_time_end ?? '').trim()
|
||||
const useRange = t0 && t1
|
||||
const periodLine = useRange
|
||||
? `统计时间:${t0} ~ ${t1}`
|
||||
: '统计时间:—'
|
||||
const scope = String(ex?.stats_scope ?? 'restricted')
|
||||
let scopeHint = '统计口径:与当前列表数据可见范围一致'
|
||||
if (scope === 'all') {
|
||||
scopeHint =
|
||||
'统计口径:支付单审核白名单角色,按全量订单;与下方筛选项(医生/医助/患者等,不含「创建时间」)一致'
|
||||
} else if (scope === 'assistant') {
|
||||
scopeHint = '统计口径:仅计诊单医助为本人且满足下方其它筛选项的订单'
|
||||
}
|
||||
const hasCustomTime =
|
||||
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
|
||||
const headPrefix = hasCustomTime ? '区间' : '今日'
|
||||
return {
|
||||
order: n(o),
|
||||
pay: n(pay),
|
||||
orderStr: fmt(n(o)),
|
||||
payStr: fmt(n(pay)),
|
||||
orderHeading: `${headPrefix}业务订单额`,
|
||||
payHeading: `${headPrefix}关联实付`,
|
||||
periodLine,
|
||||
scopeHint
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
showPrescriptionAuditFilter,
|
||||
(show) => {
|
||||
@@ -1970,6 +2149,8 @@ watch(
|
||||
)
|
||||
|
||||
function handleReset() {
|
||||
queryParams.start_time = ''
|
||||
queryParams.end_time = ''
|
||||
queryParams.order_no = ''
|
||||
queryParams.prescription_id = ''
|
||||
queryParams.patient_keyword = ''
|
||||
@@ -2035,7 +2216,13 @@ function fulfillmentText(s: number | undefined) {
|
||||
3: '已完成',
|
||||
4: '已取消',
|
||||
5: '已发货',
|
||||
6: '已签收'
|
||||
6: '已签收',
|
||||
7: '进行中',
|
||||
8: '暂不制药',
|
||||
9: '拒收',
|
||||
10: '退款',
|
||||
11: '保留药方',
|
||||
12: '制药缓发'
|
||||
}
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
@@ -2047,6 +2234,9 @@ function fulfillmentTagType(s: number | undefined): 'success' | 'warning' | 'dan
|
||||
if (n === 5) return 'primary'
|
||||
if (n === 4) return 'danger'
|
||||
if (n === 2) return 'warning'
|
||||
if (n === 7) return 'warning'
|
||||
if (n === 8 || n === 11 || n === 12) return 'info'
|
||||
if (n === 9 || n === 10) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
@@ -2182,9 +2372,10 @@ const workflowActiveStep = computed(() => {
|
||||
const rx = Number(detailData.value.prescription_audit_status)
|
||||
const pay = Number(detailData.value.payment_slip_audit_status)
|
||||
|
||||
if (fs === 3) return 5 // 订单完成
|
||||
if (fs === 3 || [8, 9, 10, 11, 12].includes(fs)) return 5 // 订单完成 / 业务结案
|
||||
if (fs === 6) return 4 // 已签收(仍停留在"已发货/送达"环节)
|
||||
if (fs === 5) return 4 // 已发货
|
||||
if (fs === 7) return 4 // 进行中
|
||||
|
||||
if (fs === 4) { // 已取消
|
||||
if (rx === 2) return 1
|
||||
@@ -2218,8 +2409,9 @@ const workflowPayStatus = computed(() => {
|
||||
|
||||
const workflowFulfillStatus = computed(() => {
|
||||
const fs = Number(detailData.value?.fulfillment_status)
|
||||
if (fs === 3 || fs === 5 || fs === 6) return 'success'
|
||||
if (fs === 4) return 'error'
|
||||
if (fs === 3 || fs === 5 || fs === 6 || fs === 8 || fs === 11 || fs === 12) return 'success'
|
||||
if (fs === 4 || fs === 9 || fs === 10) return 'error'
|
||||
if (fs === 7) return 'process'
|
||||
if (workflowActiveStep.value === 3) return 'process'
|
||||
return 'wait'
|
||||
})
|
||||
@@ -2232,6 +2424,15 @@ const detailLinkedPayOrders = computed(() => {
|
||||
return Array.isArray(arr) ? arr : []
|
||||
})
|
||||
|
||||
/** 需代收:业务订单总金额 − 关联已付总额 */
|
||||
const detailAgencyToCollect = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return 0
|
||||
const total = Number(d.amount) || 0
|
||||
const paid = Number(d.linked_pay_paid_total) || 0
|
||||
return Math.round((total - paid) * 100) / 100
|
||||
})
|
||||
|
||||
const detailUnlinkedPayOrders = ref<any[]>([])
|
||||
|
||||
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
|
||||
@@ -2329,7 +2530,16 @@ const logisticsTracePayload = ref<Record<string, any> | null>(null)
|
||||
|
||||
const detailLogs = ref<any[]>([])
|
||||
|
||||
function canViewPrescriptionOrderLogs() {
|
||||
const p = userStore.perms || []
|
||||
return p.includes('*') || p.includes('tcm.prescriptionOrder/logs')
|
||||
}
|
||||
|
||||
async function fetchLogs(id: number) {
|
||||
if (!canViewPrescriptionOrderLogs()) {
|
||||
detailLogs.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res: any = await prescriptionOrderLogs({ id })
|
||||
detailLogs.value = res?.data ?? res ?? []
|
||||
@@ -3082,6 +3292,44 @@ async function submitShip() {
|
||||
}
|
||||
}
|
||||
|
||||
const completeOrderStatusOptions = [
|
||||
{ value: 3, label: '已完成' },
|
||||
{ value: 7, label: '进行中' },
|
||||
{ value: 8, label: '暂不制药' },
|
||||
{ value: 9, label: '拒收' },
|
||||
{ value: 10, label: '退款' },
|
||||
{ value: 11, label: '保留药方' },
|
||||
{ value: 12, label: '制药缓发' }
|
||||
] as const
|
||||
|
||||
const completeOrderDialogVisible = ref(false)
|
||||
const completeOrderId = ref(0)
|
||||
const completeFulfillmentStatus = ref<number>(3)
|
||||
const completeOrderSubmitting = ref(false)
|
||||
|
||||
async function submitCompleteOrder() {
|
||||
const id = completeOrderId.value
|
||||
if (!id) return
|
||||
completeOrderSubmitting.value = true
|
||||
try {
|
||||
await prescriptionOrderComplete({ id, fulfillment_status: completeFulfillmentStatus.value })
|
||||
feedback.msgSuccess('订单状态已更新')
|
||||
completeOrderDialogVisible.value = false
|
||||
getLists()
|
||||
if (detailVisible.value && detailData.value?.id === id) {
|
||||
try {
|
||||
const r: any = await prescriptionOrderDetail({ id })
|
||||
const nd = r?.data ?? r ?? null
|
||||
if (nd) detailData.value = nd
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
} catch {
|
||||
/* 拦截器已提示 */
|
||||
} finally {
|
||||
completeOrderSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 补齐支付单(已发货状态)────────────────────────────────────────────
|
||||
const addPayOrderVisible = ref(false)
|
||||
const addPayOrderSaving = ref(false)
|
||||
@@ -3221,29 +3469,11 @@ async function submitAddPayOrder() {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 完成订单(已发货 + 支付审核通过)────────────────────────────────────────────
|
||||
async function confirmComplete(row: { id: number }) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确认将该订单标记为「已完成」?此操作不可撤销。',
|
||||
'完成订单',
|
||||
{ type: 'success', confirmButtonText: '确认完成', cancelButtonText: '暂不处理' }
|
||||
)
|
||||
await prescriptionOrderComplete({ id: row.id })
|
||||
feedback.msgSuccess('订单已标记为「已完成」')
|
||||
getLists()
|
||||
if (detailVisible.value && detailData.value?.id === row.id) {
|
||||
try {
|
||||
const r: any = await prescriptionOrderDetail({ id: row.id })
|
||||
const nd = r?.data ?? r ?? null
|
||||
if (nd) detailData.value = nd
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel' && e !== 'close') {
|
||||
/* 拦截器已提示 */
|
||||
}
|
||||
}
|
||||
// ─── 完成订单(已发货/已签收 + 支付审核通过)────────────────────────────────────────────
|
||||
function confirmComplete(row: { id: number }) {
|
||||
completeOrderId.value = row.id
|
||||
completeFulfillmentStatus.value = 3
|
||||
completeOrderDialogVisible.value = true
|
||||
}
|
||||
|
||||
// ─── 撤回处方审核 ────────────────────────────────────────────
|
||||
@@ -3327,6 +3557,14 @@ const slipChuwanVisible = computed(() => {
|
||||
return (d.prescription_type || '浓缩水丸') === '浓缩水丸'
|
||||
})
|
||||
|
||||
/** 与详情「用量」、开方时「用量」同一字段:浓缩水丸为每袋 g 数 */
|
||||
const slipPerBagDosageText = computed(() => {
|
||||
const d = prescriptionViewData.value
|
||||
if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return '—'
|
||||
if (d.dosage_amount == null || d.dosage_amount === '') return '—'
|
||||
return `${d.dosage_amount}${d.dosage_unit || 'g'}`
|
||||
})
|
||||
|
||||
const slipPillGrams = computed(() => {
|
||||
const d = prescriptionViewData.value
|
||||
if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null
|
||||
@@ -3513,6 +3751,36 @@ async function downloadPrescriptionSlipPdf() {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.po-list-stats {
|
||||
--po-stat-order-a: color-mix(in srgb, var(--el-color-primary) 12%, white);
|
||||
--po-stat-order-b: color-mix(in srgb, var(--el-color-primary) 5%, transparent);
|
||||
--po-stat-pay-a: color-mix(in srgb, #10b981 10%, white);
|
||||
--po-stat-pay-b: color-mix(in srgb, #10b981 4%, transparent);
|
||||
}
|
||||
|
||||
.po-stat-surface {
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.po-stat-surface:hover {
|
||||
box-shadow: 0 10px 28px -12px rgba(15, 23, 42, 0.18);
|
||||
border-color: color-mix(in srgb, var(--el-color-primary) 22%, var(--el-border-color));
|
||||
}
|
||||
.po-stat-surface--order {
|
||||
background: linear-gradient(135deg, var(--po-stat-order-a) 0%, #fff 48%, var(--po-stat-order-b) 100%);
|
||||
}
|
||||
.po-stat-surface--pay {
|
||||
background: linear-gradient(135deg, var(--po-stat-pay-a) 0%, #fff 48%, var(--po-stat-pay-b) 100%);
|
||||
}
|
||||
.po-stat-deco--o {
|
||||
background: radial-gradient(circle, color-mix(in srgb, var(--el-color-primary) 45%, transparent) 0%, transparent 70%);
|
||||
}
|
||||
.po-stat-deco--p {
|
||||
background: radial-gradient(circle, color-mix(in srgb, #10b981 40%, transparent) 0%, transparent 70%);
|
||||
}
|
||||
.stat-icon-wrap {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.po-table-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
+344
-22
@@ -40,15 +40,21 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="w-[280px]" label="订单类型">
|
||||
<el-select v-model="queryParams.order_type" placeholder="请选择" clearable>
|
||||
<el-select v-model="queryParams.order_type" class="w-full" placeholder="全部">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="挂号费" :value="1" />
|
||||
<el-option label="问诊费" :value="2" />
|
||||
<el-option label="药品费用" :value="3" />
|
||||
<el-option label="首付费用" :value="4" />
|
||||
<el-option label="尾款费用" :value="5" />
|
||||
<el-option label="其他费用" :value="6" />
|
||||
<el-option label="全部费用" :value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="w-[280px]" label="订单状态">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
|
||||
<el-select v-model="queryParams.status" class="w-full" placeholder="全部">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待支付" :value="1" />
|
||||
<el-option label="已支付" :value="2" />
|
||||
<el-option label="已取消" :value="3" />
|
||||
@@ -67,6 +73,12 @@
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button
|
||||
v-perms="['order.order/lists']"
|
||||
@click="statsDialogVisible = true"
|
||||
>
|
||||
操作统计
|
||||
</el-button>
|
||||
<export-data
|
||||
v-perms="['order.order/export']"
|
||||
class="ml-2.5"
|
||||
@@ -113,11 +125,23 @@
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div v-perms="['order.order/zhipai']" class="mb-3 flex items-center gap-3">
|
||||
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
|
||||
将创建人指给医助
|
||||
</el-button>
|
||||
<span v-if="selectedOrderIds.length" class="text-sm text-gray-500"
|
||||
>已选 {{ selectedOrderIds.length }} 条</span
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="orderTableRef"
|
||||
v-loading="pager.loading"
|
||||
:data="pager.lists"
|
||||
row-key="id"
|
||||
size="large"
|
||||
@selection-change="handleOrderSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column label="订单号" prop="order_no" min-width="150" />
|
||||
|
||||
<el-table-column label="患者信息" min-width="150">
|
||||
@@ -202,7 +226,7 @@
|
||||
小程序码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
v-if="row.status === 1 || (row.status === 5 && row.payment_method === 'fubei')"
|
||||
v-perms="['order.order/pay']"
|
||||
type="success"
|
||||
link
|
||||
@@ -220,7 +244,7 @@
|
||||
退款
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
v-if="row.status === 1 || (row.status === 5 && row.payment_method === 'fubei')"
|
||||
v-perms="['order.order/cancel']"
|
||||
type="danger"
|
||||
link
|
||||
@@ -249,7 +273,7 @@
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="订单详情"
|
||||
width="700px"
|
||||
width="820px"
|
||||
>
|
||||
<el-descriptions :column="2" border v-if="detailData">
|
||||
<el-descriptions-item label="订单号">
|
||||
@@ -296,6 +320,87 @@
|
||||
{{ detailData.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider content-position="left" v-if="detailData">操作记录</el-divider>
|
||||
<p v-if="detailData" class="text-xs text-gray-500 mb-2">谁在什么时间对本单做了哪些操作(数据来自服务器记录)。</p>
|
||||
<el-table
|
||||
v-if="detailData"
|
||||
v-loading="actionLogLoading"
|
||||
:data="actionLogList"
|
||||
size="small"
|
||||
max-height="320"
|
||||
>
|
||||
<el-table-column label="操作" prop="action_label" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作人" prop="admin_name" min-width="100" />
|
||||
<el-table-column label="操作时间" prop="create_time_text" min-width="160" />
|
||||
<el-table-column label="说明" prop="summary" min-width="140" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div v-if="detailData" class="flex justify-end mt-3">
|
||||
<el-pagination
|
||||
v-model:current-page="actionLogPage"
|
||||
v-model:page-size="actionLogPageSize"
|
||||
:total="actionLogTotal"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
small
|
||||
@size-change="onActionLogSizeChange"
|
||||
@current-change="loadOrderActionLogs"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 按人操作统计(时间范围内) -->
|
||||
<el-dialog
|
||||
v-model="statsDialogVisible"
|
||||
title="支付单操作统计"
|
||||
width="520px"
|
||||
@opened="onStatsDialogOpen"
|
||||
>
|
||||
<p v-if="statsRangeText" class="text-xs text-gray-500 mb-3">统计范围:{{ statsRangeText }}</p>
|
||||
<el-form class="mb-3" :inline="true" @submit.prevent>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="statsDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="至"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
class="!w-[260px]"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="statsLoading" @click="fetchActionLogStats">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="statsLoading" :data="statsList" size="small" max-height="400">
|
||||
<el-table-column label="操作人" prop="admin_name" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作次数" prop="cnt" width="100" align="right" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="assignDialogVisible" title="变更创建人(医助)" width="440px" destroy-on-close>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
将 {{ selectedOrderIds.length }} 条支付单的「创建人」改为所选医助,并写入各单操作日志。
|
||||
</p>
|
||||
<el-select
|
||||
v-model="assignForm.assistant_id"
|
||||
filterable
|
||||
placeholder="选择医助"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in assistantOptions"
|
||||
:key="item.id"
|
||||
:label="
|
||||
item.dept_names ? `${item.name}(${item.dept_names})` : item.name
|
||||
"
|
||||
:value="Number(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<el-button @click="assignDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="assignLoading" @click="submitBatchAssign">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 支付弹窗 -->
|
||||
@@ -305,7 +410,14 @@
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="payForm" label-width="100px">
|
||||
<el-form-item label="支付类型" required>
|
||||
<el-alert
|
||||
v-if="payForm.fubeiPendingAudit"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
title="该单为付呗·待审核,请选择实际到账方式后确认「已支付」"
|
||||
/>
|
||||
<el-form-item v-if="!payForm.fubeiPendingAudit" label="支付类型" required>
|
||||
<el-radio-group v-model="payForm.payType">
|
||||
<!-- <el-radio label="normal">正常支付</el-radio> -->
|
||||
<el-radio label="supplement">补单支付</el-radio>
|
||||
@@ -321,7 +433,7 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="payForm.payType === 'supplement'">
|
||||
<template v-if="!payForm.fubeiPendingAudit && payForm.payType === 'supplement'">
|
||||
<el-form-item label="支付订单号" required>
|
||||
<el-input
|
||||
v-model="payForm.supplement_order_no"
|
||||
@@ -329,9 +441,18 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-if="payForm.fubeiPendingAudit">
|
||||
<el-form-item label="订单号">
|
||||
<span>{{ payForm.order_no }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="金额">
|
||||
<span class="text-red-500 font-semibold">¥{{ payForm.amount }}</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="支付方式" required>
|
||||
<el-radio-group v-model="payForm.payment_method">
|
||||
<el-radio v-if="payForm.fubeiPendingAudit" label="manual">手动确认到账</el-radio>
|
||||
<el-radio label="alipay">支付宝</el-radio>
|
||||
<el-radio label="wechat">微信</el-radio>
|
||||
<el-radio label="wechat_work">企业微信</el-radio>
|
||||
@@ -361,6 +482,7 @@
|
||||
<el-radio-group v-model="createForm.createType">
|
||||
<el-radio label="normal">普通订单</el-radio>
|
||||
<el-radio label="wechat_work">企业微信对外收款</el-radio>
|
||||
<el-radio label="fubei">付呗</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
@@ -373,6 +495,26 @@
|
||||
<template #title>企业微信对外收款</template>
|
||||
创建后将生成订单号,请在企业微信发起收款时将该订单号填写为「商户订单号」,收款成功后系统将自动更新订单状态。
|
||||
</el-alert>
|
||||
<el-alert
|
||||
v-if="createForm.createType === 'fubei'"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
>
|
||||
<template #title>付呗</template>
|
||||
通过付呗收款的支付单,创建后请按实际对账/审核流程在列表中处理。
|
||||
</el-alert>
|
||||
<el-form-item v-if="createForm.createType === 'fubei'" label="支付单审核">
|
||||
<el-switch
|
||||
v-model="createForm.requirePaymentSlipAudit"
|
||||
active-text="申请审核"
|
||||
inactive-text="不申请"
|
||||
/>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
开启后,新订单的支付状态为「待审核」;未开启则为「待支付」。
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="患者" prop="patient_id">
|
||||
<el-select
|
||||
v-model="createForm.patient_id"
|
||||
@@ -396,6 +538,10 @@
|
||||
<el-option label="挂号费" :value="1" />
|
||||
<el-option label="问诊费" :value="2" />
|
||||
<el-option label="药品费用" :value="3" />
|
||||
<el-option label="首付费用" :value="4" />
|
||||
<el-option label="尾款费用" :value="5" />
|
||||
<el-option label="其他费用" :value="6" />
|
||||
<el-option label="全部费用" :value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -538,8 +684,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="orderList">
|
||||
import { computed } from 'vue'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { orderLists, orderDetail, orderPay, orderCancel, orderDelete, orderRefund, orderExport, orderCreate, orderCreateForWechatWork, orderEdit, orderTodayRevenue, syncWechatWorkBills, syncWechatWorkBillsDebug, searchPatients as searchPatientsAPI, alipayPay, wechatPay } from '@/api/order'
|
||||
import {
|
||||
orderLists,
|
||||
orderDetail,
|
||||
orderPay,
|
||||
orderCancel,
|
||||
orderDelete,
|
||||
orderRefund,
|
||||
orderExport,
|
||||
orderCreate,
|
||||
orderCreateForWechatWork,
|
||||
orderEdit,
|
||||
orderTodayRevenue,
|
||||
syncWechatWorkBills,
|
||||
syncWechatWorkBillsDebug,
|
||||
searchPatients as searchPatientsAPI,
|
||||
alipayPay,
|
||||
wechatPay,
|
||||
orderActionLogs,
|
||||
orderActionLogStats,
|
||||
orderBatchAssignAssistant
|
||||
} from '@/api/order'
|
||||
import { getAssistants } from '@/api/tcm'
|
||||
import { generateOrderQrcode } from '@/api/tcm'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
@@ -556,7 +723,18 @@ const queryParams = reactive({
|
||||
create_time_end: ''
|
||||
})
|
||||
|
||||
const assistantOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
const assistantOptions = ref<
|
||||
Array<{ id: number; name: string; account?: string; dept_names?: string; dept_ids?: number[] }>
|
||||
>([])
|
||||
|
||||
const orderTableRef = ref<any>(null)
|
||||
const selectedOrderRows = ref<any[]>([])
|
||||
const selectedOrderIds = computed(() => selectedOrderRows.value.map((r) => r.id as number).filter((id) => id > 0))
|
||||
const assignDialogVisible = ref(false)
|
||||
const assignLoading = ref(false)
|
||||
const assignForm = reactive({
|
||||
assistant_id: undefined as number | undefined
|
||||
})
|
||||
|
||||
const patientAssociationTab = ref<'' | 'pending' | 'associated'>('')
|
||||
const todayRevenue = ref({ amount: '0.00', count: 0 })
|
||||
@@ -566,6 +744,42 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const handleOrderSelectionChange = (rows: any[]) => {
|
||||
selectedOrderRows.value = rows || []
|
||||
}
|
||||
|
||||
const openAssignDialog = () => {
|
||||
if (!selectedOrderIds.value.length) {
|
||||
feedback.msgWarning('请先勾选要指派的支付单')
|
||||
return
|
||||
}
|
||||
assignForm.assistant_id = undefined
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitBatchAssign = async () => {
|
||||
if (assignForm.assistant_id == null) {
|
||||
feedback.msgWarning('请选择医助')
|
||||
return
|
||||
}
|
||||
assignLoading.value = true
|
||||
try {
|
||||
await orderBatchAssignAssistant({
|
||||
order_ids: [...selectedOrderIds.value],
|
||||
assistant_id: assignForm.assistant_id
|
||||
})
|
||||
feedback.msgSuccess('指派成功')
|
||||
assignDialogVisible.value = false
|
||||
orderTableRef.value?.clearSelection()
|
||||
getLists()
|
||||
fetchTodayRevenue()
|
||||
} catch (e: any) {
|
||||
if (e?.msg) feedback.msgError(e.msg)
|
||||
} finally {
|
||||
assignLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
resetPage()
|
||||
@@ -595,14 +809,82 @@ const handleReset = () => {
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref<any>(null)
|
||||
const currentOrderIdForLog = ref(0)
|
||||
const actionLogLoading = ref(false)
|
||||
const actionLogList = ref<any[]>([])
|
||||
const actionLogTotal = ref(0)
|
||||
const actionLogPage = ref(1)
|
||||
const actionLogPageSize = ref(10)
|
||||
|
||||
const statsDialogVisible = ref(false)
|
||||
const statsLoading = ref(false)
|
||||
const statsList = ref<any[]>([])
|
||||
const statsDateRange = ref<[string, string] | null>(null)
|
||||
const statsRangeText = ref('')
|
||||
|
||||
const loadOrderActionLogs = async () => {
|
||||
if (!currentOrderIdForLog.value) {
|
||||
return
|
||||
}
|
||||
actionLogLoading.value = true
|
||||
try {
|
||||
const res: any = await orderActionLogs({
|
||||
order_id: currentOrderIdForLog.value,
|
||||
page_no: actionLogPage.value,
|
||||
page_size: actionLogPageSize.value
|
||||
})
|
||||
actionLogList.value = res?.lists || []
|
||||
actionLogTotal.value = res?.count ?? 0
|
||||
} catch {
|
||||
actionLogList.value = []
|
||||
actionLogTotal.value = 0
|
||||
} finally {
|
||||
actionLogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onActionLogSizeChange = () => {
|
||||
actionLogPage.value = 1
|
||||
loadOrderActionLogs()
|
||||
}
|
||||
|
||||
const onStatsDialogOpen = () => {
|
||||
statsDateRange.value = null
|
||||
fetchActionLogStats()
|
||||
}
|
||||
|
||||
const fetchActionLogStats = async () => {
|
||||
statsLoading.value = true
|
||||
try {
|
||||
const params: { start_time?: string; end_time?: string } = {}
|
||||
if (statsDateRange.value && statsDateRange.value.length === 2) {
|
||||
params.start_time = statsDateRange.value[0]
|
||||
params.end_time = statsDateRange.value[1]
|
||||
}
|
||||
const res: any = await orderActionLogStats(Object.keys(params).length ? params : undefined)
|
||||
statsList.value = res?.list || []
|
||||
if (res?.start_time && res?.end_time) {
|
||||
statsRangeText.value = `${res.start_time} ~ ${res.end_time}`
|
||||
} else {
|
||||
statsRangeText.value = ''
|
||||
}
|
||||
} catch {
|
||||
statsList.value = []
|
||||
statsRangeText.value = ''
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
}
|
||||
}
|
||||
const payDialogVisible = ref(false)
|
||||
const payLoading = ref(false)
|
||||
const payForm = ref({
|
||||
order_id: 0,
|
||||
order_no: '',
|
||||
amount: 0,
|
||||
payment_method: 'alipay',
|
||||
payType: 'supplement',
|
||||
/** 付呗+待审核:简化弹窗,确认后标已付 */
|
||||
fubeiPendingAudit: false,
|
||||
payment_method: 'alipay' as 'alipay' | 'wechat' | 'wechat_work' | 'manual',
|
||||
payType: 'supplement' as 'supplement',
|
||||
supplement_order_no: ''
|
||||
})
|
||||
|
||||
@@ -614,7 +896,9 @@ const patientLoading = ref(false)
|
||||
const patientList = ref<any[]>([])
|
||||
|
||||
const createForm = reactive({
|
||||
createType: 'normal' as 'normal' | 'wechat_work',
|
||||
createType: 'normal' as 'normal' | 'wechat_work' | 'fubei',
|
||||
/** 仅「付呗」:开启后订单为待审核(5) */
|
||||
requirePaymentSlipAudit: false,
|
||||
patient_id: '',
|
||||
order_type: '',
|
||||
amount: 0,
|
||||
@@ -662,6 +946,7 @@ const searchPatients = async (query: string) => {
|
||||
// 重置创建表单
|
||||
const resetCreateForm = () => {
|
||||
createForm.createType = 'normal'
|
||||
createForm.requirePaymentSlipAudit = false
|
||||
createForm.patient_id = ''
|
||||
createForm.order_type = ''
|
||||
createForm.amount = 0
|
||||
@@ -774,22 +1059,38 @@ const submitCreateOrder = async () => {
|
||||
|
||||
createLoading.value = true
|
||||
|
||||
const params = {
|
||||
patient_id: createForm.patient_id,
|
||||
order_type: createForm.order_type,
|
||||
amount: createForm.amount,
|
||||
remark: createForm.remark
|
||||
}
|
||||
|
||||
if (createForm.createType === 'wechat_work') {
|
||||
const params = {
|
||||
patient_id: createForm.patient_id,
|
||||
order_type: createForm.order_type,
|
||||
amount: createForm.amount,
|
||||
remark: createForm.remark
|
||||
}
|
||||
const res: any = await orderCreateForWechatWork(params)
|
||||
const orderNo = res?.order_no ?? res?.data?.order_no
|
||||
createDialogVisible.value = false
|
||||
getLists()
|
||||
feedback.msgSuccess(orderNo ? `订单创建成功,请在企业微信收款时使用订单号:${orderNo}` : '订单创建成功')
|
||||
} else {
|
||||
const params: Record<string, unknown> = {
|
||||
patient_id: createForm.patient_id,
|
||||
order_type: createForm.order_type,
|
||||
amount: createForm.amount,
|
||||
remark: createForm.remark
|
||||
}
|
||||
if (createForm.createType === 'fubei') {
|
||||
params.payment_channel = 'fubei'
|
||||
params.require_payment_slip_audit = createForm.requirePaymentSlipAudit ? 1 : 0
|
||||
} else {
|
||||
params.payment_channel = 'normal'
|
||||
params.require_payment_slip_audit = 0
|
||||
}
|
||||
await orderCreate(params)
|
||||
feedback.msgSuccess('订单创建成功')
|
||||
const tip =
|
||||
createForm.createType === 'fubei' && createForm.requirePaymentSlipAudit
|
||||
? '订单已创建,支付状态为「待审核」'
|
||||
: '订单创建成功'
|
||||
feedback.msgSuccess(tip)
|
||||
createDialogVisible.value = false
|
||||
getLists()
|
||||
}
|
||||
@@ -854,7 +1155,9 @@ const getPaymentMethodText = (method: string | null) => {
|
||||
const methodMap: Record<string, string> = {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信',
|
||||
wechat_work: '企业微信'
|
||||
wechat_work: '企业微信',
|
||||
fubei: '付呗',
|
||||
manual: '手动确认'
|
||||
}
|
||||
return methodMap[method] || '未知'
|
||||
}
|
||||
@@ -912,7 +1215,10 @@ const handleDetail = async (row: any) => {
|
||||
try {
|
||||
const res = await orderDetail({ id: row.id })
|
||||
detailData.value = res
|
||||
currentOrderIdForLog.value = row.id
|
||||
actionLogPage.value = 1
|
||||
detailVisible.value = true
|
||||
loadOrderActionLogs()
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
}
|
||||
@@ -920,11 +1226,13 @@ const handleDetail = async (row: any) => {
|
||||
|
||||
// 支付订单
|
||||
const handlePay = (row: any) => {
|
||||
const fubeiPending = row.status === 5 && row.payment_method === 'fubei'
|
||||
payForm.value = {
|
||||
order_id: row.id,
|
||||
order_no: row.order_no,
|
||||
amount: row.amount,
|
||||
payment_method: 'alipay',
|
||||
fubeiPendingAudit: fubeiPending,
|
||||
payment_method: fubeiPending ? 'manual' : 'alipay',
|
||||
payType: 'supplement',
|
||||
supplement_order_no: ''
|
||||
}
|
||||
@@ -934,6 +1242,20 @@ const handlePay = (row: any) => {
|
||||
// 确认支付
|
||||
const confirmPay = async () => {
|
||||
try {
|
||||
// 付呗待审核:直接标记已支付
|
||||
if (payForm.value.fubeiPendingAudit) {
|
||||
payLoading.value = true
|
||||
await orderPay({
|
||||
id: payForm.value.order_id,
|
||||
payment_method: payForm.value.payment_method
|
||||
})
|
||||
feedback.msgSuccess('已更新为已支付')
|
||||
payDialogVisible.value = false
|
||||
getLists()
|
||||
fetchTodayRevenue()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证补单支付的订单号
|
||||
if (payForm.value.payType === 'supplement' && !payForm.value.supplement_order_no) {
|
||||
feedback.msgWarning('请输入支付订单号')
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:title="drawerTitle"
|
||||
size="60%"
|
||||
:before-close="handleClose"
|
||||
:z-index="1500"
|
||||
:z-index="viewOnly ? 4000 : 1500"
|
||||
:modal="true"
|
||||
class="diagnosis-drawer"
|
||||
>
|
||||
@@ -18,6 +18,10 @@
|
||||
:rules="formRules"
|
||||
label-width="160px"
|
||||
>
|
||||
<fieldset
|
||||
:disabled="viewOnly"
|
||||
class="diagnosis-view-fieldset border-0 min-w-0 p-0 m-0"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
|
||||
@@ -520,6 +524,7 @@
|
||||
placeholder="请输入病史补充"
|
||||
/>
|
||||
</el-form-item>
|
||||
</fieldset>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -664,11 +669,11 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3 px-4 pb-4">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'basic'"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
<el-button @click="handleClose">{{ viewOnly ? '关闭' : '取消' }}</el-button>
|
||||
<el-button
|
||||
v-if="!viewOnly && activeTab === 'basic'"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitting"
|
||||
>
|
||||
确定
|
||||
@@ -711,6 +716,8 @@ const callRecordPanelRef = ref<{ refresh?: () => void } | null>(null)
|
||||
const prescriptionRef = ref()
|
||||
|
||||
const visible = ref(false)
|
||||
/** 只读查看:与 consumers 处方等处「查看诊单」一致,全部 Tab 仅展示不可保存 */
|
||||
const viewOnly = ref(false)
|
||||
const formRef = ref()
|
||||
const mode = ref('add')
|
||||
const submitting = ref(false)
|
||||
@@ -721,7 +728,10 @@ const hasPhonePlainPermission = computed(() => hasPermission(['tcm.diagnosis/pho
|
||||
const showPhoneMaskedEdit = computed(() => mode.value === 'edit' && !hasPhonePlainPermission.value)
|
||||
const phoneRevealUnlocked = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
const drawerTitle = computed(() => (mode.value === 'add' ? '新增诊单' : '编辑诊单'))
|
||||
const drawerTitle = computed(() => {
|
||||
if (viewOnly.value) return '诊单详情'
|
||||
return mode.value === 'add' ? '新增诊单' : '编辑诊单'
|
||||
})
|
||||
|
||||
watch([visible, activeTab], () => {
|
||||
if (!visible.value) return
|
||||
@@ -1075,7 +1085,22 @@ const getDictOptions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
|
||||
const data = await tcmDiagnosisDetail({ id })
|
||||
|
||||
originalPhone.value = data.phone || ''
|
||||
originalIdCard.value = data.id_card || ''
|
||||
|
||||
data.phone = hasPhonePlainPermission.value ? data.phone || '' : maskPhone(data.phone || '')
|
||||
data.id_card = maskIdCard(data.id_card || '')
|
||||
|
||||
formData.value = data
|
||||
const y = data.diabetes_discovery_year
|
||||
formData.value.diabetes_discovery_year = y == null || y === '' ? '' : String(y)
|
||||
}
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
viewOnly.value = false
|
||||
mode.value = type
|
||||
visible.value = true
|
||||
activeTab.value = 'basic' // 重置到基本信息标签页
|
||||
@@ -1085,27 +1110,27 @@ const open = async (type: string, id?: number) => {
|
||||
await getDictOptions()
|
||||
|
||||
if (type === 'edit' && id) {
|
||||
const data = await tcmDiagnosisDetail({ id })
|
||||
|
||||
// 保存原始完整数据
|
||||
originalPhone.value = data.phone || ''
|
||||
originalIdCard.value = data.id_card || ''
|
||||
|
||||
// 有「手机号明文」权限:表单中保持完整号码;否则表单内为脱敏(编辑无权限时用只读区+点击展开)
|
||||
data.phone = hasPhonePlainPermission.value ? data.phone || '' : maskPhone(data.phone || '')
|
||||
data.id_card = maskIdCard(data.id_card || '')
|
||||
|
||||
formData.value = data
|
||||
const y = data.diabetes_discovery_year
|
||||
formData.value.diabetes_discovery_year =
|
||||
y == null || y === '' ? '' : String(y)
|
||||
await loadDiagnosisDetailIntoForm(id)
|
||||
} else if (type === 'add') {
|
||||
const userStore = useUserStore()
|
||||
formData.value.assistant_id = userStore.userInfo?.id || ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 只读打开:与编辑同一套界面与全部 Tab,不可保存 */
|
||||
const openViewOnly = async (id: number) => {
|
||||
if (!id) return
|
||||
viewOnly.value = true
|
||||
mode.value = 'edit'
|
||||
visible.value = true
|
||||
activeTab.value = 'basic'
|
||||
phoneRevealUnlocked.value = false
|
||||
await getDictOptions()
|
||||
await loadDiagnosisDetailIntoForm(id)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (viewOnly.value) return
|
||||
await formRef.value?.validate()
|
||||
|
||||
submitting.value = true
|
||||
@@ -1239,12 +1264,14 @@ const handleClose = () => {
|
||||
isPhoneFocused.value = false
|
||||
isIdCardFocused.value = false
|
||||
phoneRevealUnlocked.value = false
|
||||
viewOnly.value = false
|
||||
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
openViewOnly,
|
||||
refreshCaseList: () => caseRecordListRef.value?.refresh?.()
|
||||
})
|
||||
</script>
|
||||
@@ -1252,7 +1279,15 @@ defineExpose({
|
||||
<style lang="scss" scoped>
|
||||
.edit-drawer {
|
||||
font-size: 12px;
|
||||
|
||||
|
||||
.diagnosis-view-fieldset:disabled {
|
||||
opacity: 1;
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__inner) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
padding-bottom: 20px;
|
||||
font-size: 12px;
|
||||
@@ -1415,14 +1450,7 @@ defineExpose({
|
||||
z-index: 3000 !important;
|
||||
}
|
||||
|
||||
/* 降低抽屉及其遮罩层的z-index */
|
||||
.el-drawer {
|
||||
z-index: 1500 !important;
|
||||
}
|
||||
|
||||
.el-overlay {
|
||||
z-index: 1499 ;
|
||||
}
|
||||
/* 勿对全局 .el-drawer / .el-overlay 写 z-index,会覆盖子组件的 :z-index(例如从消费处方里嵌套打开只读诊单时会被压在下面) */
|
||||
|
||||
/* 确保日期选择器和下拉选择器的弹出层在最上层 */
|
||||
.el-picker__popper,
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
{
|
||||
active:
|
||||
formData.pending_assign !== '1' &&
|
||||
formData.pending_booking !== '1' &&
|
||||
formData.completed_appointment !== '1' &&
|
||||
formData.pending_unscheduled !== '1' &&
|
||||
formData.appointment_date === tab.value
|
||||
}
|
||||
]"
|
||||
@@ -23,18 +22,11 @@
|
||||
<em v-if="tab.value && dateCounts[tab.value] !== undefined">{{ dateCounts[tab.value] }}</em>
|
||||
</span>
|
||||
<span
|
||||
:class="['chip', 'chip-pending-booking', { active: formData.pending_booking === '1' }]"
|
||||
@click="handlePendingBookingTabClick"
|
||||
:class="['chip', 'chip-unscheduled', { active: formData.pending_unscheduled === '1' }]"
|
||||
@click="handlePendingUnscheduledTabClick"
|
||||
>
|
||||
待预约
|
||||
<em v-if="pendingBookingCount !== undefined">{{ pendingBookingCount }}</em>
|
||||
</span>
|
||||
<span
|
||||
:class="['chip', 'chip-completed-visit', { active: formData.completed_appointment === '1' }]"
|
||||
@click="handleCompletedVisitTabClick"
|
||||
>
|
||||
已完成
|
||||
<em v-if="completedVisitCount !== undefined">{{ completedVisitCount }}</em>
|
||||
<em v-if="pendingUnscheduledCount !== undefined">{{ pendingUnscheduledCount }}</em>
|
||||
</span>
|
||||
<span
|
||||
:class="['chip', 'chip-pending', { active: formData.pending_assign === '1' }]"
|
||||
@@ -607,11 +599,7 @@ const formData = reactive({
|
||||
diagnosis_confirmed: '' as '' | '0' | '1',
|
||||
appointment_date: '' as string,
|
||||
has_appointment: '' as '' | '0' | '1',
|
||||
pending_assign: '' as '' | '1',
|
||||
/** 顶部「待预约」Tab:仅展示已建诊单且尚未挂号(无有效预约) */
|
||||
pending_booking: '' as '' | '1',
|
||||
/** 顶部「已完成」Tab:至少有一条挂号记录为已完成 status=3 */
|
||||
completed_appointment: '' as '' | '1'
|
||||
pending_assign: '' as '' | '1'
|
||||
})
|
||||
|
||||
const activeTab = ref('all')
|
||||
@@ -630,8 +618,6 @@ const confirmedOptions = [
|
||||
|
||||
const setHasAppointment = (v: string) => {
|
||||
formData.has_appointment = v as '' | '0' | '1'
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
pager.page = 1
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
@@ -639,8 +625,6 @@ const setHasAppointment = (v: string) => {
|
||||
|
||||
const setConfirmed = (v: string) => {
|
||||
formData.diagnosis_confirmed = v as '' | '0' | '1'
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
pager.page = 1
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
@@ -661,65 +645,31 @@ const dateTabs = computed(() => [
|
||||
])
|
||||
|
||||
const dateCounts = ref<Record<string, number>>({})
|
||||
const pendingBookingCount = ref<number | undefined>(undefined)
|
||||
const completedVisitCount = ref<number | undefined>(undefined)
|
||||
const pendingAssignCount = ref<number | undefined>(undefined)
|
||||
|
||||
const handleDateTabClick = async (value: string) => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = value
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
/** 已创建诊单、尚未挂号的记录(与第二行「未挂号」同条件,便于从 Tab 一键进入) */
|
||||
const handlePendingBookingTabClick = async () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = '1'
|
||||
formData.completed_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = '0'
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
/** 至少有一条挂号为「已完成」(后端 appointment.status=3) */
|
||||
const handleCompletedVisitTabClick = async () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = '1'
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
const handlePendingAssignTabClick = async () => {
|
||||
formData.pending_assign = '1'
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
// 获取各日期挂号数量 + 待预约 + 已完成 + 待分配医助数量
|
||||
// 获取各日期挂号数量 + 待分配医助数量
|
||||
const fetchDateCounts = async () => {
|
||||
try {
|
||||
const [today, tomorrow, dayAfter, noApt, doneVisit, pending] = await Promise.all([
|
||||
const [today, tomorrow, dayAfter, pending] = await Promise.all([
|
||||
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ pending_assign: 1, page_no: 1, page_size: 1 })
|
||||
])
|
||||
dateCounts.value = {
|
||||
@@ -727,8 +677,6 @@ const fetchDateCounts = async () => {
|
||||
[tomorrowStr.value]: tomorrow?.count ?? 0,
|
||||
[dayAfterStr.value]: dayAfter?.count ?? 0
|
||||
}
|
||||
pendingBookingCount.value = noApt?.count ?? 0
|
||||
completedVisitCount.value = doneVisit?.count ?? 0
|
||||
pendingAssignCount.value = pending?.count ?? 0
|
||||
} catch (e) {
|
||||
console.error('获取日期数量失败', e)
|
||||
@@ -777,8 +725,6 @@ const handleTabChange = (tabName: string | number) => {
|
||||
formData.diagnosis_confirmed = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
if (tabName === 'unconfirmed') {
|
||||
formData.diagnosis_confirmed = '0'
|
||||
} else if (tabName === 'confirmed') {
|
||||
@@ -859,8 +805,6 @@ const handleReset = () => {
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = ''
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
pager.page = 1
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
@@ -1588,36 +1532,6 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.chip-pending-booking {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-info);
|
||||
background: var(--el-color-info-light-9);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-info-light-8);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
|
||||
.chip-completed-visit {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-success);
|
||||
background: var(--el-color-success-light-9);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-success-light-8);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
.chip-pending {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-warning);
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace app\adminapi\controller\order;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\order\OrderLists;
|
||||
use app\adminapi\logic\order\OrderActionLogLogic;
|
||||
use app\adminapi\logic\order\OrderLogic;
|
||||
use app\adminapi\validate\order\OrderValidate;
|
||||
|
||||
@@ -140,15 +141,62 @@ class OrderController extends BaseAdminController
|
||||
public function detail()
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('detail');
|
||||
$detail = OrderLogic::detail((int)$params['id']);
|
||||
$orderId = (int)$params['id'];
|
||||
$detail = OrderLogic::detail($orderId);
|
||||
|
||||
if (!$detail) {
|
||||
return $this->fail('订单不存在');
|
||||
}
|
||||
|
||||
$this->logOrderAction($orderId, 'view_detail', '查看订单详情');
|
||||
|
||||
return $this->data($detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 支付单操作日志(单条订单,分页)
|
||||
*/
|
||||
public function actionLogs()
|
||||
{
|
||||
$orderId = (int)$this->request->get('order_id', 0);
|
||||
if ($orderId <= 0) {
|
||||
return $this->fail('order_id 必填');
|
||||
}
|
||||
$pageNo = (int)$this->request->get('page_no', 1);
|
||||
$pageSize = (int)$this->request->get('page_size', 20);
|
||||
$data = OrderActionLogLogic::listByOrderId($orderId, $pageNo, $pageSize);
|
||||
|
||||
return $this->success('', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 按人统计操作次数(时间范围内)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function actionLogStats()
|
||||
{
|
||||
$start = trim((string)$this->request->get('start_time', ''));
|
||||
$end = trim((string)$this->request->get('end_time', ''));
|
||||
if ($start === '' || $end === '') {
|
||||
$endTime = time();
|
||||
$startTime = $endTime - 7 * 86400;
|
||||
} else {
|
||||
$startTime = strtotime($start . ' 00:00:00') ?: 0;
|
||||
$endTime = strtotime($end . ' 23:59:59') ?: 0;
|
||||
}
|
||||
if ($startTime <= 0 || $endTime < $startTime) {
|
||||
return $this->fail('时间范围无效');
|
||||
}
|
||||
$limit = (int)$this->request->get('limit', 50);
|
||||
$rows = OrderActionLogLogic::statsByAdmin($startTime, $endTime, $limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $rows,
|
||||
'start_time' => date('Y-m-d H:i:s', $startTime),
|
||||
'end_time' => date('Y-m-d H:i:s', $endTime),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 创建订单
|
||||
* @return \think\response\Json
|
||||
@@ -157,11 +205,14 @@ class OrderController extends BaseAdminController
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('create');
|
||||
$params['creator_id'] = $this->adminId;
|
||||
|
||||
$params['payment_channel'] = (string)$this->request->post('payment_channel', 'normal');
|
||||
$params['require_payment_slip_audit'] = (int)$this->request->post('require_payment_slip_audit', 0);
|
||||
|
||||
$result = OrderLogic::create($params);
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction((int)$result->id, 'create', '创建支付单(普通/付呗等)');
|
||||
|
||||
return $this->success('创建成功', $result->toArray());
|
||||
}
|
||||
@@ -181,6 +232,10 @@ class OrderController extends BaseAdminController
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$ord = $result['order'] ?? null;
|
||||
if (is_array($ord) && !empty($ord['id'])) {
|
||||
$this->logOrderAction((int)$ord['id'], 'create_wechat_work', '创建支付单(企业微信对外收款)');
|
||||
}
|
||||
|
||||
return $this->success('创建成功,请在企业微信收款时使用以下订单号', [
|
||||
'order_no' => $result['order_no'],
|
||||
@@ -189,6 +244,51 @@ class OrderController extends BaseAdminController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 批量指派医助(写入 order.assistant_id,并记操作日志)
|
||||
*/
|
||||
public function assignAssistant()
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('assign_assistant');
|
||||
$rawIds = $params['order_ids'] ?? [];
|
||||
if (!\is_array($rawIds) || $rawIds === []) {
|
||||
return $this->fail('请选择订单');
|
||||
}
|
||||
$orderIds = array_map('intval', $rawIds);
|
||||
$orderIds = array_values(array_unique(array_filter($orderIds, static fn (int $id) => $id > 0)));
|
||||
if ($orderIds === []) {
|
||||
return $this->fail('请选择有效订单');
|
||||
}
|
||||
$allowed = [];
|
||||
foreach ($orderIds as $id) {
|
||||
$order = \app\common\model\Order::find($id);
|
||||
if (!$order) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->canEditOrder($order)) {
|
||||
return $this->fail('无权限操作订单:' . (string) ($order->order_no ?? $id));
|
||||
}
|
||||
$allowed[] = $id;
|
||||
}
|
||||
if ($allowed === []) {
|
||||
return $this->fail('没有可指派的订单');
|
||||
}
|
||||
$assistantId = (int) $params['assistant_id'];
|
||||
$result = OrderLogic::batchAssignAssistant($allowed, $assistantId);
|
||||
if ($result === false) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
foreach ($result['per_log'] as $row) {
|
||||
$this->logOrderAction((int) $row['order_id'], 'assign_assistant', (string) ($row['summary'] ?? ''));
|
||||
}
|
||||
$msg = '已变更「创建人」' . (int) $result['success'] . ' 单';
|
||||
if (!empty($result['errors'])) {
|
||||
$msg .= ',说明:' . implode(';', $result['errors']);
|
||||
}
|
||||
|
||||
return $this->success($msg, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 编辑订单(关联患者、订单类型)
|
||||
* 权限:超管或指定角色组可修改任意订单;其他用户只能修改自己创建的订单
|
||||
@@ -210,6 +310,7 @@ class OrderController extends BaseAdminController
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction($orderId, 'edit', '编辑患者/订单类型等');
|
||||
|
||||
return $this->success('编辑成功');
|
||||
}
|
||||
@@ -261,6 +362,7 @@ class OrderController extends BaseAdminController
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction($orderId, 'pay', '确认支付/标记已付,方式:' . (string)($params['payment_method'] ?? ''));
|
||||
|
||||
return $this->success('支付成功');
|
||||
}
|
||||
@@ -273,10 +375,12 @@ class OrderController extends BaseAdminController
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('cancel');
|
||||
|
||||
$result = OrderLogic::cancel((int)$params['id']);
|
||||
$oid = (int)$params['id'];
|
||||
$result = OrderLogic::cancel($oid);
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction($oid, 'cancel', '取消订单');
|
||||
|
||||
return $this->success('取消成功');
|
||||
}
|
||||
@@ -289,10 +393,12 @@ class OrderController extends BaseAdminController
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('refund');
|
||||
|
||||
$result = OrderLogic::refund((int)$params['id']);
|
||||
$oid = (int)$params['id'];
|
||||
$result = OrderLogic::refund($oid);
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction($oid, 'refund', '退款');
|
||||
|
||||
return $this->success('退款成功');
|
||||
}
|
||||
@@ -305,10 +411,12 @@ class OrderController extends BaseAdminController
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('delete');
|
||||
|
||||
$result = OrderLogic::delete((int)$params['id']);
|
||||
$oid = (int)$params['id'];
|
||||
$result = OrderLogic::delete($oid);
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction($oid, 'delete', '删除订单(软删)');
|
||||
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
@@ -409,4 +517,9 @@ class OrderController extends BaseAdminController
|
||||
return \app\common\model\Order::find((int)$params['id']);
|
||||
}
|
||||
}
|
||||
|
||||
private function logOrderAction(int $orderId, string $action, string $summary = ''): void
|
||||
{
|
||||
OrderActionLogLogic::record($orderId, (int) $this->adminId, $this->adminInfo, $action, $summary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ namespace app\adminapi\controller\tcm;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\tcm\DiagnosisLists;
|
||||
use app\adminapi\logic\order\OrderActionLogLogic;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\adminapi\validate\tcm\DiagnosisValidate;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\WechatChatRecord;
|
||||
|
||||
/**
|
||||
@@ -482,6 +484,19 @@ class DiagnosisController extends BaseAdminController
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
$orderNo = (string)($params['order_no'] ?? '');
|
||||
if ($orderNo !== '') {
|
||||
$po = Order::where('order_no', $orderNo)->find();
|
||||
if ($po) {
|
||||
OrderActionLogLogic::record(
|
||||
(int)$po->id,
|
||||
(int)$this->adminId,
|
||||
$this->adminInfo,
|
||||
'wx_qrcode',
|
||||
'生成订单小程序码'
|
||||
);
|
||||
}
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
|
||||
@@ -257,12 +257,17 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
public function complete()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('complete');
|
||||
$result = PrescriptionOrderLogic::complete((int) $params['id'], $this->adminId, $this->adminInfo);
|
||||
$result = PrescriptionOrderLogic::complete(
|
||||
(int) $params['id'],
|
||||
$this->adminId,
|
||||
$this->adminInfo,
|
||||
(int) $params['fulfillment_status']
|
||||
);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('订单已完成', $result);
|
||||
return $this->success('操作成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,122 @@ use app\common\model\auth\AdminRole;
|
||||
*/
|
||||
class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
{
|
||||
/**
|
||||
* 将列表「创建时间」参数规范为可解析的日期时间(datetimerange 已是 Y-m-d H:i:s,不能再去拼接 00:00:00,否则会变成非法字符串)
|
||||
*/
|
||||
private function normalizeListDateTimeString(string $raw, bool $isEnd): string
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw) === 1) {
|
||||
return $isEnd ? ($raw . ' 23:59:59') : ($raw . ' 00:00:00');
|
||||
}
|
||||
if (strlen($raw) === 19 && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $raw) === 1) {
|
||||
return $raw;
|
||||
}
|
||||
$ts = strtotime($raw);
|
||||
if ($ts !== false) {
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 患者关键词:在诊单表按姓名、手机号或诊单ID匹配(子查询里显式 whereOr,避免 | 在闭包/子查下兼容问题)
|
||||
*
|
||||
* @return \Closure|array{} 无匹配关键词时返回空数组表示不加条件
|
||||
*/
|
||||
private function patientKeywordSubWhere()
|
||||
{
|
||||
$kw = trim((string) ($this->params['patient_keyword'] ?? ''));
|
||||
if ($kw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return function ($query) use ($kw) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where(function ($q) use ($kw) {
|
||||
$q->where('patient_name', 'like', '%' . $kw . '%')
|
||||
->whereOr('phone', 'like', '%' . $kw . '%');
|
||||
if (preg_match('/^\d+$/', $kw) && (int) $kw > 0) {
|
||||
$q->whereOr('id', '=', (int) $kw);
|
||||
}
|
||||
})
|
||||
->field('id');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建时间:支持 int 时间戳 与 字符串 datetime 两种存法(用 OR 包裹,至少命中一类)
|
||||
*/
|
||||
private function appendCreateTimeWhere(array &$where): void
|
||||
{
|
||||
$s = !empty($this->params['create_time_start'])
|
||||
? $this->normalizeListDateTimeString((string) $this->params['create_time_start'], false)
|
||||
: '';
|
||||
$e = !empty($this->params['create_time_end'])
|
||||
? $this->normalizeListDateTimeString((string) $this->params['create_time_end'], true)
|
||||
: '';
|
||||
if ($s === '' && $e === '') {
|
||||
return;
|
||||
}
|
||||
$i0 = $s !== '' ? (int) strtotime($s) : 0;
|
||||
$i1 = $e !== '' ? (int) strtotime($e) : 0;
|
||||
if ($i0 > 0 && $i1 > 0 && $i1 < $i0) {
|
||||
$t = $i0;
|
||||
$i0 = $i1;
|
||||
$i1 = $t;
|
||||
$ts = $s;
|
||||
$s = $e;
|
||||
$e = $ts;
|
||||
}
|
||||
$intOk = $i0 > 0 || $i1 > 0;
|
||||
$strOk = $s !== '' || $e !== '';
|
||||
if (!$intOk && !$strOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
$where[] = function ($query) use ($s, $e, $i0, $i1, $intOk, $strOk) {
|
||||
$query->where(function ($q) use ($s, $e, $i0, $i1, $intOk, $strOk) {
|
||||
if ($intOk) {
|
||||
$q->where(function ($qi) use ($i0, $i1) {
|
||||
if ($i0 > 0) {
|
||||
$qi->where('create_time', '>=', $i0);
|
||||
}
|
||||
if ($i1 > 0) {
|
||||
$qi->where('create_time', '<=', $i1);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($strOk) {
|
||||
if ($intOk) {
|
||||
$q->whereOr(function ($qs) use ($s, $e) {
|
||||
if ($s !== '') {
|
||||
$qs->where('create_time', '>=', $s);
|
||||
}
|
||||
if ($e !== '') {
|
||||
$qs->where('create_time', '<=', $e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$q->where(function ($qs) use ($s, $e) {
|
||||
if ($s !== '') {
|
||||
$qs->where('create_time', '>=', $s);
|
||||
}
|
||||
if ($e !== '') {
|
||||
$qs->where('create_time', '<=', $e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 设置搜索条件
|
||||
* @return array
|
||||
@@ -24,15 +140,23 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
{
|
||||
return [
|
||||
'=' => ['order_type', 'status'],
|
||||
'like' => ['order_no'],
|
||||
// ListsSearchTrait 只识别 %like% / like% 等,「like」不会命中任何 case,订单号因此搜不到
|
||||
'%like%' => ['order_no'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 权限过滤 - 普通员工只能看自己创建的订单
|
||||
* @param array $where
|
||||
* @return void
|
||||
* 按医助筛选:与「创建人」一致(指派后创建人为该医助)
|
||||
*/
|
||||
private function appendAssistantFilter(array &$where): void
|
||||
{
|
||||
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
|
||||
if ($assistantId <= 0) {
|
||||
return;
|
||||
}
|
||||
$where[] = ['creator_id', '=', $assistantId];
|
||||
}
|
||||
|
||||
private function filterByPermission(array &$where): void
|
||||
{
|
||||
// 检查是否是超管(root字段为1)
|
||||
@@ -78,35 +202,15 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
|
||||
// 处理患者关键词搜索(从诊单表搜索)
|
||||
if (!empty($this->params['patient_keyword'])) {
|
||||
$where[] = ['patient_id', 'in', function ($query) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where('patient_name|phone', 'like', '%' . $this->params['patient_keyword'] . '%')
|
||||
->field('id');
|
||||
}];
|
||||
$pkw = $this->patientKeywordSubWhere();
|
||||
if ($pkw !== []) {
|
||||
$where[] = ['patient_id', 'in', $pkw];
|
||||
}
|
||||
|
||||
// 按诊单医助筛选(订单 patient_id 存诊单 id)
|
||||
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
|
||||
if ($assistantId > 0) {
|
||||
$where[] = ['patient_id', 'in', function ($query) use ($assistantId) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where('assistant_id', '=', $assistantId)
|
||||
->field('id');
|
||||
}];
|
||||
}
|
||||
$this->appendAssistantFilter($where);
|
||||
|
||||
$this->appendCreateTimeWhere($where);
|
||||
|
||||
// 处理创建时间范围
|
||||
if (!empty($this->params['create_time_start'])) {
|
||||
$where[] = ['create_time', '>=', $this->params['create_time_start'] . ' 00:00:00'];
|
||||
}
|
||||
|
||||
if (!empty($this->params['create_time_end'])) {
|
||||
$where[] = ['create_time', '<=', $this->params['create_time_end'] . ' 23:59:59'];
|
||||
}
|
||||
|
||||
return Order::where($where)
|
||||
->with(['patient', 'creator'])
|
||||
->order(['create_time' => 'desc'])
|
||||
@@ -135,34 +239,15 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
|
||||
// 处理患者关键词搜索(从诊单表搜索)
|
||||
if (!empty($this->params['patient_keyword'])) {
|
||||
$where[] = ['patient_id', 'in', function ($query) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where('patient_name|phone', 'like', '%' . $this->params['patient_keyword'] . '%')
|
||||
->field('id');
|
||||
}];
|
||||
$pkw = $this->patientKeywordSubWhere();
|
||||
if ($pkw !== []) {
|
||||
$where[] = ['patient_id', 'in', $pkw];
|
||||
}
|
||||
|
||||
$assistantIdCount = (int) ($this->params['assistant_id'] ?? 0);
|
||||
if ($assistantIdCount > 0) {
|
||||
$where[] = ['patient_id', 'in', function ($query) use ($assistantIdCount) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where('assistant_id', '=', $assistantIdCount)
|
||||
->field('id');
|
||||
}];
|
||||
}
|
||||
$this->appendAssistantFilter($where);
|
||||
|
||||
$this->appendCreateTimeWhere($where);
|
||||
|
||||
// 处理创建时间范围
|
||||
if (!empty($this->params['create_time_start'])) {
|
||||
$where[] = ['create_time', '>=', $this->params['create_time_start'] . ' 00:00:00'];
|
||||
}
|
||||
|
||||
if (!empty($this->params['create_time_end'])) {
|
||||
$where[] = ['create_time', '<=', $this->params['create_time_end'] . ' 23:59:59'];
|
||||
}
|
||||
|
||||
return Order::where($where)->count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,25 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源:system=系统代开(is_system_auto=1),manual=手工(0 或空)
|
||||
*/
|
||||
private function applySourceFilter($query): void
|
||||
{
|
||||
$sf = (string) ($this->params['source_filter'] ?? '');
|
||||
if ($sf === '' || $sf === 'all') {
|
||||
return;
|
||||
}
|
||||
if ($sf === 'system') {
|
||||
$query->where('is_system_auto', 1);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($sf === 'manual') {
|
||||
$query->where('is_system_auto', 0);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyVisibilityScope($query): void
|
||||
{
|
||||
$query->where(function ($query) {
|
||||
@@ -116,6 +135,7 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
{
|
||||
$query = Prescription::where($this->searchWhere);
|
||||
$this->applyAuditFilter($query);
|
||||
$this->applySourceFilter($query);
|
||||
$this->applyVisibilityScope($query);
|
||||
$this->applyCreatorIdsFilter($query);
|
||||
|
||||
@@ -208,6 +228,7 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
|
||||
{
|
||||
$query = Prescription::where($this->searchWhere);
|
||||
$this->applyAuditFilter($query);
|
||||
$this->applySourceFilter($query);
|
||||
$this->applyVisibilityScope($query);
|
||||
$this->applyCreatorIdsFilter($query);
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ use app\common\model\tcm\Prescription;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
use app\common\service\gancao\GancaoScmRecipelService;
|
||||
use think\facade\Config;
|
||||
use think\db\Query;
|
||||
|
||||
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
|
||||
{
|
||||
@@ -22,12 +24,45 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
return [
|
||||
'=' => ['prescription_id', 'fulfillment_status', 'prescription_audit_status', 'payment_slip_audit_status'],
|
||||
'%like%' => ['order_no'],
|
||||
'between_time' => 'create_time',
|
||||
];
|
||||
}
|
||||
|
||||
public function lists(): array
|
||||
/**
|
||||
* 列表/统计条数:含全部搜索条件(含创建时间区间)
|
||||
*/
|
||||
private function buildFilteredQuery(): Query
|
||||
{
|
||||
$query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time');
|
||||
$this->applyPermissionAndExtraFilters($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日汇总:与列表相同的「非时间」条件(医生/医助/患者/状态/单号等),不含创建时间区间,避免与「今日」语义重复
|
||||
*
|
||||
* @return array<int, array{0: string, 1: string, 2: mixed}>
|
||||
*/
|
||||
private function searchWhereWithoutCreateTimeRange(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->searchWhere,
|
||||
static function ($w): bool {
|
||||
if (!\is_array($w) || \count($w) < 2) {
|
||||
return true;
|
||||
}
|
||||
if (($w[0] ?? '') === 'create_time' && ($w[1] ?? '') === 'between') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private function applyPermissionAndExtraFilters($query): void
|
||||
{
|
||||
$this->applyDoctorAssistantFilters($query);
|
||||
$this->applyPatientKeywordFilter($query);
|
||||
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
|
||||
@@ -39,6 +74,85 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助:仅「诊单医助=本人」的订单计业绩
|
||||
*/
|
||||
private function applyAssistantDiagnosisOnlyFilter($query): void
|
||||
{
|
||||
$poTbl = (new PrescriptionOrder())->getTable();
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$aid = (int) $this->adminId;
|
||||
$query->whereExists(
|
||||
"SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$aid}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用查询骨架:不含 create_time 区间;其它筛选项 + 统计专用数据域(不重复 where)
|
||||
*/
|
||||
private function buildBaseQueryForStats(): Query
|
||||
{
|
||||
$query = PrescriptionOrder::where($this->searchWhereWithoutCreateTimeRange())->whereNull('delete_time');
|
||||
$this->applyDoctorAssistantFilters($query);
|
||||
$this->applyPatientKeywordFilter($query);
|
||||
if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) {
|
||||
return $query;
|
||||
}
|
||||
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
|
||||
$myRoles = array_map('intval', $this->adminInfo['role_id'] ?? []);
|
||||
if ($assistantRid > 0 && in_array($assistantRid, $myRoles, true)) {
|
||||
$this->applyAssistantDiagnosisOnlyFilter($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
|
||||
$diagIds = Diagnosis::where('assistant_id', $this->adminId)->whereNull('delete_time')->column('id');
|
||||
$query->where(function ($q) use ($diagIds) {
|
||||
$q->where('creator_id', $this->adminId);
|
||||
if ($diagIds !== []) {
|
||||
$q->whereOr('diagnosis_id', 'in', $diagIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计所对应的时间:与入参 start_time / end_time 一致;未传则按自然日「今天」
|
||||
*
|
||||
* @return array{0: int, 1: int, 2: string, 3: string}
|
||||
*/
|
||||
private function resolveStatsTimeWindow(): array
|
||||
{
|
||||
$st = trim((string) ($this->params['start_time'] ?? ''));
|
||||
$et = trim((string) ($this->params['end_time'] ?? ''));
|
||||
if ($st !== '' && $et !== '') {
|
||||
$t0 = strtotime($st);
|
||||
$t1 = strtotime($et);
|
||||
if ($t0 !== false && $t1 !== false) {
|
||||
return [(int) $t0, (int) $t1, $st, $et];
|
||||
}
|
||||
}
|
||||
$t0 = strtotime(date('Y-m-d 00:00:00'));
|
||||
$t1 = strtotime(date('Y-m-d 23:59:59'));
|
||||
if ($t0 === false || $t1 === false) {
|
||||
return [0, 0, '', ''];
|
||||
}
|
||||
|
||||
return [
|
||||
(int) $t0,
|
||||
(int) $t1,
|
||||
date('Y-m-d 00:00:00'),
|
||||
date('Y-m-d 23:59:59'),
|
||||
];
|
||||
}
|
||||
|
||||
public function lists(): array
|
||||
{
|
||||
$query = $this->buildFilteredQuery();
|
||||
$lists = $query
|
||||
->order('id', 'desc')
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
@@ -159,26 +273,87 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
|
||||
public function extend(): array
|
||||
{
|
||||
$s = $this->computeListStatsForExtend();
|
||||
|
||||
return [
|
||||
'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(),
|
||||
'gancao_scm_enabled' => GancaoScmRecipelService::isConfigured(),
|
||||
'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(),
|
||||
'gancao_scm_enabled' => GancaoScmRecipelService::isConfigured(),
|
||||
'stats_order_amount' => $s['order_amount'],
|
||||
'stats_linked_pay_amount' => $s['linked_pay_amount'],
|
||||
'stats_time_start' => $s['label_start'],
|
||||
'stats_time_end' => $s['label_end'],
|
||||
/** 统计口径:all=支付审核白名单全量;assistant=医助仅诊单协助业绩;restricted=与列表同域 */
|
||||
'stats_scope' => $s['scope'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{order_amount: float, linked_pay_amount: float, label_start: string, label_end: string, scope: string}
|
||||
*/
|
||||
private function computeListStatsForExtend(): array
|
||||
{
|
||||
[$t0, $t1, $l0, $l1] = $this->resolveStatsTimeWindow();
|
||||
if ($t0 <= 0 || $t1 <= 0) {
|
||||
return [
|
||||
'order_amount' => 0.0,
|
||||
'linked_pay_amount' => 0.0,
|
||||
'label_start' => '',
|
||||
'label_end' => '',
|
||||
'scope' => 'restricted',
|
||||
];
|
||||
}
|
||||
if ($t1 < $t0) {
|
||||
$tmp = $t0;
|
||||
$t0 = $t1;
|
||||
$t1 = $tmp;
|
||||
}
|
||||
|
||||
$q = $this->buildBaseQueryForStats()->whereBetween('create_time', [$t0, $t1]);
|
||||
$orderAmount = round((float) (clone $q)->sum('amount'), 2);
|
||||
$ids = (clone $q)->column('id');
|
||||
$ids = array_values(array_filter(array_map('intval', $ids), static function (int $id): bool {
|
||||
return $id > 0;
|
||||
}));
|
||||
if ($ids === []) {
|
||||
return $this->statsExtendPayload($orderAmount, 0.0, $l0, $l1);
|
||||
}
|
||||
$sumPaid = (float) PrescriptionOrderPayOrder::alias('l')
|
||||
->join('order o', 'l.pay_order_id = o.id')
|
||||
->whereIn('l.prescription_order_id', $ids)
|
||||
->whereNull('o.delete_time')
|
||||
->sum('o.amount');
|
||||
$sumPaid = round($sumPaid, 2);
|
||||
|
||||
return $this->statsExtendPayload($orderAmount, $sumPaid, $l0, $l1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{order_amount: float, linked_pay_amount: float, label_start: string, label_end: string, scope: string}
|
||||
*/
|
||||
private function statsExtendPayload(float $orderAmount, float $linked, string $l0, string $l1): array
|
||||
{
|
||||
$scope = 'restricted';
|
||||
if (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) {
|
||||
$scope = 'all';
|
||||
} else {
|
||||
$ar = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
|
||||
if ($ar > 0 && in_array($ar, array_map('intval', $this->adminInfo['role_id'] ?? []), true)) {
|
||||
$scope = 'assistant';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'order_amount' => $orderAmount,
|
||||
'linked_pay_amount' => $linked,
|
||||
'label_start' => $l0,
|
||||
'label_end' => $l1,
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
$query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time');
|
||||
$this->applyDoctorAssistantFilters($query);
|
||||
$this->applyPatientKeywordFilter($query);
|
||||
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
|
||||
$diagIds = Diagnosis::where('assistant_id', $this->adminId)->whereNull('delete_time')->column('id');
|
||||
$query->where(function ($q) use ($diagIds) {
|
||||
$q->where('creator_id', $this->adminId);
|
||||
if ($diagIds !== []) {
|
||||
$q->whereOr('diagnosis_id', 'in', $diagIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
$query = $this->buildFilteredQuery();
|
||||
|
||||
return (int) $query->count();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\order;
|
||||
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\OrderActionLog;
|
||||
|
||||
/**
|
||||
* 支付单操作日志(写主库、失败忽略)
|
||||
*/
|
||||
class OrderActionLogLogic
|
||||
{
|
||||
/** @var array<string,string> 动作码 => 中文说明 */
|
||||
public const ACTION_LABELS = [
|
||||
'view_detail' => '查看详情',
|
||||
'edit' => '编辑订单',
|
||||
'wx_qrcode' => '小程序码',
|
||||
'pay' => '确认支付',
|
||||
'refund' => '退款',
|
||||
'cancel' => '取消订单',
|
||||
'delete' => '删除订单',
|
||||
'create' => '创建订单',
|
||||
'create_wechat_work' => '创建订单(企微对外收款)',
|
||||
'assign_assistant' => '变更创建人(指派医助)',
|
||||
];
|
||||
|
||||
public static function record(
|
||||
int $orderId,
|
||||
int $adminId,
|
||||
array $adminInfo,
|
||||
string $action,
|
||||
string $summary = ''
|
||||
): void {
|
||||
if ($orderId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adminName = (string)($adminInfo['name'] ?? '');
|
||||
if ($adminName === '' && $adminId > 0) {
|
||||
$adminName = (string) Admin::where('id', $adminId)->value('name');
|
||||
}
|
||||
|
||||
$log = new OrderActionLog();
|
||||
$log->order_id = $orderId;
|
||||
$log->admin_id = $adminId;
|
||||
$log->admin_name = mb_substr($adminName, 0, 64);
|
||||
$log->action = mb_substr($action, 0, 32);
|
||||
$log->summary = mb_substr($summary, 0, 500);
|
||||
$log->create_time = time();
|
||||
|
||||
try {
|
||||
$log->save();
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略日志表未创建等错误,不影响主业务
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条支付单操作记录列表(新在前)
|
||||
*
|
||||
* @return array{lists: array<int, array<string,mixed>>, count: int}
|
||||
*/
|
||||
public static function listByOrderId(int $orderId, int $pageNo, int $pageSize): array
|
||||
{
|
||||
if ($orderId <= 0) {
|
||||
return ['lists' => [], 'count' => 0];
|
||||
}
|
||||
|
||||
$pageSize = max(1, min(100, $pageSize));
|
||||
$pageNo = max(1, $pageNo);
|
||||
$offset = ($pageNo - 1) * $pageSize;
|
||||
|
||||
$q = OrderActionLog::where('order_id', $orderId);
|
||||
$count = (int) $q->count();
|
||||
$rows = OrderActionLog::where('order_id', $orderId)
|
||||
->order('id', 'desc')
|
||||
->limit($offset, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$code = (string)($r['action'] ?? '');
|
||||
$r['action_label'] = self::ACTION_LABELS[$code] ?? $code;
|
||||
$r['create_time_text'] = !empty($r['create_time'])
|
||||
? date('Y-m-d H:i:s', (int) $r['create_time'])
|
||||
: '';
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return ['lists' => $rows, 'count' => $count];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按时间范围统计每人操作次数(仅统计有日志表的数据)
|
||||
*
|
||||
* @return array<int, array{admin_id: int, admin_name: string, cnt: int}>
|
||||
*/
|
||||
public static function statsByAdmin(int $startTime, int $endTime, int $limit = 50): array
|
||||
{
|
||||
if ($endTime < $startTime) {
|
||||
return [];
|
||||
}
|
||||
$limit = max(1, min(200, $limit));
|
||||
|
||||
try {
|
||||
$rows = OrderActionLog::field('admin_id, admin_name, COUNT(*) AS cnt')
|
||||
->whereBetween('create_time', [$startTime, $endTime])
|
||||
->group('admin_id, admin_name')
|
||||
->order('cnt', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[] = [
|
||||
'admin_id' => (int)($r['admin_id'] ?? 0),
|
||||
'admin_name' => (string)($r['admin_name'] ?? ''),
|
||||
'cnt' => (int)($r['cnt'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace app\adminapi\logic\order;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\OrderDetail;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
@@ -38,13 +39,25 @@ class OrderLogic
|
||||
public static function create(array $params)
|
||||
{
|
||||
try {
|
||||
$channel = (string)($params['payment_channel'] ?? 'normal');
|
||||
if (!in_array($channel, ['normal', 'fubei'], true)) {
|
||||
$channel = 'normal';
|
||||
}
|
||||
$requirePaymentSlipAudit = (int)($params['require_payment_slip_audit'] ?? 0) === 1;
|
||||
|
||||
$order = new Order();
|
||||
$order->order_no = self::generateOrderNo();
|
||||
$order->patient_id = $params['patient_id'];
|
||||
$order->creator_id = $params['creator_id'];
|
||||
$order->order_type = $params['order_type'];
|
||||
$order->amount = $params['amount'];
|
||||
$order->status = 1; // 待支付
|
||||
// 付呗且申请审核支付单:待审核(5);否则待支付(1)
|
||||
if ($channel === 'fubei') {
|
||||
$order->payment_method = 'fubei';
|
||||
$order->status = $requirePaymentSlipAudit ? 5 : 1;
|
||||
} else {
|
||||
$order->status = 1; // 待支付
|
||||
}
|
||||
$order->remark = $params['remark'] ?? '';
|
||||
$order->save();
|
||||
|
||||
@@ -331,7 +344,13 @@ class OrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($order->status != 1) {
|
||||
$st = (int)$order->status;
|
||||
// 1=待支付;5=待审核(付呗+申请支付单审核) 时允许录入手动到账
|
||||
if ($st === 1) {
|
||||
// 正常待支付
|
||||
} elseif ($st === 5 && (string)($order->payment_method ?? '') === 'fubei') {
|
||||
// 待审核的付呗单,通过人工确认后标记已支付
|
||||
} else {
|
||||
self::setError('订单状态不允许支付');
|
||||
return false;
|
||||
}
|
||||
@@ -362,8 +381,9 @@ class OrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($order->status != 1) {
|
||||
self::setError('只有待支付的订单才能取消');
|
||||
$st = (int)$order->status;
|
||||
if ($st !== 1 && !($st === 5 && (string)($order->payment_method ?? '') === 'fubei')) {
|
||||
self::setError('只有待支付或待审核(付呗)的订单才能取消');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1097,4 +1117,84 @@ class OrderLogic
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量指派给医助:将「创建人」变更为目标医助(须为医助 role_id=2),用于列表筛选项为创建人=该医助
|
||||
*
|
||||
* @param int[] $orderIds
|
||||
* @return array{success: int, per_log: list<array{order_id: int, summary: string}>, fail: int, errors: list<string>}|false
|
||||
*/
|
||||
public static function batchAssignAssistant(array $orderIds, int $assistantAdminId)
|
||||
{
|
||||
$assistantAdminId = (int) $assistantAdminId;
|
||||
if ($assistantAdminId <= 0) {
|
||||
self::setError('请选择医助');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
AdminRole::where('admin_id', $assistantAdminId)
|
||||
->where('role_id', 2)
|
||||
->count() === 0
|
||||
) {
|
||||
self::setError('目标账号不是医助角色');
|
||||
return false;
|
||||
}
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $orderIds), static fn (int $id) => $id > 0)));
|
||||
if ($ids === []) {
|
||||
self::setError('请选择订单');
|
||||
return false;
|
||||
}
|
||||
if (count($ids) > 200) {
|
||||
self::setError('单次最多200单');
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetName = (string) Admin::where('id', $assistantAdminId)->whereNull('delete_time')->value('name');
|
||||
if ($targetName === '') {
|
||||
$targetName = (string) $assistantAdminId;
|
||||
}
|
||||
$perLog = [];
|
||||
$errors = [];
|
||||
foreach ($ids as $oid) {
|
||||
$order = Order::where('id', $oid)->whereNull('delete_time')->find();
|
||||
if (! $order) {
|
||||
$errors[] = "订单{$oid}不存在或已删除";
|
||||
continue;
|
||||
}
|
||||
$oldCreatorId = (int) $order->creator_id;
|
||||
if ($oldCreatorId === $assistantAdminId) {
|
||||
$errors[] = "订单{$oid}创建人已是该医助,已跳过";
|
||||
continue;
|
||||
}
|
||||
$oldName = $oldCreatorId > 0
|
||||
? (string) Admin::where('id', $oldCreatorId)->whereNull('delete_time')->value('name')
|
||||
: '';
|
||||
if ($oldName === '' && $oldCreatorId > 0) {
|
||||
$oldName = (string) $oldCreatorId;
|
||||
} elseif ($oldName === '') {
|
||||
$oldName = '—';
|
||||
}
|
||||
$order->creator_id = $assistantAdminId;
|
||||
if (! $order->save()) {
|
||||
$errors[] = "订单{$oid}保存失败";
|
||||
continue;
|
||||
}
|
||||
$perLog[] = [
|
||||
'order_id' => (int) $order->id,
|
||||
'summary' => '创建人由「' . $oldName . '」变更为医助「' . $targetName . '」',
|
||||
];
|
||||
}
|
||||
|
||||
if ($perLog === []) {
|
||||
self::setError($errors !== [] ? implode(';', $errors) : '未能变更任何订单');
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => count($perLog),
|
||||
'per_log' => $perLog,
|
||||
'fail' => count($errors),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,22 @@ class PrescriptionOrderLogic
|
||||
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表顶栏金额统计:与「关联支付单」审核白名单同档的角色可见「全量」统计(不缩小到本人/医助诊单域)
|
||||
*/
|
||||
public static function canViewOrderListStatsAllScope(array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3, 4, 9, 6]);
|
||||
if (!is_array($allow)) {
|
||||
$allow = [0, 3];
|
||||
}
|
||||
|
||||
return self::roleIntersect($adminInfo, $allow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $row
|
||||
*/
|
||||
@@ -424,6 +440,12 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int) ($rx['void_status'] ?? 0) === 1) {
|
||||
self::$error = '已作废处方不可创建业务订单';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PrescriptionOrder::where('prescription_id', $rxId)->whereNull('delete_time')->where('fulfillment_status', '<>', 4)->count() > 0) {
|
||||
self::$error = '该处方已存在有效业务订单(未撤回前不可重复创建)';
|
||||
|
||||
@@ -1513,14 +1535,25 @@ class PrescriptionOrderLogic
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 完成订单时可选的履约终态:3=已完成,7-12=业务结案类状态 */
|
||||
private const COMPLETE_FULFILLMENT_TARGETS = [3, 7, 8, 9, 10, 11, 12];
|
||||
|
||||
/** 完成时是否按「实付=关联支付单金额」重算并清空诊单医助(与 clearDiagnosisAssistantOnComplete 的终态判断一致) */
|
||||
private const COMPLETE_FULFILLMENT_TERMINAL = [3, 8, 9, 10, 11, 12];
|
||||
|
||||
/**
|
||||
* 将「已发货」(fulfillment_status=5) 且支付审核已通过的订单标记为「已完成」(3)。
|
||||
* 将「已发货/已签收」且支付审核已通过的订单结案:可选「已完成」或 7-12 业务状态。
|
||||
*
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function complete(int $id, int $adminId, array $adminInfo)
|
||||
public static function complete(int $id, int $adminId, array $adminInfo, int $targetFulfillmentStatus = 3)
|
||||
{
|
||||
self::$error = '';
|
||||
if (!in_array($targetFulfillmentStatus, self::COMPLETE_FULFILLMENT_TARGETS, true)) {
|
||||
self::$error = '无效的完成状态';
|
||||
return false;
|
||||
}
|
||||
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
@@ -1530,8 +1563,9 @@ class PrescriptionOrderLogic
|
||||
self::$error = '无权限操作';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->fulfillment_status !== 5) {
|
||||
self::$error = '仅「已发货」状态可标记为已完成';
|
||||
$curFs = (int) $order->fulfillment_status;
|
||||
if ($curFs !== 5 && $curFs !== 6) {
|
||||
self::$error = '仅「已发货」或「已签收」状态可完成订单';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->payment_slip_audit_status !== 1) {
|
||||
@@ -1539,17 +1573,22 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算关联订单总额
|
||||
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
|
||||
$linkedPayOrders = self::linkedPayOrdersPayload($linkedPayOrderIds);
|
||||
$label = self::fulfillmentStatusLabel($targetFulfillmentStatus);
|
||||
|
||||
$linkedPayOrderIds = self::linkedPayOrderIdList($id);
|
||||
$linkedPayOrders = self::linkedPayOrdersPayload($linkedPayOrderIds);
|
||||
$linkedPayPaidTotal = 0.0;
|
||||
foreach ($linkedPayOrders as $o) {
|
||||
$linkedPayPaidTotal += (float) ($o['amount'] ?? 0);
|
||||
}
|
||||
$linkedPayPaidTotal = round($linkedPayPaidTotal, 2);
|
||||
|
||||
$order->fulfillment_status = 3;
|
||||
$order->paid = $linkedPayPaidTotal;
|
||||
if ($targetFulfillmentStatus === 3) {
|
||||
$order->fulfillment_status = 3;
|
||||
$order->paid = $linkedPayPaidTotal;
|
||||
} else {
|
||||
$order->fulfillment_status = $targetFulfillmentStatus;
|
||||
}
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
@@ -1558,14 +1597,22 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
|
||||
$unassignSummary = '';
|
||||
if (in_array($targetFulfillmentStatus, self::COMPLETE_FULFILLMENT_TERMINAL, true)) {
|
||||
$unassignSummary = self::clearDiagnosisAssistantOnComplete((int) $order->diagnosis_id);
|
||||
}
|
||||
|
||||
if ($targetFulfillmentStatus === 3) {
|
||||
$logLine = '订单完成,状态变更为「' . $label . '」,实付金额更新为 ¥' . $linkedPayPaidTotal . $unassignSummary;
|
||||
} else {
|
||||
$logLine = '订单处理完成,状态变更为「' . $label . '」' . $unassignSummary;
|
||||
}
|
||||
self::writeLog(
|
||||
$id,
|
||||
$adminId,
|
||||
$adminInfo,
|
||||
'complete',
|
||||
'订单完成,状态变更为「已完成」,实付金额更新为 ¥' . $linkedPayPaidTotal . $unassignSummary
|
||||
$logLine
|
||||
);
|
||||
|
||||
$out = $order->toArray();
|
||||
@@ -1575,6 +1622,26 @@ class PrescriptionOrderLogic
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function fulfillmentStatusLabel(int $fs): string
|
||||
{
|
||||
$m = [
|
||||
1 => '待双审通过',
|
||||
2 => '待发货',
|
||||
3 => '已完成',
|
||||
4 => '已取消',
|
||||
5 => '已发货',
|
||||
6 => '已签收',
|
||||
7 => '进行中',
|
||||
8 => '暂不制药',
|
||||
9 => '拒收',
|
||||
10 => '退款',
|
||||
11 => '保留药方',
|
||||
12 => '制药缓发',
|
||||
];
|
||||
|
||||
return $m[$fs] ?? ('状态' . (string) $fs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态
|
||||
* 仅当该患者没有其它进行中的处方订单时才清空,避免误覆盖
|
||||
@@ -1600,7 +1667,7 @@ class PrescriptionOrderLogic
|
||||
// 同一诊单下若仍有未完成/未取消的处方订单,则不清空
|
||||
$pendingCount = PrescriptionOrder::where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->whereNotIn('fulfillment_status', [3, 4])
|
||||
->whereNotIn('fulfillment_status', [3, 4, 8, 9, 10, 11, 12])
|
||||
->count();
|
||||
if ($pendingCount > 0) {
|
||||
return '';
|
||||
@@ -1635,7 +1702,7 @@ class PrescriptionOrderLogic
|
||||
if ($pa !== 1) {
|
||||
return false;
|
||||
}
|
||||
if ($fs === 3 || $fs === 4) {
|
||||
if (in_array($fs, [3, 4, 8, 9, 10, 11, 12], true)) {
|
||||
return false;
|
||||
}
|
||||
if (trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '') {
|
||||
|
||||
@@ -20,10 +20,12 @@ class OrderValidate extends BaseValidate
|
||||
'order_type' => 'require|in:1,2,3,4,5,6,7',
|
||||
'amount' => 'require|float',
|
||||
'status' => 'require|in:1,2,3,4',
|
||||
'payment_method' => 'in:alipay,wechat,wechat_work',
|
||||
'payment_method' => 'in:alipay,wechat,wechat_work,fubei,manual',
|
||||
'remark' => 'string|max:500',
|
||||
'order_no' => 'string|max:50',
|
||||
'is_supplement' => 'in:0,1',
|
||||
'order_ids' => 'require|array',
|
||||
'assistant_id' => 'require|integer|gt:0',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
@@ -44,5 +46,6 @@ class OrderValidate extends BaseValidate
|
||||
'cancel' => ['id'],
|
||||
'refund' => ['id'],
|
||||
'delete' => ['id'],
|
||||
'assign_assistant' => ['order_ids', 'assistant_id'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'remark_extra' => 'max:500',
|
||||
'action' => 'require|in:approve,reject',
|
||||
'remark' => 'max:500',
|
||||
'fulfillment_status' => 'require|integer|in:3,7,8,9,10,11,12',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
@@ -64,7 +65,7 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'paidPayOrders' => ['diagnosis_id'],
|
||||
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
|
||||
'linkPayOrder' => ['id', 'pay_order_id'],
|
||||
'complete' => ['id'],
|
||||
'complete' => ['id', 'fulfillment_status'],
|
||||
'submitGancaoRecipel' => ['id'],
|
||||
'previewGancaoRecipel' => ['id'],
|
||||
];
|
||||
|
||||
@@ -121,7 +121,7 @@ class Order extends BaseModel
|
||||
*/
|
||||
public function getStatusDescAttr($value, $data)
|
||||
{
|
||||
$statusMap = [1 => '待支付', 2 => '已支付', 3 => '已取消', 4 => '已退款'];
|
||||
$statusMap = [1 => '待支付', 2 => '已支付', 3 => '已取消', 4 => '已退款', 5 => '待审核'];
|
||||
return $statusMap[$data['status']] ?? '未知';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
/**
|
||||
* 支付单(zyt_order)后台操作日志
|
||||
*/
|
||||
class OrderActionLog extends BaseModel
|
||||
{
|
||||
protected $name = 'order_action_log';
|
||||
|
||||
protected $autoWriteTimestamp = false;
|
||||
}
|
||||
@@ -117,6 +117,9 @@ return [
|
||||
// 业务订单「关联支付单」审核:以下角色 ID + root 可操作(可与财务角色一致)
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 4, 9, 6],
|
||||
|
||||
// 业务订单列表金额统计:医助角色 ID;非支付审核白名单的医助仅统计其诊单 assistant_id=本人 的业绩
|
||||
'prescription_order_stats_assistant_role_id' => 2,
|
||||
|
||||
// 腾讯云实时音视频(TRTC)配置
|
||||
'trtc' => [
|
||||
'sdkAppId' => (int)env('trtc.sdk_app_id', 1600127710),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 支付单(zyt_order)后台操作日志:操作人、动作、时间
|
||||
CREATE TABLE IF NOT EXISTS `zyt_order_action_log` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`order_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT 'zyt_order.id',
|
||||
`admin_id` int(11) unsigned NOT NULL DEFAULT 0,
|
||||
`admin_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人姓名快照',
|
||||
`action` varchar(32) NOT NULL DEFAULT '' COMMENT '动作码',
|
||||
`summary` varchar(500) NOT NULL DEFAULT '' COMMENT '补充说明',
|
||||
`create_time` int(11) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_admin_id_time` (`admin_id`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付单操作日志';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 已改为仅更新「创建人 creator_id」,不再使用 assistant_id 列;若已执行过 add 脚本可执行本脚本清理
|
||||
-- 若表上无此列,执行会报错,可忽略或先 SHOW COLUMNS 确认
|
||||
ALTER TABLE `zyt_order` DROP INDEX `idx_assistant_id`, DROP COLUMN `assistant_id`;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 支付单指派医助:后台批量指派、列表筛选
|
||||
-- 表前缀 zyt_ 与 .env 中 PREFIX 一致
|
||||
ALTER TABLE `zyt_order`
|
||||
ADD COLUMN `assistant_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '指派医助 la_admin.id,0=未指派' AFTER `creator_id`,
|
||||
ADD KEY `idx_assistant_id` (`assistant_id`);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 处方业务订单:查看「操作日志」区块与 /tcm.prescriptionOrder/logs 接口
|
||||
SET @po_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/lists' LIMIT 1);
|
||||
|
||||
INSERT INTO zyt_system_menu (
|
||||
pid, type, name, icon, sort, perms, paths, component,
|
||||
selected, params, is_cache, is_show, is_disable, create_time, update_time
|
||||
)
|
||||
SELECT
|
||||
@po_menu_id, 'A', '操作日志', '', 9, 'tcm.prescriptionOrder/logs', '', '',
|
||||
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE @po_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/logs');
|
||||
Reference in New Issue
Block a user