更新
This commit is contained in:
+356
-37
@@ -123,7 +123,10 @@
|
||||
</el-card>
|
||||
|
||||
<el-card class="yeji-panel yeji-main-tabs-card" shadow="never">
|
||||
<div class="yeji-table-tabs-head">表格统计</div>
|
||||
<div class="yeji-table-tabs-head">
|
||||
<span class="yeji-table-tabs-head__title">表格统计</span>
|
||||
<span class="yeji-table-tabs-head__hint">列较多时可横向滚动浏览;「部门 / 排名」列在滚动时保持固定。</span>
|
||||
</div>
|
||||
<el-tabs v-model="yejiDisplayTab" class="yeji-view-tabs">
|
||||
<el-tab-pane label="医生统计" name="doctor" lazy>
|
||||
<el-card
|
||||
@@ -208,7 +211,8 @@
|
||||
({{ leaderboardBlock.channel_name }})
|
||||
</span>
|
||||
</div>
|
||||
<table class="yeji-table__body">
|
||||
<div class="yeji-table__scroll" tabindex="0" role="region" aria-label="排行榜,可横向滚动">
|
||||
<table class="yeji-table__body yeji-table__body--leaderboard">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-rank">排名</th>
|
||||
@@ -274,6 +278,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="yeji-table__foot yeji-table__foot--left">
|
||||
<span>诊金单位:元 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线(元/进线)</span>
|
||||
<span v-if="lb.er_center_subtree">
|
||||
@@ -381,7 +386,8 @@
|
||||
<div class="yeji-table__header">
|
||||
{{ buildTableTitle(tb) }}
|
||||
</div>
|
||||
<table class="yeji-table__body">
|
||||
<div class="yeji-table__scroll" tabindex="0" role="region" aria-label="业绩数据表,可横向滚动">
|
||||
<table class="yeji-table__body yeji-table__body--data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-dept">部门</th>
|
||||
@@ -444,24 +450,24 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
|
||||
按订单<b>创建时间</b>统计,与「处方订单列表」业绩合计口径一致。<br />
|
||||
按订单<b>创建时间</b>统计,与「处方订单列表」业绩合计口径一致(<b>诊单医助</b>归属展示部门)。<br />
|
||||
<b>不受渠道筛选影响</b>,展示部门下全部订单。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<th v-if="tb.channel_name" class="col-completed">
|
||||
<span>{{ buildChannelColTitle(tb, '业绩') }}</span>
|
||||
<th v-if="tb.channel_name" class="col-completed col-narrow-head">
|
||||
<span class="yeji-th-label">{{ yejiChannelMetricShort('perf') }}</span>
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
|
||||
与「处方订单列表」业绩<b>同口径</b>:按订单 <b>create_time</b> 归日,<br />
|
||||
fulfillment_status ≠ 4。<br />
|
||||
渠道判定仅用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。
|
||||
「{{ tb.channel_name }}」渠道业绩(与卡片标题渠道一致)。与「处方订单列表」业绩<b>同口径</b>:按订单
|
||||
<b>create_time</b> 归日,剔除履约已取消(4)。<br />
|
||||
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<th>
|
||||
@@ -488,27 +494,27 @@
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<th v-if="tb.channel_name" class="col-completed">
|
||||
<span>{{ buildChannelColTitle(tb, '成交单') }}</span>
|
||||
<th v-if="tb.channel_name" class="col-completed col-narrow-head">
|
||||
<span class="yeji-th-label">{{ yejiChannelMetricShort('deal') }}</span>
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
与「{{ tb.channel_name }}业绩」同源:统计区间内 <b>非取消业务订单</b> 条数(fulfillment_status ≠ 4),<br />
|
||||
按订单 <b>create_time</b> 归日;渠道判定同业绩列(患者须有对应 channels 的已完成挂号)。
|
||||
「{{ tb.channel_name }}」渠道成交单数;与渠道业绩同源:区间内 <b>非取消业务订单</b> 条数,按订单
|
||||
<b>create_time</b> 归日;渠道判定同业绩列。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<th v-if="tb.channel_name" class="col-completed">
|
||||
<span>{{ buildChannelColTitle(tb, '客单价') }}</span>
|
||||
<th v-if="tb.channel_name" class="col-completed col-narrow-head">
|
||||
<span class="yeji-th-label">{{ yejiChannelMetricShort('aov') }}</span>
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 280px; line-height: 1.7; font-size: 12px">
|
||||
{{ tb.channel_name }}业绩 ÷ {{ tb.channel_name }}接诊诊单(均值)。
|
||||
「{{ tb.channel_name }}」渠道客单价 ≈ 渠道业绩 ÷ 渠道成交单。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<th>
|
||||
@@ -609,6 +615,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="yeji-table__foot">
|
||||
<span>区间:{{ tb.start_date }} ~ {{ tb.end_date }}</span>
|
||||
<span v-if="tb.channel_name"> · 渠道:{{ tb.channel_name }}</span>
|
||||
@@ -830,7 +837,7 @@
|
||||
<el-drawer
|
||||
v-model="orderDrawerVisible"
|
||||
direction="rtl"
|
||||
size="min(960px, 96vw)"
|
||||
size="min(1120px, 98vw)"
|
||||
destroy-on-close
|
||||
class="yeji-order-drawer"
|
||||
:show-close="true"
|
||||
@@ -861,7 +868,36 @@
|
||||
orderDrawerConsultCount === null ? '—' : orderDrawerConsultCount.toLocaleString('zh-CN')
|
||||
}} 次</span>
|
||||
</div>
|
||||
<div class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--accent">
|
||||
<template v-if="orderDrawerChannelSplitActive && orderDrawerSplitPerfCache">
|
||||
<div class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--total">
|
||||
<span class="yeji-order-drawer__stat-label">
|
||||
合计业绩
|
||||
<span class="yeji-order-drawer__stat-note"
|
||||
>(与看板「合计业绩」列同口径,不限渠道;{{ orderDrawerSplitPerfCache.totalOrderCount.toLocaleString('zh-CN') }} 笔)</span
|
||||
>
|
||||
</span>
|
||||
<span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
|
||||
¥{{ formatOrderDrawerMoney(orderDrawerSplitPerfCache.totalAmount) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--accent">
|
||||
<span class="yeji-order-drawer__stat-label">
|
||||
{{ orderDrawerChannelLabel }}业绩
|
||||
<span class="yeji-order-drawer__stat-note"
|
||||
>(与看板「{{ orderDrawerChannelLabel }}业绩」同口径;{{
|
||||
orderDrawerSplitPerfCache.channelOrderCount.toLocaleString('zh-CN')
|
||||
}} 笔)</span
|
||||
>
|
||||
</span>
|
||||
<span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
|
||||
¥{{ formatOrderDrawerMoney(orderDrawerChannelStatAmount) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--accent"
|
||||
>
|
||||
<span class="yeji-order-drawer__stat-label">
|
||||
业务订单金额合计
|
||||
<span class="yeji-order-drawer__stat-note">(不含履约已取消,与上方业绩口径一致)</span>
|
||||
@@ -879,6 +915,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-tabs
|
||||
v-if="orderDrawerChannelSplitActive"
|
||||
v-model="orderDrawerListTab"
|
||||
class="yeji-order-drawer__list-tabs"
|
||||
@tab-change="onOrderDrawerListTabChange"
|
||||
>
|
||||
<el-tab-pane label="合计业绩(全量订单)" name="total" />
|
||||
<el-tab-pane :label="`渠道筛选(${orderDrawerChannelLabel})`" name="channel" />
|
||||
</el-tabs>
|
||||
|
||||
<div v-loading="orderDrawerLoading" class="yeji-order-drawer__body">
|
||||
<el-table
|
||||
v-if="orderDrawerLists.length > 0 || orderDrawerLoading"
|
||||
@@ -892,6 +938,39 @@
|
||||
>
|
||||
<el-table-column type="index" label="#" width="46" :index="orderDrawerIndexMethod" />
|
||||
<el-table-column prop="order_no" label="订单号" min-width="152" show-overflow-tooltip />
|
||||
<el-table-column label="关联挂号" min-width="218">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.linked_appointment" class="yeji-order-appt-lines">
|
||||
<div class="yeji-order-appt-line">
|
||||
<span class="yeji-order-appt-k">日期</span>
|
||||
{{ orderDrawerApptSchedule(row.linked_appointment) }}
|
||||
</div>
|
||||
<div class="yeji-order-appt-line">
|
||||
<span class="yeji-order-appt-k">状态</span>
|
||||
{{ row.linked_appointment.status_desc || '—' }}
|
||||
<span
|
||||
v-if="row.linked_appointment.doctor_name"
|
||||
class="yeji-order-appt-sub"
|
||||
>
|
||||
· {{ row.linked_appointment.doctor_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="yeji-order-appt-line">
|
||||
<span class="yeji-order-appt-k">类型</span>
|
||||
{{ row.linked_appointment.appointment_type_desc || '—' }}
|
||||
</div>
|
||||
<div class="yeji-order-appt-line">
|
||||
<span class="yeji-order-appt-k">渠道</span>
|
||||
{{
|
||||
row.linked_appointment.channel_source_desc ||
|
||||
row.linked_appointment.channel_source ||
|
||||
'—'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="yeji-order-appt-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="108" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="yeji-order-amt">¥{{ row.amount ?? '—' }}</span>
|
||||
@@ -955,7 +1034,7 @@
|
||||
class="yeji-unassigned-dialog__table"
|
||||
:empty-text="unassignedDialogLoading ? '加载中…' : '暂无数据'"
|
||||
>
|
||||
<el-table-column prop="name" label="创建人" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="诊单医助" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="账号" width="88" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.admin_id > 0 ? row.admin_id : '—' }}
|
||||
@@ -1822,6 +1901,15 @@ const orderDrawerLists = ref<Array<Record<string, any>>>([])
|
||||
const orderDrawerCount = ref(0)
|
||||
const orderDrawerPage = ref(1)
|
||||
const orderDrawerPageSize = ref(15)
|
||||
/** 侧栏列表:有渠道筛选时,合计业绩=全量订单 / 渠道=收窄订单 */
|
||||
const orderDrawerListTab = ref<'total' | 'channel'>('channel')
|
||||
/** 渠道模式下接口返回的合计 vs 渠道业绩快照(切换 Tab 时用于对照卡片) */
|
||||
const orderDrawerSplitPerfCache = ref<{
|
||||
totalAmount: number
|
||||
totalOrderCount: number
|
||||
channelAmount: number
|
||||
channelOrderCount: number
|
||||
} | null>(null)
|
||||
|
||||
type OrderDrawerFilter =
|
||||
| { mode: 'dept'; deptId: number; deptName: string; start: string; end: string }
|
||||
@@ -1862,6 +1950,28 @@ const orderDrawerPageAmountSum = computed(() =>
|
||||
orderDrawerLists.value.reduce((s, row) => s + Number(row.amount ?? 0), 0)
|
||||
)
|
||||
|
||||
const orderDrawerChannelSplitActive = computed(
|
||||
() => Boolean(selectedChannel.value) && orderDrawerVisible.value
|
||||
)
|
||||
|
||||
const orderDrawerChannelLabel = computed(() => {
|
||||
const code = selectedChannel.value
|
||||
if (!code) return ''
|
||||
for (const g of channelGroups.value) {
|
||||
const c = g.channels?.find((x: ChannelOption) => x.channel_code === code)
|
||||
if (c?.channel_name) return String(c.channel_name)
|
||||
}
|
||||
return code
|
||||
})
|
||||
|
||||
const orderDrawerChannelStatAmount = computed(() => {
|
||||
const c = orderDrawerSplitPerfCache.value
|
||||
if (c && orderDrawerListTab.value === 'total') {
|
||||
return Number.isFinite(c.channelAmount) ? c.channelAmount : null
|
||||
}
|
||||
return orderDrawerTotalAmount.value
|
||||
})
|
||||
|
||||
// ── 目标进度卡片 ──────────────────────────────
|
||||
interface TargetProgressSection {
|
||||
dept_id: number
|
||||
@@ -2273,6 +2383,16 @@ function orderDrawerIndexMethod(index: number) {
|
||||
return (orderDrawerPage.value - 1) * orderDrawerPageSize.value + index + 1
|
||||
}
|
||||
|
||||
/** 挂号展示:预约日期 + 时段 + 时点 */
|
||||
function orderDrawerApptSchedule(a: Record<string, any>): string {
|
||||
const date = a?.appointment_date != null && String(a.appointment_date) !== '' ? String(a.appointment_date) : ''
|
||||
const period = a?.period != null && String(a.period) !== '' ? String(a.period) : ''
|
||||
const time = a?.appointment_time != null && String(a.appointment_time) !== '' ? String(a.appointment_time) : ''
|
||||
const tail = [period, time].filter(Boolean).join(' ')
|
||||
if (date && tail) return `${date} ${tail}`
|
||||
return date || tail || '—'
|
||||
}
|
||||
|
||||
function formatOrderDrawerTime(t: unknown): string {
|
||||
if (t == null || t === '') return '—'
|
||||
const n = Number(t)
|
||||
@@ -2316,10 +2436,32 @@ async function fetchOrderDrawerPage() {
|
||||
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
|
||||
}
|
||||
}
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerChannelSplitActive.value &&
|
||||
orderDrawerListTab.value === 'channel'
|
||||
) {
|
||||
params.channel_code = selectedChannel.value
|
||||
}
|
||||
const res: any = await prescriptionOrderLists(params)
|
||||
orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
|
||||
orderDrawerCount.value = Number(res?.count ?? 0)
|
||||
orderDrawerExtend.value = res?.extend && typeof res.extend === 'object' ? res.extend : null
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerListTab.value === 'channel' &&
|
||||
orderDrawerExtend.value &&
|
||||
orderDrawerExtend.value.stats_yeji_drawer_total_performance_amount != null
|
||||
) {
|
||||
orderDrawerSplitPerfCache.value = {
|
||||
totalAmount: Number(orderDrawerExtend.value.stats_yeji_drawer_total_performance_amount),
|
||||
totalOrderCount: Number(
|
||||
orderDrawerExtend.value.stats_yeji_drawer_total_performance_order_count ?? 0
|
||||
),
|
||||
channelAmount: Number(orderDrawerExtend.value.stats_order_amount_performance ?? 0),
|
||||
channelOrderCount: Number(res?.count ?? 0),
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
|
||||
orderDrawerLists.value = []
|
||||
@@ -2331,6 +2473,8 @@ async function fetchOrderDrawerPage() {
|
||||
}
|
||||
|
||||
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'dept',
|
||||
deptId: row.dept_id,
|
||||
@@ -2346,6 +2490,8 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
||||
}
|
||||
|
||||
function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['rows'][0], start: string, end: string) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'assistant',
|
||||
assistantId: row.admin_id,
|
||||
@@ -2366,6 +2512,8 @@ function openOrderDrawerByAssistantErCenterRevisit(
|
||||
end: string,
|
||||
revisitSlot: number
|
||||
) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'assistant',
|
||||
assistantId: assistant.admin_id,
|
||||
@@ -2414,6 +2562,11 @@ function onLeaderboardRevisitSlotClick(r: LeaderboardPack['leaderboards'][0]['ro
|
||||
)
|
||||
}
|
||||
|
||||
function onOrderDrawerListTabChange() {
|
||||
orderDrawerPage.value = 1
|
||||
void fetchOrderDrawerPage()
|
||||
}
|
||||
|
||||
function onOrderDrawerPageChange(p: number) {
|
||||
orderDrawerPage.value = p
|
||||
void fetchOrderDrawerPage()
|
||||
@@ -2600,7 +2753,7 @@ function onLeadLinesPageSizeChange(size: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单;「未归属中心」→ 按创建人拆解弹窗
|
||||
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单;「未归属中心」→ 按诊单医助拆解弹窗
|
||||
*/
|
||||
function onYejiDeptRowClick(tb: YejiTable, row: YejiRow) {
|
||||
if (isUnassignedYejiRow(row)) {
|
||||
@@ -2883,14 +3036,10 @@ function getDeptPerformancePieOption(tb: YejiTable) {
|
||||
}
|
||||
}
|
||||
|
||||
// 表头:「{渠道名}业绩」(未选渠道时该列被 v-if 隐藏,因为它和「合计业绩」同值)
|
||||
function buildCompletedColTitle(t: YejiTable): string {
|
||||
return t.channel_name ? `${t.channel_name}业绩` : '合计业绩'
|
||||
}
|
||||
|
||||
// 通用:「{渠道名}{后缀}」,仅选渠道时使用
|
||||
function buildChannelColTitle(t: YejiTable, suffix: string): string {
|
||||
return `${t.channel_name}${suffix}`
|
||||
// 选渠道时表头缩略(卡片标题已含渠道名,避免「自媒体1成交单」过长挤压)
|
||||
function yejiChannelMetricShort(kind: 'perf' | 'deal' | 'aov'): string {
|
||||
const m: Record<string, string> = { perf: '渠道业绩', deal: '渠道成交', aov: '渠道客单' }
|
||||
return m[kind] ?? kind
|
||||
}
|
||||
|
||||
// ── 格式化 ───────────────────────────────
|
||||
@@ -3074,12 +3223,26 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.yeji-table-tabs-head {
|
||||
padding: 14px 20px 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-ink);
|
||||
padding: 14px 20px 10px;
|
||||
background: var(--yj-surface-muted);
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-ink);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--yj-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
.yeji-tab-pane-stack {
|
||||
@@ -3470,8 +3633,8 @@ onMounted(async () => {
|
||||
|
||||
.tables-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(560px, 1fr));
|
||||
gap: 18px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.leaderboards-wrap {
|
||||
@@ -3686,6 +3849,13 @@ onMounted(async () => {
|
||||
border: 1px dashed var(--yj-line-strong);
|
||||
}
|
||||
|
||||
.yeji-th-label {
|
||||
display: inline-block;
|
||||
line-height: 1.25;
|
||||
vertical-align: middle;
|
||||
max-width: 6.5rem;
|
||||
}
|
||||
|
||||
.yeji-table {
|
||||
background: var(--yj-surface);
|
||||
border-radius: var(--yj-radius);
|
||||
@@ -3693,6 +3863,29 @@ onMounted(async () => {
|
||||
box-shadow: var(--yj-shadow-sm);
|
||||
border: 1px solid var(--yj-line);
|
||||
|
||||
&__scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-gutter: stable;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--yj-muted) 50%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--yj-surface-muted);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--yj-brand) 40%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
background: var(--yj-surface-muted);
|
||||
color: var(--yj-ink);
|
||||
@@ -3826,6 +4019,76 @@ onMounted(async () => {
|
||||
color: #b91c1c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.yeji-table__body--data {
|
||||
min-width: max(100%, 1180px);
|
||||
width: max-content;
|
||||
|
||||
thead th.col-dept,
|
||||
tbody td.col-dept {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
thead th.col-dept {
|
||||
z-index: 6;
|
||||
text-align: left;
|
||||
padding-left: 12px;
|
||||
padding-right: 10px;
|
||||
box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
tbody td.col-dept {
|
||||
box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.28);
|
||||
white-space: normal;
|
||||
max-width: 300px;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
padding-left: 12px;
|
||||
padding-right: 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.total-row td.col-dept {
|
||||
box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
thead th:not(.col-dept) {
|
||||
min-width: 88px;
|
||||
}
|
||||
thead th.col-narrow-head {
|
||||
min-width: 76px;
|
||||
max-width: 112px;
|
||||
padding: 8px 6px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
&.yeji-table__body--leaderboard {
|
||||
min-width: max(100%, 780px);
|
||||
width: max-content;
|
||||
|
||||
thead th.col-rank,
|
||||
tbody td.col-rank {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
thead th.col-rank {
|
||||
z-index: 6;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--yj-dept-col-head-start) 0%,
|
||||
var(--yj-dept-col-head-end) 100%
|
||||
) !important;
|
||||
box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.3);
|
||||
}
|
||||
tbody td.col-rank {
|
||||
background: var(--yj-surface);
|
||||
box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
tbody tr.yeji-row--action:hover td.col-rank {
|
||||
background: var(--yj-brand-soft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__foot {
|
||||
@@ -3904,6 +4167,27 @@ onMounted(async () => {
|
||||
&--muted {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&--total {
|
||||
border-color: rgba(217, 119, 6, 0.22);
|
||||
background: linear-gradient(180deg, #fffbeb 0%, var(--el-fill-color-blank) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.yeji-order-drawer__list-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.yeji-order-drawer__stat-label {
|
||||
@@ -3947,6 +4231,41 @@ onMounted(async () => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.yeji-order-appt-lines {
|
||||
line-height: 1.45;
|
||||
font-size: 12px;
|
||||
color: var(--yj-ink, #0f172a);
|
||||
}
|
||||
|
||||
.yeji-order-appt-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 4px 6px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.yeji-order-appt-k {
|
||||
flex: 0 0 auto;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--yj-muted, #64748b);
|
||||
}
|
||||
|
||||
.yeji-order-appt-sub {
|
||||
font-size: 11px;
|
||||
color: var(--yj-muted, #64748b);
|
||||
}
|
||||
|
||||
.yeji-order-appt-empty {
|
||||
font-size: 12px;
|
||||
color: var(--yj-muted, #94a3b8);
|
||||
}
|
||||
|
||||
.yeji-order-amt {
|
||||
color: #b91c1c;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -105,7 +105,7 @@ class YejiStatsController extends BaseAdminController
|
||||
return $this->data(YejiStatsLogic::assistantLeaderboards($params, $this->adminId, $this->adminInfo));
|
||||
}
|
||||
|
||||
/** 「未归属中心」行:按订单创建人拆解业绩(与「合计业绩」未归属部分同口径) */
|
||||
/** 「未归属中心」行:按诊单医助拆解业绩(与表格补差、合计业绩诊单医助归属一致) */
|
||||
public function unassignedBreakdown()
|
||||
{
|
||||
@set_time_limit(120);
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace app\adminapi\lists\tcm;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||||
use app\common\lists\ListsExtendInterface;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
@@ -101,11 +102,12 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
$this->applySupplyModeFilter($query);
|
||||
$this->applyCreatorOrOwnPrescriptionVisibility($query);
|
||||
$this->applyDataScopeForPrescriptionOrder($query);
|
||||
// 业绩看板侧栏等:不展示履约已取消(4),与 stats 业绩统计口径一致
|
||||
// 业绩看板侧栏等:不展示履约已取消(4),与看板合计业绩 NULL 安全口径一致
|
||||
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
|
||||
$query->where('fulfillment_status', '<>', 4);
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
|
||||
}
|
||||
$this->applyYejiErCenterRevisitOrderFilter($query);
|
||||
YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,8 +283,10 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
|
||||
/**
|
||||
* 统计用查询骨架:不含 create_time 区间;其它筛选项 + 统计专用数据域(不重复 where)
|
||||
*
|
||||
* @param bool $applyYejiDrawerChannel 是否应用业绩侧栏渠道收窄(合计业绩统计须传 false)
|
||||
*/
|
||||
private function buildBaseQueryForStats(): Query
|
||||
private function buildBaseQueryForStats(bool $applyYejiDrawerChannel = true): Query
|
||||
{
|
||||
$query = PrescriptionOrder::where($this->searchWhereWithoutCreateTimeRange())->whereNull('delete_time');
|
||||
$this->applyDoctorAssistantFilters($query);
|
||||
@@ -302,9 +306,12 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
$this->applyDataScopeForPrescriptionOrder($query);
|
||||
}
|
||||
}
|
||||
// 与列表 applyPermissionAndExtraFilters 一致:业绩侧栏传 exclude 时不统计已取消行
|
||||
// 与看板「合计业绩」一致:NULL 安全剔除履约已取消(4),勿用 `<>` 以免误丢 NULL 状态行
|
||||
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
|
||||
$query->where('fulfillment_status', '<>', 4);
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
|
||||
}
|
||||
if ($applyYejiDrawerChannel) {
|
||||
YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params);
|
||||
}
|
||||
|
||||
return $query;
|
||||
@@ -461,6 +468,10 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
}
|
||||
unset($item);
|
||||
|
||||
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
|
||||
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists);
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
@@ -495,6 +506,11 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
if ($yejiConsult !== null) {
|
||||
$base['stats_yeji_consult_count'] = $yejiConsult;
|
||||
}
|
||||
$yejiDrawerTotal = $this->computeYejiDrawerPerformanceTotalForExtend();
|
||||
if ($yejiDrawerTotal !== null) {
|
||||
$base['stats_yeji_drawer_total_performance_amount'] = $yejiDrawerTotal['amount'];
|
||||
$base['stats_yeji_drawer_total_performance_order_count'] = $yejiDrawerTotal['order_count'];
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
@@ -570,6 +586,37 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
return [date('Y-m-d', $t0), date('Y-m-d', $t1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏已选渠道时:「合计业绩」金额与订单数(与看板列同口径,不限渠道收窄)
|
||||
*
|
||||
* @return array{amount: float, order_count: int}|null
|
||||
*/
|
||||
private function computeYejiDrawerPerformanceTotalForExtend(): ?array
|
||||
{
|
||||
if ((int) ($this->params['yeji_order_drawer'] ?? 0) !== 1) {
|
||||
return null;
|
||||
}
|
||||
if (trim((string) ($this->params['channel_code'] ?? '')) === '') {
|
||||
return null;
|
||||
}
|
||||
[$t0, $t1] = $this->resolveStatsTimeWindow();
|
||||
if ($t0 <= 0 || $t1 <= 0) {
|
||||
return null;
|
||||
}
|
||||
if ($t1 < $t0) {
|
||||
$tmp = $t0;
|
||||
$t0 = $t1;
|
||||
$t1 = $tmp;
|
||||
}
|
||||
$q = $this->buildBaseQueryForStats(false)->whereBetween('create_time', [$t0, $t1]);
|
||||
// 全量「合计业绩」与看板列同口径:始终剔除履约已取消(4),不依赖 exclude 参数
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
|
||||
$amount = round((float) (clone $q)->sum('amount'), 2);
|
||||
$count = (int) (clone $q)->count();
|
||||
|
||||
return ['amount' => $amount, 'order_count' => $count];
|
||||
}
|
||||
|
||||
/**
|
||||
* 重点看板计数(基于“当前筛选(去除审核/履约状态)”后的全量命中)
|
||||
*
|
||||
|
||||
@@ -243,7 +243,8 @@ class AppointmentLogic extends BaseLogic
|
||||
'appointment_time' => $appointmentTime,
|
||||
'appointment_type' => $params['appointment_type'] ?? 'video',
|
||||
'remark' => $params['remark'] ?? '',
|
||||
'channels' => $params['channel_source'] ?? '',
|
||||
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
|
||||
'channel_source' => trim((string) ($params['channel_source'] ?? '')),
|
||||
'status' => 1,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
|
||||
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\logic\stats;
|
||||
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\db\Query;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
@@ -19,7 +21,7 @@ use think\facade\Db;
|
||||
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按诊单医助在组织树内归属 rollup(含下级),与 dept 模块 ErCenter 复诊算法一致
|
||||
* - 接诊诊单 = 已完成挂号 status=3,**与 admin 挂号列表条数同口径**(appointment_date、诊单未软删),不限医助角色、不要求 assistant_id 非 0;
|
||||
* 部门归属:优先 COALESCE(挂号.assistant_id, 诊单.assistant_id) 映射到展示「中心」,否则回退接诊医生 doctor_id;仍无法归属则计入「未归属中心」行。
|
||||
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4;NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag/creator 两套口径均按订单计,非挂号条数。
|
||||
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**(仅剔除 fulfillment_status=4;NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(tag 时按诊单医助收窄),非挂号条数。
|
||||
* - 合计业绩 = 区间内 zyt_tcm_prescription_order.amount 求和(排除已取消(4) 与软删),按 dg.assistant_id 归属到「中心」展示部门;
|
||||
* 无法归属到任一展示部门的金额单列「未归属中心」行;合计行 = 与列表同条件的全表 SUM(不限部门)
|
||||
* · 时间窗与业绩条件与列表 stats_order_amount_performance 一致;列表若另有患者/单号等筛选或账号数据域收窄,金额可能仍不同
|
||||
@@ -311,7 +313,7 @@ class YejiStatsLogic
|
||||
);
|
||||
$performance = $perfBoth['all'];
|
||||
$channelPerformance = $perfBoth['scoped'];
|
||||
// 成交单数(全量):与合计业绩同 WHERE,按 creator_id 归属部门,合计用全表 COUNT
|
||||
// 成交单数(全量):与合计业绩同 WHERE,按诊单医助归属部门,合计用全表 COUNT
|
||||
$dealOrderByDept = self::aggregateDealOrderCount($startTs, $endTs, $adminToPrimary, null, null);
|
||||
$globalDealOrderCount = self::countGlobalDealOrders($startTs, $endTs);
|
||||
|
||||
@@ -485,7 +487,7 @@ class YejiStatsLogic
|
||||
. ($dataScopeRestricted
|
||||
? ' 当前账号受角色「数据范围」限制,仅统计可见成员及其归属展示中心;合计与表格一致,不含权限范围外数据。'
|
||||
: '');
|
||||
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入(与订单列表 stats_order_amount_performance 一致)。无法归入「中心」展示部门的创建人业绩计入「未归属中心」行;合计=全表 SUM。**投放成本**按进线占比分摊至各部门。';
|
||||
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入(与订单列表 stats_order_amount_performance 一致)。无法归入「中心」展示部门的诊单医助业绩(及无诊单医助的订单)计入「未归属中心」行;合计=全表 SUM。**投放成本**按进线占比分摊至各部门。';
|
||||
|
||||
return [
|
||||
'start_date' => $startDate,
|
||||
@@ -1440,7 +1442,7 @@ class YejiStatsLogic
|
||||
/**
|
||||
* 接诊诊单聚合:返回两组按部门归属的接诊计数。
|
||||
* - all: 区间内 status=3 挂号条数按部门归属(与列表计数一致),不含「仅 role_id=2」等额外限制。
|
||||
* - scoped: 选定渠道时「{渠道}成交单」= **非取消业务订单条数**(与 {渠道}业绩同源):有 dict(channels) 走诊单医助+EXISTS 口径;否则走 tag(diagnosis_id IN / creator_id IN)与 aggregatePerformance 完全一致。
|
||||
* - scoped: 选定渠道时「{渠道}成交单」= **非取消业务订单条数**(与 {渠道}业绩同源):有 dict(channels) 走诊单医助+EXISTS 口径;否则走 tag(diagnosis_id IN / **诊单医助** IN)与 aggregatePerformance 完全一致。
|
||||
* 未选渠道时 scoped == all(不再额外查一次)。
|
||||
*
|
||||
* @param int[] $appointmentChannelValues dict_data value 整型列表(通常 1=自媒体1)
|
||||
@@ -1483,7 +1485,7 @@ class YejiStatsLogic
|
||||
);
|
||||
}
|
||||
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
|
||||
$scopedByDept = self::aggregateDealOrderCountByTagScope(
|
||||
$scopedByDept = self::aggregateDealOrderCount(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$adminToPrimary,
|
||||
@@ -2327,13 +2329,26 @@ class YejiStatsLogic
|
||||
|
||||
/**
|
||||
* 业绩/成交单统计:仅排除履约已取消 fulfillment_status = 4,其余(含 NULL 与任意其它状态)全部计入。
|
||||
* 与处方订单列表 stats_order_amount_performance(总业绩 − 已取消业绩)一致;避免 SQL 中 `<> 4` 把 NULL 行误排除。
|
||||
* 与看板「合计业绩」、处方列表业绩 extend 口径一致;勿用 `<> 4`,以免误排除 NULL。
|
||||
*
|
||||
* @param \think\db\Query $query
|
||||
* @param \think\db\BaseQuery|\think\Model $query Db::name / Model::where 等查询实例
|
||||
* @param string $tableAlias 主表别名(如 Db 侧 `alias('o')` 传 `o`);无别名时传空字符串
|
||||
*/
|
||||
public static function applyPrescriptionOrderNotCancelledForPerformanceQuery($query, string $tableAlias = ''): void
|
||||
{
|
||||
if ($tableAlias !== '') {
|
||||
$query->whereRaw("NOT ({$tableAlias}.fulfillment_status <=> ?)", [4]);
|
||||
} else {
|
||||
$query->whereRaw('NOT (fulfillment_status <=> ?)', [4]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \think\db\BaseQuery|\think\Model $query
|
||||
*/
|
||||
private static function applyPrescriptionOrderNotCancelledForPerformance($query, string $tableAlias = 'o'): void
|
||||
{
|
||||
$query->whereRaw("NOT ({$tableAlias}.fulfillment_status <=> ?)", [4]);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, $tableAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2365,16 +2380,20 @@ class YejiStatsLogic
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return 0.0;
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->whereNull('o.delete_time')
|
||||
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||
->where('o.create_time', 'between', [$startTs, $endTs])
|
||||
->where('o.diagnosis_id', '>', 0)
|
||||
->where('dg.assistant_id', '>', 0);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$query->whereIn('o.creator_id', array_keys($tagAssistantIds));
|
||||
$query->whereIn('dg.assistant_id', array_keys($tagAssistantIds));
|
||||
}
|
||||
|
||||
return round((float) $query->sum('o.amount'), 1);
|
||||
@@ -2630,62 +2649,10 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签口径下的「{渠道}成交单」计数:与 aggregatePerformance 同一套 WHERE(仅剔除已取消=4,含 NULL),
|
||||
* 按 o.creator_id 分组 COUNT 后映射到 primary 部门,用于 tag 路径的「成交单」列展示。
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private static function aggregateDealOrderCountByTagScope(
|
||||
int $startTs,
|
||||
int $endTs,
|
||||
array $adminToPrimary,
|
||||
?array $tagDiagIds,
|
||||
?array $tagAssistantIds
|
||||
): array {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
return [];
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return [];
|
||||
}
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->whereNull('o.delete_time')
|
||||
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
$query->field(['o.creator_id', 'COUNT(o.id) AS cnt'])
|
||||
->group('o.creator_id');
|
||||
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$query->whereIn('o.creator_id', array_keys($tagAssistantIds));
|
||||
}
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
$byDept = [];
|
||||
foreach ($rows as $r) {
|
||||
$cid = (int) ($r['creator_id'] ?? 0);
|
||||
if ($cid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$primary = (int) ($adminToPrimary[$cid] ?? 0);
|
||||
if ($primary <= 0) {
|
||||
continue;
|
||||
}
|
||||
$byDept[$primary] = ($byDept[$primary] ?? 0) + (int) $r['cnt'];
|
||||
}
|
||||
|
||||
return $byDept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次业绩聚合查询。WHERE: delete_time IS NULL AND NOT(fulfillment <=> 4) AND create_time BETWEEN ...
|
||||
* 可选 diagnosis_id IN tagDiagIds、creator_id IN tagAssistantIds。
|
||||
* 单次业绩聚合:与 aggregatePerformanceAmountsByAdmin / aggregateDealOrderCount 同源 ——
|
||||
* INNER 诊单,按 dg.assistant_id SUM(amount) 后映射到展示「中心」(与 PrescriptionOrderLists assistant_dept 一致)。
|
||||
* WHERE: delete_time IS NULL、NOT(fulfillment <=> 4)、create_time 区间、diagnosis_id>0、dg.assistant_id>0;
|
||||
* 可选 tag:diagnosis_id IN、dg.assistant_id IN。
|
||||
*
|
||||
* @return array<int, float>
|
||||
*/
|
||||
@@ -2697,20 +2664,23 @@ class YejiStatsLogic
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return [];
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->whereNull('o.delete_time')
|
||||
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||
->where('o.create_time', 'between', [$startTs, $endTs])
|
||||
->where('o.diagnosis_id', '>', 0)
|
||||
->where('dg.assistant_id', '>', 0);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
$query->field(['o.creator_id', 'SUM(o.amount) AS amt'])
|
||||
->group('o.creator_id');
|
||||
$query->field(['dg.assistant_id', 'SUM(o.amount) AS amt'])
|
||||
->group('dg.assistant_id');
|
||||
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
// 兜底口径:限定订单创建人为「跟进过该标签客户的医助」
|
||||
$query->whereIn('o.creator_id', array_keys($tagAssistantIds));
|
||||
$query->whereIn('dg.assistant_id', array_keys($tagAssistantIds));
|
||||
}
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
@@ -2719,11 +2689,11 @@ class YejiStatsLogic
|
||||
}
|
||||
$byDept = [];
|
||||
foreach ($rows as $r) {
|
||||
$cid = (int) ($r['creator_id'] ?? 0);
|
||||
if ($cid <= 0) {
|
||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$primary = (int) ($adminToPrimary[$cid] ?? 0);
|
||||
$primary = (int) ($adminToPrimary[$aid] ?? 0);
|
||||
if ($primary <= 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -3059,7 +3029,7 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 「未归属中心」合计业绩按订单创建人拆解(与 aggregatePerformance /「合计业绩」列归属一致:仅 null,null 标签口径,不按渠道收窄)。
|
||||
* 「未归属中心」业绩下钻:按诊单医助汇总无法映射到展示「中心」的金额(与 aggregatePerformance 部门行互补)。
|
||||
*
|
||||
* @return array{
|
||||
* start_date: string,
|
||||
@@ -3078,18 +3048,64 @@ class YejiStatsLogic
|
||||
$startTs = (int) $c['startTs'];
|
||||
$endTs = (int) $c['endTs'];
|
||||
$adminToPrimary = $c['adminToPrimary'];
|
||||
$visibleCreatorIds = null;
|
||||
if (!empty($c['dataScopeRestricted'])) {
|
||||
$visibleCreatorIds = $c['dataScopeVisibleAdminIds'] ?? [];
|
||||
|
||||
$byAdminAmt = self::aggregatePerformanceAmountsByAdmin($startTs, $endTs, null, null);
|
||||
$byAdminCnt = self::aggregateDealOrderCountGroupedByAssistant($startTs, $endTs, null, null);
|
||||
|
||||
$visFlip = null;
|
||||
if (!empty($c['dataScopeRestricted']) && isset($c['dataScopeVisibleAdminIds'])) {
|
||||
$visFlip = array_flip(array_values(array_unique(array_filter(
|
||||
array_map('intval', $c['dataScopeVisibleAdminIds']),
|
||||
static function (int $v): bool {
|
||||
return $v > 0;
|
||||
}
|
||||
))));
|
||||
}
|
||||
$rows = self::aggregateUnassignedByCreator(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$adminToPrimary,
|
||||
null,
|
||||
null,
|
||||
$visibleCreatorIds
|
||||
);
|
||||
|
||||
$rows = [];
|
||||
foreach ($byAdminAmt as $aid => $amt) {
|
||||
$aid = (int) $aid;
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($visFlip !== null && !isset($visFlip[$aid])) {
|
||||
continue;
|
||||
}
|
||||
if ((int) ($adminToPrimary[$aid] ?? 0) > 0) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = [
|
||||
'admin_id' => $aid,
|
||||
'name' => '',
|
||||
'order_count' => (int) ($byAdminCnt[$aid] ?? 0),
|
||||
'amount' => round((float) $amt, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$globalAmt = self::sumGlobalPerformanceAmount($startTs, $endTs);
|
||||
$globalCnt = self::countGlobalDealOrders($startTs, $endTs);
|
||||
$innerAmt = array_sum($byAdminAmt);
|
||||
$innerCnt = array_sum($byAdminCnt);
|
||||
$orphanAmt = round(max(0.0, $globalAmt - $innerAmt), 2);
|
||||
$orphanCnt = max(0, $globalCnt - $innerCnt);
|
||||
if ($orphanAmt >= 0.01 || $orphanCnt > 0) {
|
||||
$rows[] = [
|
||||
'admin_id' => 0,
|
||||
'name' => '',
|
||||
'order_count' => $orphanCnt,
|
||||
'amount' => $orphanAmt,
|
||||
];
|
||||
}
|
||||
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
$c = $b['amount'] <=> $a['amount'];
|
||||
if ($c !== 0) {
|
||||
return $c;
|
||||
}
|
||||
|
||||
return $b['order_count'] <=> $a['order_count'];
|
||||
});
|
||||
|
||||
$ids = [];
|
||||
foreach ($rows as $row) {
|
||||
if ($row['admin_id'] > 0) {
|
||||
@@ -3100,7 +3116,7 @@ class YejiStatsLogic
|
||||
foreach ($rows as &$row) {
|
||||
$aid = (int) $row['admin_id'];
|
||||
if ($aid <= 0) {
|
||||
$row['name'] = '(无创建人/0)';
|
||||
$row['name'] = '(无诊单或医助未填)';
|
||||
} else {
|
||||
$row['name'] = (string) ($names[$aid] ?? ('#' . $aid));
|
||||
}
|
||||
@@ -3112,80 +3128,10 @@ class YejiStatsLogic
|
||||
'end_date' => (string) $c['endDate'],
|
||||
'channel_code' => (string) $c['channelCode'],
|
||||
'rows' => $rows,
|
||||
'note' => '与看板「合计业绩」列口径一致:按订单创建时间、剔除履约已取消(4);创建人无法映射到任一展示「中心」部门的业绩计入未归属。账号数据权限下仅列出可见创建人相关订单。',
|
||||
'note' => '与看板「未归属中心」补差一致:按订单 create_time、剔除履约已取消(4);诊单医助无法映射到任一展示「中心」计入各行;其余(无诊单/医助为空)汇总在首行。账号数据权限下仅列出可见医助。',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|null $visibleCreatorIds 数据权限:仅统计这些创建人(null=不额外限制);须含 creator_id=0 时用 OR 条件已并入查询
|
||||
*
|
||||
* @return list<array{admin_id:int, name:string, order_count:int, amount:float}>
|
||||
*/
|
||||
private static function aggregateUnassignedByCreator(
|
||||
int $startTs,
|
||||
int $endTs,
|
||||
array $adminToPrimary,
|
||||
?array $tagDiagIds,
|
||||
?array $tagAssistantIds,
|
||||
?array $visibleCreatorIds
|
||||
): array {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
return [];
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return [];
|
||||
}
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->whereNull('o.delete_time')
|
||||
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$query->whereIn('o.creator_id', array_keys($tagAssistantIds));
|
||||
}
|
||||
if ($visibleCreatorIds !== null) {
|
||||
$visibleCreatorIds = array_values(array_unique(array_filter(array_map('intval', $visibleCreatorIds), static fn (int $v): bool => $v > 0)));
|
||||
if ($visibleCreatorIds === []) {
|
||||
return [];
|
||||
}
|
||||
$ph = implode(',', array_fill(0, count($visibleCreatorIds), '?'));
|
||||
$query->whereRaw("(o.creator_id = 0 OR o.creator_id IN ({$ph}))", $visibleCreatorIds);
|
||||
}
|
||||
$query->field(['o.creator_id', 'COUNT(*) AS order_count', 'SUM(o.amount) AS amount'])
|
||||
->group('o.creator_id');
|
||||
$dbRows = $query->select()->toArray();
|
||||
if ($dbRows === []) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($dbRows as $r) {
|
||||
$cid = (int) ($r['creator_id'] ?? 0);
|
||||
$primary = (int) ($adminToPrimary[$cid] ?? 0);
|
||||
if ($primary > 0) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'admin_id' => $cid,
|
||||
'name' => '',
|
||||
'order_count' => (int) ($r['order_count'] ?? 0),
|
||||
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
||||
];
|
||||
}
|
||||
usort($out, static function (array $a, array $b): int {
|
||||
$c = $b['amount'] <=> $a['amount'];
|
||||
if ($c !== 0) {
|
||||
return $c;
|
||||
}
|
||||
|
||||
return $b['order_count'] <=> $a['order_count'];
|
||||
});
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
*
|
||||
@@ -3219,4 +3165,156 @@ class YejiStatsLogic
|
||||
|
||||
return $prefix . $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂号字典 channels:与渠道配置名称匹配的 value 字符串列表(用于 ap.channel_source)
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private static function appointmentChannelDictStringValuesForDrawer(?array $channelInfo): array
|
||||
{
|
||||
if ($channelInfo === null) {
|
||||
return [];
|
||||
}
|
||||
$names = array_values(array_unique(array_filter([
|
||||
trim((string) ($channelInfo['channel_name'] ?? '')),
|
||||
trim((string) ($channelInfo['source_tag_name'] ?? '')),
|
||||
])));
|
||||
if ($names === []) {
|
||||
return [];
|
||||
}
|
||||
$vals = Db::name('dict_data')->where('type_value', 'channels')->whereIn('name', $names)->column('value');
|
||||
$out = [];
|
||||
foreach ($vals as $v) {
|
||||
$s = trim((string) $v);
|
||||
if ($s !== '') {
|
||||
$out[$s] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($out);
|
||||
}
|
||||
|
||||
/** @var bool|null 进程内缓存:doctor_appointment 是否已加 channel_source 列 */
|
||||
private static ?bool $doctorAppointmentHasChannelSourceColumn = null;
|
||||
|
||||
private static function doctorAppointmentHasChannelSourceColumn(): bool
|
||||
{
|
||||
if (self::$doctorAppointmentHasChannelSourceColumn !== null) {
|
||||
return self::$doctorAppointmentHasChannelSourceColumn;
|
||||
}
|
||||
try {
|
||||
$fields = Db::name('doctor_appointment')->getTableFields();
|
||||
self::$doctorAppointmentHasChannelSourceColumn = is_array($fields)
|
||||
&& in_array('channel_source', $fields, true);
|
||||
} catch (\Throwable) {
|
||||
self::$doctorAppointmentHasChannelSourceColumn = false;
|
||||
}
|
||||
|
||||
return self::$doctorAppointmentHasChannelSourceColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source)
|
||||
*
|
||||
* @param Query $query PrescriptionOrder 主表查询
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function applyYejiDrawerChannelFilterToPrescriptionOrderQuery(Query $query, array $params): void
|
||||
{
|
||||
if ((int) ($params['yeji_order_drawer'] ?? 0) !== 1) {
|
||||
return;
|
||||
}
|
||||
$channelCode = trim((string) ($params['channel_code'] ?? ''));
|
||||
if ($channelCode === '') {
|
||||
return;
|
||||
}
|
||||
$startDate = '';
|
||||
$endDate = '';
|
||||
$st = trim((string) ($params['start_time'] ?? ''));
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $st, $m)) {
|
||||
$startDate = $m[1];
|
||||
}
|
||||
$et = trim((string) ($params['end_time'] ?? ''));
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $et, $m)) {
|
||||
$endDate = $m[1];
|
||||
}
|
||||
if ($startDate === '' || $endDate === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$ctx = self::resolveYejiContext([
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'channel_code' => $channelCode,
|
||||
'dept_ids' => null,
|
||||
]);
|
||||
|
||||
if (!$ctx['channelFilterActive'] || $ctx['channelInfo'] === null) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$poTbl = (new PrescriptionOrder())->getTable();
|
||||
$apTable = self::tableWithPrefix('doctor_appointment');
|
||||
$adminRoleTable = self::tableWithPrefix('admin_role');
|
||||
|
||||
$norm = self::normalizeAppointmentChannelValues($ctx['appointmentChannelValues']);
|
||||
$srcStrings = self::appointmentChannelDictStringValuesForDrawer($ctx['channelInfo']);
|
||||
|
||||
if ($norm !== [] || $srcStrings !== []) {
|
||||
$subParts = [];
|
||||
$subBind = [];
|
||||
if ($norm !== []) {
|
||||
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $norm)));
|
||||
$ph = implode(',', array_fill(0, count($strVals), '?'));
|
||||
$subParts[] = "ap.channels IN ({$ph})";
|
||||
$subBind = array_merge($subBind, $strVals);
|
||||
}
|
||||
if ($srcStrings !== [] && self::doctorAppointmentHasChannelSourceColumn()) {
|
||||
$ph = implode(',', array_fill(0, count($srcStrings), '?'));
|
||||
$subParts[] = "ap.channel_source IN ({$ph})";
|
||||
$subBind = array_merge($subBind, $srcStrings);
|
||||
}
|
||||
if ($subParts !== []) {
|
||||
$innerOr = implode(' OR ', $subParts);
|
||||
$existsSql = "EXISTS (SELECT 1 FROM {$apTable} ap INNER JOIN {$adminRoleTable} ar "
|
||||
. "ON ar.admin_id = ap.assistant_id AND ar.role_id = 2 WHERE ap.patient_id = `{$poTbl}`.`diagnosis_id` "
|
||||
. "AND ap.status = 3 AND ({$innerOr}))";
|
||||
$query->whereRaw($existsSql, $subBind);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$tagDiagIds = $ctx['tagDiagIds'];
|
||||
$tagAssistantIds = $ctx['tagAssistantIds'];
|
||||
$tagFallback = $ctx['tagFallback'];
|
||||
$tagFilterId = (string) ($ctx['tagFilterId'] ?? '');
|
||||
if ($tagFilterId === '') {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === [] && $tagFallback) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('diagnosis_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagFallback && $tagAssistantIds !== null && $tagAssistantIds !== []) {
|
||||
$query->whereIn('creator_id', array_map('intval', array_keys($tagAssistantIds)));
|
||||
}
|
||||
if ($tagDiagIds === null && !($tagFallback && $tagAssistantIds !== null && $tagAssistantIds !== [])) {
|
||||
$query->whereRaw('0 = 1');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,25 +891,31 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂号预约简要信息(业务订单详情展示)
|
||||
* 渠道字典 value => 名称(与挂号 channel_source 存值一致)
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function buildAppointmentBrief(int $appointmentId): ?array
|
||||
private static function channelSourceDictMap(): array
|
||||
{
|
||||
if ($appointmentId <= 0) {
|
||||
return null;
|
||||
}
|
||||
$ap = Appointment::find($appointmentId);
|
||||
if ($ap === null) {
|
||||
return null;
|
||||
}
|
||||
$row = $ap->toArray();
|
||||
$doctorName = '';
|
||||
$did = (int) ($row['doctor_id'] ?? 0);
|
||||
if ($did > 0) {
|
||||
$doctorName = (string) (Admin::where('id', $did)->value('name') ?? '');
|
||||
try {
|
||||
/** @var array<string, string> $m */
|
||||
$m = Db::name('dict_data')->where('type_value', 'channels')->column('name', 'value');
|
||||
|
||||
return $m;
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row doctor_appointment 行
|
||||
* @param array<string, string> $channelDict
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function formatAppointmentBriefFromRow(array $row, string $doctorName, array $channelDict = []): array
|
||||
{
|
||||
$apptId = (int) ($row['id'] ?? 0);
|
||||
$statusMap = [
|
||||
1 => '已预约',
|
||||
2 => '已取消',
|
||||
@@ -929,9 +935,28 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
$status = (int) ($row['status'] ?? 0);
|
||||
$atype = (string) ($row['appointment_type'] ?? '');
|
||||
$chSrc = trim((string) ($row['channel_source'] ?? ''));
|
||||
if ($chSrc === '') {
|
||||
$legacy = $row['channels'] ?? null;
|
||||
if ($legacy !== null && $legacy !== '') {
|
||||
$chSrc = is_numeric($legacy)
|
||||
? (string) (int) $legacy
|
||||
: trim((string) $legacy);
|
||||
}
|
||||
}
|
||||
$chDesc = '';
|
||||
if ($chSrc !== '') {
|
||||
$chDesc = (string) ($channelDict[$chSrc] ?? '');
|
||||
if ($chDesc === '' && is_numeric($chSrc)) {
|
||||
$chDesc = (string) ($channelDict[(string) (int) $chSrc] ?? '');
|
||||
}
|
||||
if ($chDesc === '') {
|
||||
$chDesc = $chSrc;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $appointmentId,
|
||||
'id' => $apptId,
|
||||
'appointment_date' => (string) ($row['appointment_date'] ?? ''),
|
||||
'period' => $period,
|
||||
'appointment_time' => $timePart,
|
||||
@@ -940,11 +965,144 @@ class PrescriptionOrderLogic
|
||||
'status' => $status,
|
||||
'status_desc' => $statusMap[$status] ?? '未知',
|
||||
'doctor_name' => $doctorName,
|
||||
'channel_source' => (string) ($row['channel_source'] ?? ''),
|
||||
'channel_source' => $chSrc,
|
||||
'channel_source_desc' => $chDesc,
|
||||
'remark' => (string) ($row['remark'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂号预约简要信息(业务订单详情展示)
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private static function buildAppointmentBrief(int $appointmentId): ?array
|
||||
{
|
||||
if ($appointmentId <= 0) {
|
||||
return null;
|
||||
}
|
||||
$ap = Appointment::find($appointmentId);
|
||||
if ($ap === null) {
|
||||
return null;
|
||||
}
|
||||
$row = $ap->toArray();
|
||||
$doctorName = '';
|
||||
$did = (int) ($row['doctor_id'] ?? 0);
|
||||
if ($did > 0) {
|
||||
$doctorName = (string) (Admin::where('id', $did)->value('name') ?? '');
|
||||
}
|
||||
$channelDict = self::channelSourceDictMap();
|
||||
|
||||
return self::formatAppointmentBriefFromRow($row, $doctorName, $channelDict);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约)
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $lists
|
||||
*/
|
||||
public static function appendLinkedAppointmentsToListRows(array &$lists): void
|
||||
{
|
||||
if ($lists === []) {
|
||||
return;
|
||||
}
|
||||
foreach ($lists as &$item) {
|
||||
$item['linked_appointment'] = null;
|
||||
}
|
||||
unset($item);
|
||||
|
||||
$rxIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
|
||||
$lists
|
||||
))));
|
||||
$rxApptByRx = [];
|
||||
if ($rxIds !== []) {
|
||||
$rxApptByRx = Prescription::whereIn('id', $rxIds)->whereNull('delete_time')->column('appointment_id', 'id');
|
||||
}
|
||||
|
||||
$diagNeedFallback = [];
|
||||
foreach ($lists as $item) {
|
||||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||||
$dgId = (int) ($item['diagnosis_id'] ?? 0);
|
||||
$apFromRx = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0;
|
||||
if ($apFromRx <= 0 && $dgId > 0) {
|
||||
$diagNeedFallback[$dgId] = true;
|
||||
}
|
||||
}
|
||||
$diagIds = array_keys($diagNeedFallback);
|
||||
$latestApptByDiag = [];
|
||||
if ($diagIds !== []) {
|
||||
try {
|
||||
$agg = Db::name('doctor_appointment')
|
||||
->whereIn('patient_id', $diagIds)
|
||||
->fieldRaw('patient_id, MAX(id) as max_id')
|
||||
->group('patient_id')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($agg as $r) {
|
||||
$pid = (int) ($r['patient_id'] ?? 0);
|
||||
$mid = (int) ($r['max_id'] ?? 0);
|
||||
if ($pid > 0 && $mid > 0) {
|
||||
$latestApptByDiag[$pid] = $mid;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$latestApptByDiag = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<int, array{0: int, 1: int}> $resolvedPairs index => [ resolvedApptId, rxAppointmentId ] */
|
||||
$resolvedPairs = [];
|
||||
foreach ($lists as $idx => $item) {
|
||||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||||
$dgId = (int) ($item['diagnosis_id'] ?? 0);
|
||||
$rxApptId = $rxId > 0 ? (int) ($rxApptByRx[$rxId] ?? 0) : 0;
|
||||
$resolved = $rxApptId;
|
||||
if ($resolved <= 0 && $dgId > 0) {
|
||||
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
|
||||
}
|
||||
if ($resolved > 0) {
|
||||
$resolvedPairs[$idx] = [$resolved, $rxApptId];
|
||||
}
|
||||
}
|
||||
if ($resolvedPairs === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allApptIds = array_values(array_unique(array_map(static fn ($p) => $p[0], $resolvedPairs)));
|
||||
$appts = Appointment::whereIn('id', $allApptIds)->select()->toArray();
|
||||
$apptById = [];
|
||||
foreach ($appts as $a) {
|
||||
$aid = (int) ($a['id'] ?? 0);
|
||||
if ($aid > 0) {
|
||||
$apptById[$aid] = $a;
|
||||
}
|
||||
}
|
||||
$doctorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($a) => (int) ($a['doctor_id'] ?? 0),
|
||||
$appts
|
||||
))));
|
||||
$doctorNames = [];
|
||||
if ($doctorIds !== []) {
|
||||
$doctorNames = Admin::whereIn('id', $doctorIds)->column('name', 'id');
|
||||
}
|
||||
$channelDict = self::channelSourceDictMap();
|
||||
|
||||
foreach ($resolvedPairs as $idx => $pair) {
|
||||
$resolvedId = $pair[0];
|
||||
$rxApptId = $pair[1];
|
||||
$row = $apptById[$resolvedId] ?? null;
|
||||
if ($row === null || !is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$did = (int) ($row['doctor_id'] ?? 0);
|
||||
$dname = $did > 0 ? (string) ($doctorNames[$did] ?? '') : '';
|
||||
$brief = self::formatAppointmentBriefFromRow($row, $dname, $channelDict);
|
||||
$brief['resolved_from'] = $rxApptId === $resolvedId ? 'prescription' : 'diagnosis';
|
||||
$lists[$idx]['linked_appointment'] = $brief;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 物流轨迹(顺丰/京东等,优先快递100;未配置密钥时返回官网链接)
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user