更新
This commit is contained in:
+375
-218
@@ -317,75 +317,7 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
||||||
<p class="yeji-zyyt-intro">
|
<!-- ── 业绩表(4 张/N 张);目标看板见「目标看板(卡片)」Tab ── -->
|
||||||
各中心部门诊金、业绩、进线与 ROI 等,与顶部渠道、展示部门、日期筛选联动。本月 / 本周 / 今日等多张业绩表中「预约诊单」列数字可点开查看每条挂号记录。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- <el-alert
|
|
||||||
v-if="channelFilterNote"
|
|
||||||
:title="channelFilterNote"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="filter-alert"
|
|
||||||
/> -->
|
|
||||||
<!-- ── 目标进度卡片 ── -->
|
|
||||||
<div
|
|
||||||
v-if="targetProgressCard || targetProgressLoading"
|
|
||||||
v-loading="targetProgressLoading"
|
|
||||||
class="tp-wrap"
|
|
||||||
>
|
|
||||||
<div v-if="targetProgressCard" class="tp-card">
|
|
||||||
<div class="tp-card__title">{{ targetProgressCard.title }}</div>
|
|
||||||
<div class="tp-card__body">
|
|
||||||
<div
|
|
||||||
v-for="sec in targetProgressCard.sections"
|
|
||||||
:key="sec.dept_id"
|
|
||||||
class="tp-section"
|
|
||||||
>
|
|
||||||
<div class="tp-section__head">
|
|
||||||
<div class="tp-section__title-col">
|
|
||||||
<span v-if="sec.parent_dept_name" class="tp-section__parent">{{
|
|
||||||
sec.parent_dept_name
|
|
||||||
}}</span>
|
|
||||||
<span class="tp-section__name">{{ sec.dept_name }}目标</span>
|
|
||||||
</div>
|
|
||||||
<span class="tp-section__target">{{ formatMoneyW(sec.target_amount) }}</span>
|
|
||||||
</div>
|
|
||||||
<table class="tp-table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="tp-lbl">昨日诊金</td>
|
|
||||||
<td class="tp-val">{{ formatInt(sec.yesterday_amount) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="sec.douyin_amount !== null">
|
|
||||||
<td class="tp-lbl">抖音总诊金</td>
|
|
||||||
<td class="tp-val">{{ formatInt(sec.douyin_amount) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="sec.tuifen_amount !== null">
|
|
||||||
<td class="tp-lbl">推粉总诊金</td>
|
|
||||||
<td class="tp-val">{{ formatInt(sec.tuifen_amount) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="tp-lbl">总诊金</td>
|
|
||||||
<td class="tp-val tp-val--total">{{ formatInt(sec.total_amount) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="tp-lbl">目标进度</td>
|
|
||||||
<td class="tp-val">{{ formatMoney(sec.target_progress) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="tp-lbl">完成进度</td>
|
|
||||||
<td class="tp-val" :class="achievementClass(sec.completion_rate)">{{ formatAchievement(sec.completion_rate) }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── 业绩表(4 张/N 张) ────────────────── -->
|
|
||||||
<div v-loading="loading" class="tables-wrap">
|
<div v-loading="loading" class="tables-wrap">
|
||||||
<div v-if="!loading && tables.length === 0" class="empty-tip">
|
<div v-if="!loading && tables.length === 0" class="empty-tip">
|
||||||
<el-empty description="暂无数据" />
|
<el-empty description="暂无数据" />
|
||||||
@@ -464,7 +396,8 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
|
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
|
||||||
按订单<b>创建时间</b>统计(剔除履约已取消),金额与<b>接诊诊单</b>列一致:诊单<b>医助</b>或订单<b>创建人</b>的人事部门落在本展示行(含下级)即计入,多行命中时取最深的展示部门。<br />
|
按订单<b>创建时间</b>统计(剔除履约已取消),金额与<b>接诊诊单</b>列一致:诊单<b>医助</b>或订单<b>创建人</b>的人事部门落在本展示行(含下级)即计入,多行命中时取最深的展示部门。<br />
|
||||||
<b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表可对齐。
|
<b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表可对齐。<br />
|
||||||
|
点击金额可查看对应业务订单明细(侧栏默认「合计业绩」全量列表)。
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||||
@@ -477,7 +410,8 @@
|
|||||||
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
|
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
|
||||||
「{{ tb.channel_name }}」渠道业绩(与卡片标题渠道一致)。与「处方订单列表」业绩<b>同口径</b>:按订单
|
「{{ tb.channel_name }}」渠道业绩(与卡片标题渠道一致)。与「处方订单列表」业绩<b>同口径</b>:按订单
|
||||||
<b>create_time</b> 归日,剔除履约已取消(4)。<br />
|
<b>create_time</b> 归日,剔除履约已取消(4)。<br />
|
||||||
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。
|
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。<br />
|
||||||
|
点击金额可查看该部门本渠道订单明细(侧栏「渠道筛选」列表)。
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
||||||
@@ -611,8 +545,25 @@
|
|||||||
>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</span>
|
>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</span>
|
||||||
<template v-else>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</template>
|
<template v-else>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</template>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ formatMoney(r.performance_amount) }}</td>
|
<td>
|
||||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
|
<span
|
||||||
|
v-if="yejiPerfCellDrawerEligible(tb, r, 'total')"
|
||||||
|
class="yeji-lead-cell--link"
|
||||||
|
@click.stop="onYejiPerformanceCellClick(tb, r, 'total')"
|
||||||
|
>{{ formatMoney(r.performance_amount) }}</span>
|
||||||
|
<template v-else>{{ formatMoney(r.performance_amount) }}</template>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
v-if="tb.channel_name"
|
||||||
|
class="col-completed"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="yejiPerfCellDrawerEligible(tb, r, 'channel')"
|
||||||
|
class="yeji-lead-cell--link"
|
||||||
|
@click.stop="onYejiPerformanceCellClick(tb, r, 'channel')"
|
||||||
|
>{{ formatMoney(r.completed_performance_amount) }}</span>
|
||||||
|
<template v-else>{{ formatMoney(r.completed_performance_amount) }}</template>
|
||||||
|
</td>
|
||||||
<td @click.stop="onYejiDeptAppointmentBookedClick(tb, r)">
|
<td @click.stop="onYejiDeptAppointmentBookedClick(tb, r)">
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
@@ -639,8 +590,25 @@
|
|||||||
v-for="slot in yejiRevisitSlotColumns"
|
v-for="slot in yejiRevisitSlotColumns"
|
||||||
:key="'yjvt-' + idx + '-' + slot"
|
:key="'yjvt-' + idx + '-' + slot"
|
||||||
>{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
|
>{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
|
||||||
<td>{{ formatMoney(tb.total.performance_amount) }}</td>
|
<td>
|
||||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(tb.total.completed_performance_amount) }}</td>
|
<span
|
||||||
|
v-if="yejiPerfTotalRowDrawerEligible(tb, 'total')"
|
||||||
|
class="yeji-lead-cell--link"
|
||||||
|
@click.stop="onYejiPerformanceTotalRowClick(tb, 'total')"
|
||||||
|
>{{ formatMoney(tb.total.performance_amount) }}</span>
|
||||||
|
<template v-else>{{ formatMoney(tb.total.performance_amount) }}</template>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
v-if="tb.channel_name"
|
||||||
|
class="col-completed"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="yejiPerfTotalRowDrawerEligible(tb, 'channel')"
|
||||||
|
class="yeji-lead-cell--link"
|
||||||
|
@click.stop="onYejiPerformanceTotalRowClick(tb, 'channel')"
|
||||||
|
>{{ formatMoney(tb.total.completed_performance_amount) }}</span>
|
||||||
|
<template v-else>{{ formatMoney(tb.total.completed_performance_amount) }}</template>
|
||||||
|
</td>
|
||||||
<td>{{ formatInt(tb.total.appointment_booked_count ?? 0) }}</td>
|
<td>{{ formatInt(tb.total.appointment_booked_count ?? 0) }}</td>
|
||||||
<td>{{ formatInt(tb.total.consult_count) }}</td>
|
<td>{{ formatInt(tb.total.consult_count) }}</td>
|
||||||
<td>{{ formatInt(tb.total.deal_order_count) }}</td>
|
<td>{{ formatInt(tb.total.deal_order_count) }}</td>
|
||||||
@@ -663,6 +631,72 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="目标看板(卡片)" name="zyyt_matrix" lazy>
|
||||||
|
<div v-loading="targetProgressLoading" class="tp-matrix-wrap">
|
||||||
|
<template v-if="targetProgressCard">
|
||||||
|
<h2 class="tp-matrix__title">{{ targetProgressCard.title }}</h2>
|
||||||
|
<div class="tp-matrix" role="list">
|
||||||
|
<div
|
||||||
|
v-for="sec in targetProgressCard.sections"
|
||||||
|
:key="'tm-' + sec.dept_id"
|
||||||
|
class="tp-matrix-card"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div class="tp-matrix-card__head">
|
||||||
|
<div class="tp-matrix-card__titles">
|
||||||
|
<span v-if="sec.parent_dept_name" class="tp-matrix-card__parent">{{
|
||||||
|
sec.parent_dept_name
|
||||||
|
}}</span>
|
||||||
|
<span class="tp-matrix-card__name">{{ sec.dept_name }}目标</span>
|
||||||
|
</div>
|
||||||
|
<span class="tp-matrix-card__target">{{ formatMoneyW(sec.target_amount) }}</span>
|
||||||
|
</div>
|
||||||
|
<table class="tp-matrix-card__table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="tp-matrix-card__lbl">昨日诊金</td>
|
||||||
|
<td class="tp-matrix-card__num">{{ formatInt(sec.yesterday_amount) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="sec.douyin_amount !== null">
|
||||||
|
<td class="tp-matrix-card__lbl">抖音总诊金</td>
|
||||||
|
<td class="tp-matrix-card__num">{{ formatInt(sec.douyin_amount) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="sec.tuifen_amount !== null">
|
||||||
|
<td class="tp-matrix-card__lbl">推粉总诊金</td>
|
||||||
|
<td class="tp-matrix-card__num">{{ formatInt(sec.tuifen_amount) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tp-matrix-card__lbl">总诊金</td>
|
||||||
|
<td class="tp-matrix-card__num tp-matrix-card__num--hot">
|
||||||
|
{{ formatInt(sec.total_amount) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tp-matrix-card__lbl">目标进度</td>
|
||||||
|
<td class="tp-matrix-card__num">{{ formatMoney(sec.target_progress) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="tp-matrix-card__lbl">完成进度</td>
|
||||||
|
<td
|
||||||
|
class="tp-matrix-card__num tp-matrix-card__num--hot"
|
||||||
|
:class="achievementClass(sec.completion_rate)"
|
||||||
|
>
|
||||||
|
{{ formatAchievement(sec.completion_rate) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-empty
|
||||||
|
v-else-if="!targetProgressLoading"
|
||||||
|
class="tp-matrix-empty"
|
||||||
|
description="暂无目标数据,请先配置当月部门目标或为部门设置诊金目标后刷新页面"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -1623,7 +1657,7 @@ const appointmentLinesShowChannelColumn = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** 图表 / 数据表切换 */
|
/** 图表 / 数据表切换 */
|
||||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt'>('zyyt')
|
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt' | 'zyyt_matrix'>('zyyt')
|
||||||
|
|
||||||
/** 与菜单 stats.doctorDailyStats/overview 一致;无权限则不展示医生统计表/图、不请求接口 */
|
/** 与菜单 stats.doctorDailyStats/overview 一致;无权限则不展示医生统计表/图、不请求接口 */
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -2586,40 +2620,79 @@ function formatOrderDrawerTime(t: unknown): string {
|
|||||||
return s.length >= 16 ? s.slice(0, 16) : s
|
return s.length >= 16 ? s.slice(0, 16) : s
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOrderDrawerPage() {
|
/** 业务订单侧栏列表筛选(不含分页) */
|
||||||
|
function buildOrderDrawerListParams(): Record<string, any> | null {
|
||||||
const f = orderDrawerFilter.value
|
const f = orderDrawerFilter.value
|
||||||
if (!f) return
|
if (!f) return null
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
start_time: `${f.start} 00:00:00`,
|
||||||
|
end_time: `${f.end} 23:59:59`,
|
||||||
|
/** 与业绩列口径一致:不列出履约已取消(4) */
|
||||||
|
exclude_fulfillment_cancelled: 1,
|
||||||
|
/** 侧栏展示约诊数等 extend 字段 */
|
||||||
|
yeji_order_drawer: 1,
|
||||||
|
}
|
||||||
|
if (f.mode === 'dept') {
|
||||||
|
params.assistant_dept_id = f.deptId
|
||||||
|
/** 勿传 yeji_drawer_match_table_performance:该参数按「业绩归因」医助收窄,与看板渠道成交/渠道业绩的列表摊行(诊单医助∪创建人∪最深展示行)不一致 */
|
||||||
|
if (selectedDeptIds.value.length > 0) {
|
||||||
|
params.dept_ids = selectedDeptIds.value.join(',')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.assistant_id = f.assistantId
|
||||||
|
if (f.erCenterRevisitSlot !== undefined) {
|
||||||
|
params.yeji_er_center_revisit_only = 1
|
||||||
|
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
selectedChannel.value &&
|
||||||
|
orderDrawerVisible.value &&
|
||||||
|
orderDrawerListTab.value === 'channel'
|
||||||
|
) {
|
||||||
|
params.channel_code = selectedChannel.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hydrateOrderDrawerSplitPerfCache() {
|
||||||
|
const base = buildOrderDrawerListParams()
|
||||||
|
if (!base || !selectedChannel.value) return
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
...base,
|
||||||
|
channel_code: selectedChannel.value,
|
||||||
|
page_no: 1,
|
||||||
|
page_size: 1,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res: any = await prescriptionOrderLists(params)
|
||||||
|
const ex = res?.extend && typeof res.extend === 'object' ? res.extend : null
|
||||||
|
if (ex && ex.stats_yeji_drawer_total_performance_amount != null) {
|
||||||
|
orderDrawerSplitPerfCache.value = {
|
||||||
|
totalAmount: Number(ex.stats_yeji_drawer_total_performance_amount),
|
||||||
|
totalOrderCount: Number(ex.stats_yeji_drawer_total_performance_order_count ?? 0),
|
||||||
|
channelAmount: Number(ex.stats_order_amount_performance ?? 0),
|
||||||
|
channelOrderCount: Number(res?.count ?? 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 对照指标失败不打断列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOrderDrawerPage() {
|
||||||
|
const base = buildOrderDrawerListParams()
|
||||||
|
if (!base) return
|
||||||
|
if (selectedChannel.value && orderDrawerListTab.value === 'total') {
|
||||||
|
orderDrawerSplitPerfCache.value = null
|
||||||
|
}
|
||||||
orderDrawerLoading.value = true
|
orderDrawerLoading.value = true
|
||||||
try {
|
try {
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
|
...base,
|
||||||
page_no: orderDrawerPage.value,
|
page_no: orderDrawerPage.value,
|
||||||
page_size: orderDrawerPageSize.value,
|
page_size: orderDrawerPageSize.value,
|
||||||
start_time: `${f.start} 00:00:00`,
|
|
||||||
end_time: `${f.end} 23:59:59`,
|
|
||||||
/** 与业绩列口径一致:不列出履约已取消(4) */
|
|
||||||
exclude_fulfillment_cancelled: 1,
|
|
||||||
/** 侧栏展示约诊数等 extend 字段 */
|
|
||||||
yeji_order_drawer: 1,
|
|
||||||
}
|
|
||||||
if (f.mode === 'dept') {
|
|
||||||
params.assistant_dept_id = f.deptId
|
|
||||||
/** 与看板「接诊诊单」列一致:处方列表默认 assistant_dept_id(诊单医助或创建人∪子树),不用业绩摊行医助集合 */
|
|
||||||
if (selectedDeptIds.value.length > 0) {
|
|
||||||
params.dept_ids = selectedDeptIds.value.join(',')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
params.assistant_id = f.assistantId
|
|
||||||
if (f.erCenterRevisitSlot !== undefined) {
|
|
||||||
params.yeji_er_center_revisit_only = 1
|
|
||||||
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)
|
const res: any = await prescriptionOrderLists(params)
|
||||||
orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
|
orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
|
||||||
@@ -2640,6 +2713,13 @@ async function fetchOrderDrawerPage() {
|
|||||||
channelOrderCount: Number(res?.count ?? 0),
|
channelOrderCount: Number(res?.count ?? 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
selectedChannel.value &&
|
||||||
|
orderDrawerVisible.value &&
|
||||||
|
orderDrawerListTab.value === 'total'
|
||||||
|
) {
|
||||||
|
await hydrateOrderDrawerSplitPerfCache()
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
|
ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
|
||||||
orderDrawerLists.value = []
|
orderDrawerLists.value = []
|
||||||
@@ -2650,8 +2730,9 @@ async function fetchOrderDrawerPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: 'total' | 'channel' }) {
|
||||||
orderDrawerListTab.value = 'channel'
|
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
|
||||||
|
orderDrawerListTab.value = channelSplit ? opts?.listTab ?? 'channel' : 'channel'
|
||||||
orderDrawerSplitPerfCache.value = null
|
orderDrawerSplitPerfCache.value = null
|
||||||
orderDrawerFilter.value = {
|
orderDrawerFilter.value = {
|
||||||
mode: 'dept',
|
mode: 'dept',
|
||||||
@@ -2667,6 +2748,69 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
|||||||
void fetchOrderDrawerPage()
|
void fetchOrderDrawerPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 顶部「展示部门」仅选中 1 个根节点时:合计行业绩对应该子树下的订单汇总 */
|
||||||
|
function openOrderDrawerByYejiTableScope(
|
||||||
|
tb: YejiTable,
|
||||||
|
rootDeptId: number,
|
||||||
|
listTab: 'total' | 'channel'
|
||||||
|
) {
|
||||||
|
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
|
||||||
|
orderDrawerListTab.value = channelSplit ? listTab : 'channel'
|
||||||
|
orderDrawerSplitPerfCache.value = null
|
||||||
|
orderDrawerFilter.value = {
|
||||||
|
mode: 'dept',
|
||||||
|
deptId: rootDeptId,
|
||||||
|
deptName: '当前展示合计',
|
||||||
|
start: tb.start_date,
|
||||||
|
end: tb.end_date,
|
||||||
|
}
|
||||||
|
orderDrawerTitle.value = '业务订单 · 当前展示合计'
|
||||||
|
orderDrawerHint.value = `创建时间:${tb.start_date} ~ ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约已取消`
|
||||||
|
orderDrawerPage.value = 1
|
||||||
|
orderDrawerVisible.value = true
|
||||||
|
void fetchOrderDrawerPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function yejiPerfCellDrawerEligible(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel'): boolean {
|
||||||
|
if (isUnassignedYejiRow(row) || row.dept_id <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (kind === 'channel' && !tb.channel_name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function yejiPerfTotalRowDrawerEligible(tb: YejiTable, kind: 'total' | 'channel'): boolean {
|
||||||
|
if (selectedDeptIds.value.length !== 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (kind === 'channel' && !tb.channel_name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onYejiPerformanceCellClick(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel') {
|
||||||
|
if (!yejiPerfCellDrawerEligible(tb, row, kind)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
|
||||||
|
openOrderDrawerByDept(tb, row, { listTab })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onYejiPerformanceTotalRowClick(tb: YejiTable, kind: 'total' | 'channel') {
|
||||||
|
if (!yejiPerfTotalRowDrawerEligible(tb, kind)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rootDeptId = selectedDeptIds.value[0]
|
||||||
|
if (!rootDeptId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
|
||||||
|
openOrderDrawerByYejiTableScope(tb, rootDeptId, listTab)
|
||||||
|
}
|
||||||
|
|
||||||
function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['rows'][0], start: string, end: string) {
|
function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['rows'][0], start: string, end: string) {
|
||||||
orderDrawerListTab.value = 'channel'
|
orderDrawerListTab.value = 'channel'
|
||||||
orderDrawerSplitPerfCache.value = null
|
orderDrawerSplitPerfCache.value = null
|
||||||
@@ -3946,118 +4090,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 目标进度卡片 ── */
|
/* 目标完成率色条:目标看板(卡片)Tab 复用 */
|
||||||
.tp-wrap {
|
|
||||||
min-height: 40px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tp-card {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: var(--yj-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 13px;
|
|
||||||
background: var(--yj-surface);
|
|
||||||
border: 1px solid var(--yj-line);
|
|
||||||
box-shadow: var(--yj-shadow-md);
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
background: var(--yj-surface-muted);
|
|
||||||
color: var(--yj-ink);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 14px 20px;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
border-bottom: 1px solid var(--yj-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__body {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tp-section {
|
|
||||||
min-width: 200px;
|
|
||||||
flex: 1 1 200px;
|
|
||||||
border-right: 1px solid var(--yj-line);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
background: var(--yj-brand-soft);
|
|
||||||
padding: 11px 14px;
|
|
||||||
border-bottom: 1px solid var(--yj-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__parent {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--yj-muted);
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--yj-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__target {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--yj-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tp-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
.tp-lbl {
|
|
||||||
padding: 8px 12px;
|
|
||||||
color: var(--yj-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
border-bottom: 1px solid var(--yj-line);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tp-val {
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: right;
|
|
||||||
border-bottom: 1px solid var(--yj-line);
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--yj-ink);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
|
|
||||||
&--total {
|
|
||||||
color: var(--yj-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tp-good {
|
.tp-good {
|
||||||
color: #15803d !important;
|
color: #15803d !important;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -4071,6 +4104,126 @@ onMounted(async () => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 目标看板卡片栅格(与顶部「甄养堂」条带数据同源,8 列铺满) */
|
||||||
|
.tp-matrix-wrap {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 8px 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-matrix__title {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--yj-ink);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-matrix {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1600px) {
|
||||||
|
.tp-matrix {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.tp-matrix {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-matrix-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--yj-line);
|
||||||
|
border-radius: var(--yj-radius-sm);
|
||||||
|
background: var(--yj-surface);
|
||||||
|
box-shadow: var(--yj-shadow-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--yj-brand-soft);
|
||||||
|
border-bottom: 1px solid var(--yj-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__titles {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__parent {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--yj-muted);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yj-ink);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__target {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--yj-accent);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__lbl {
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: var(--yj-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
border-bottom: 1px solid var(--yj-line);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__num {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
border-bottom: 1px solid var(--yj-line);
|
||||||
|
color: var(--yj-ink);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
|
&--hot {
|
||||||
|
color: var(--yj-accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-matrix-empty {
|
||||||
|
padding: 32px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.leaderboard-sub {
|
.leaderboard-sub {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -4640,6 +4793,10 @@ tbody tr.yeji-row--action:hover .yeji-lead-cell--link {
|
|||||||
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tbody tr.total-row:hover .yeji-lead-cell--link {
|
||||||
|
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||||
|
}
|
||||||
|
|
||||||
.yeji-leadlines-dialog__pager {
|
.yeji-leadlines-dialog__pager {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
v-model="form.channel_source"
|
v-model="form.channel_source"
|
||||||
placeholder="请选择渠道来源"
|
placeholder="请选择渠道来源"
|
||||||
class="channel-source-select"
|
class="channel-source-select"
|
||||||
|
@change="onChannelSourceChange"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in channelOptions"
|
v-for="item in channelOptions"
|
||||||
@@ -58,6 +59,20 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
v-if="needsChannelSourceDetail"
|
||||||
|
label="自媒体补充:"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="form.channel_source_detail"
|
||||||
|
clearable
|
||||||
|
maxlength="128"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入自媒体相关补充内容"
|
||||||
|
class="channel-source-select"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<!-- 预约医生 -->
|
<!-- 预约医生 -->
|
||||||
<el-form-item label="预约医生:">
|
<el-form-item label="预约医生:">
|
||||||
<div class="doctor-list">
|
<div class="doctor-list">
|
||||||
@@ -233,9 +248,44 @@ const form = reactive({
|
|||||||
date: '',
|
date: '',
|
||||||
appointmentTime: '',
|
appointmentTime: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
channel_source: '' as string
|
channel_source: '' as string,
|
||||||
|
channel_source_detail: '' as string
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 仅这些渠道字典 name 需填「自媒体补充」(与后台字典名称完全一致) */
|
||||||
|
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
|
||||||
|
'自媒体4H',
|
||||||
|
'自媒体3Q',
|
||||||
|
'自媒体3H',
|
||||||
|
'自媒体2H',
|
||||||
|
'自媒体2Q'
|
||||||
|
])
|
||||||
|
|
||||||
|
function channelNameRequiresSelfMediaDetail(name: string) {
|
||||||
|
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前选中渠道字典名称(用于判断是否需填「自媒体补充」) */
|
||||||
|
const selectedChannelDictName = computed(() => {
|
||||||
|
const v = form.channel_source
|
||||||
|
if (v === '' || v == null) return ''
|
||||||
|
const row = channelOptions.value.find((item: any) => String(item.value) === String(v))
|
||||||
|
return row ? String(row.name ?? '').trim() : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 仅白名单内字典项需填写补充说明 */
|
||||||
|
const needsChannelSourceDetail = computed(() =>
|
||||||
|
channelNameRequiresSelfMediaDetail(selectedChannelDictName.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChannelSourceChange = (val: string) => {
|
||||||
|
const row = channelOptions.value.find((item: any) => String(item.value) === String(val))
|
||||||
|
const name = row ? String(row.name ?? '').trim() : ''
|
||||||
|
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||||||
|
form.channel_source_detail = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
|
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
|
||||||
const dateOptions = computed<DateOption[]>(() => {
|
const dateOptions = computed<DateOption[]>(() => {
|
||||||
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
|
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
|
||||||
@@ -321,7 +371,8 @@ const canSubmit = computed(() => {
|
|||||||
selectedDoctorId.value &&
|
selectedDoctorId.value &&
|
||||||
form.date &&
|
form.date &&
|
||||||
form.appointmentTime &&
|
form.appointmentTime &&
|
||||||
!!form.channel_source
|
!!form.channel_source &&
|
||||||
|
(!needsChannelSourceDetail.value || form.channel_source_detail.trim() !== '')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -344,6 +395,7 @@ const open = async (patient: any) => {
|
|||||||
form.appointmentTime = ''
|
form.appointmentTime = ''
|
||||||
form.remark = ''
|
form.remark = ''
|
||||||
form.channel_source = ''
|
form.channel_source = ''
|
||||||
|
form.channel_source_detail = ''
|
||||||
timeSlots.value = []
|
timeSlots.value = []
|
||||||
doctorRosterDates.value = []
|
doctorRosterDates.value = []
|
||||||
lastVisit.value = ''
|
lastVisit.value = ''
|
||||||
@@ -412,7 +464,13 @@ const loadLastVisit = async () => {
|
|||||||
const loadChannelOptions = async () => {
|
const loadChannelOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getDictData({ type: 'channels' })
|
const data = await getDictData({ type: 'channels' })
|
||||||
channelOptions.value = (data?.channels || []).filter((row: any) => row.status !== 0)
|
const rows = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||||
|
rows.sort((a: any, b: any) => {
|
||||||
|
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
||||||
|
if (ds !== 0) return ds
|
||||||
|
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
||||||
|
})
|
||||||
|
channelOptions.value = rows
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载渠道来源失败:', e)
|
console.error('加载渠道来源失败:', e)
|
||||||
channelOptions.value = []
|
channelOptions.value = []
|
||||||
@@ -612,6 +670,10 @@ const handleConfirm = async () => {
|
|||||||
feedback.msgWarning('请选择渠道来源')
|
feedback.msgWarning('请选择渠道来源')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (needsChannelSourceDetail.value && !form.channel_source_detail.trim()) {
|
||||||
|
feedback.msgWarning('请填写自媒体渠道补充信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (blockedByTodayDuplicate.value) {
|
if (blockedByTodayDuplicate.value) {
|
||||||
feedback.msgWarning(
|
feedback.msgWarning(
|
||||||
'该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
|
'该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
|
||||||
@@ -631,7 +693,8 @@ const handleConfirm = async () => {
|
|||||||
appointment_time: form.appointmentTime,
|
appointment_time: form.appointmentTime,
|
||||||
appointment_type: form.appointmentType,
|
appointment_type: form.appointmentType,
|
||||||
remark: form.remark,
|
remark: form.remark,
|
||||||
channel_source: form.channel_source
|
channel_source: form.channel_source,
|
||||||
|
channel_source_detail: form.channel_source_detail.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('提交预约参数:', params)
|
console.log('提交预约参数:', params)
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ function channelLabel(row: Record<string, any>) {
|
|||||||
const raw = row.channel_source ?? row.channels ?? ''
|
const raw = row.channel_source ?? row.channels ?? ''
|
||||||
if (raw === '' || raw === null || raw === undefined) return '—'
|
if (raw === '' || raw === null || raw === undefined) return '—'
|
||||||
const key = String(raw)
|
const key = String(raw)
|
||||||
return channelMap.value[key] || key
|
const base = channelMap.value[key] || key
|
||||||
|
const extra = String(row.channel_source_detail ?? '').trim()
|
||||||
|
return extra !== '' ? `${base}(${extra})` : base
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortRows(list: any[]) {
|
function sortRows(list: any[]) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -552,7 +552,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
unset($item);
|
unset($item);
|
||||||
|
|
||||||
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
|
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
|
||||||
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists);
|
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists, $this->params);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $lists;
|
return $lists;
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ use think\facade\Log;
|
|||||||
*/
|
*/
|
||||||
class AppointmentLogic extends BaseLogic
|
class AppointmentLogic extends BaseLogic
|
||||||
{
|
{
|
||||||
|
/** 仅这些字典名称需填写 channel_source_detail(与 admin 预约弹窗白名单一致,按 name 全等) */
|
||||||
|
private const CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL = [
|
||||||
|
'自媒体4H',
|
||||||
|
'自媒体3Q',
|
||||||
|
'自媒体3H',
|
||||||
|
'自媒体2H',
|
||||||
|
'自媒体2Q',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static function channelDictNameRequiresSourceDetail(string $name): bool
|
||||||
|
{
|
||||||
|
return in_array($name, self::CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL, true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notes 获取可用时间段
|
* @notes 获取可用时间段
|
||||||
* @param array $params
|
* @param array $params
|
||||||
@@ -233,6 +247,21 @@ class AppointmentLogic extends BaseLogic
|
|||||||
Db::rollback();
|
Db::rollback();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$chSrc = trim((string) ($params['channel_source'] ?? ''));
|
||||||
|
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
|
||||||
|
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
|
||||||
|
->where('value', $chSrc)
|
||||||
|
->value('name') ?? ''));
|
||||||
|
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
|
||||||
|
if ($chDetail === '') {
|
||||||
|
self::setError('请填写自媒体渠道补充信息');
|
||||||
|
Db::rollback();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'patient_id' => $params['patient_id'],
|
'patient_id' => $params['patient_id'],
|
||||||
'assistant_id'=>$params['assistant_id'],
|
'assistant_id'=>$params['assistant_id'],
|
||||||
@@ -244,7 +273,8 @@ class AppointmentLogic extends BaseLogic
|
|||||||
'appointment_type' => $params['appointment_type'] ?? 'video',
|
'appointment_type' => $params['appointment_type'] ?? 'video',
|
||||||
'remark' => $params['remark'] ?? '',
|
'remark' => $params['remark'] ?? '',
|
||||||
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
|
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
|
||||||
'channel_source' => trim((string) ($params['channel_source'] ?? '')),
|
'channel_source' => $chSrc,
|
||||||
|
'channel_source_detail' => $chDetail,
|
||||||
'status' => 1,
|
'status' => 1,
|
||||||
'create_time' => time(),
|
'create_time' => time(),
|
||||||
'update_time' => time(),
|
'update_time' => time(),
|
||||||
|
|||||||
@@ -4701,7 +4701,8 @@ class YejiStatsLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source)
|
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source);
|
||||||
|
* **二中心及其组织下级**展示部门与看板一致:选定渠道收窄时本条无渠道订单(表中渠道业绩已为 0)。
|
||||||
*
|
*
|
||||||
* @param Query $query PrescriptionOrder 主表查询
|
* @param Query $query PrescriptionOrder 主表查询
|
||||||
* @param array<string, mixed> $params
|
* @param array<string, mixed> $params
|
||||||
@@ -4715,6 +4716,16 @@ class YejiStatsLogic
|
|||||||
if ($channelCode === '') {
|
if ($channelCode === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/** @see excludeErCenterSubtreeFromChannelMetrics 看板选定渠道时对二中心子树渠道列置 0,侧栏须同步 */
|
||||||
|
$assistantDeptForDrawer = (int) ($params['assistant_dept_id'] ?? 0);
|
||||||
|
if ($assistantDeptForDrawer > 0) {
|
||||||
|
$erCenterSubtree = DeptLogic::getErCenterSubtreeDeptIdSet();
|
||||||
|
if (isset($erCenterSubtree[$assistantDeptForDrawer])) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
$startDate = '';
|
$startDate = '';
|
||||||
$endDate = '';
|
$endDate = '';
|
||||||
$st = trim((string) ($params['start_time'] ?? ''));
|
$st = trim((string) ($params['start_time'] ?? ''));
|
||||||
@@ -4803,4 +4814,110 @@ class YejiStatsLogic
|
|||||||
$query->whereRaw('0 = 1');
|
$query->whereRaw('0 = 1');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业绩看板业务订单侧栏:在「渠道筛选」模式下,为每个诊单挑一条用于「关联挂号」展示的预约行——
|
||||||
|
* 须为已完成(status=3)、挂号助理为医助(role_id=2),且 channels/channel_source 与当前 channel_code 同源命中(与 {@see applyYejiDrawerChannelFilterToPrescriptionOrderQuery} 的 dict 收窄一致)。
|
||||||
|
* 若为 tag 等未走 channels 表达式的情况则返回空图,交由列表沿用处方/最近挂号。
|
||||||
|
*
|
||||||
|
* @param int[] $diagnosisIds doctor_appointment.patient_id(与本系统诊单 id 一致)
|
||||||
|
* @param array<string, mixed> $params 与列表同源:yeji_order_drawer、channel_code、start_time、end_time
|
||||||
|
*
|
||||||
|
* @return array<int, int> diagnosis_id => appointment_id
|
||||||
|
*/
|
||||||
|
public static function batchResolveYejiChannelDrawerHighlightAppointmentIds(array $diagnosisIds, array $params): array
|
||||||
|
{
|
||||||
|
$diagnosisIds = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0)));
|
||||||
|
if ($diagnosisIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$norm = self::normalizeAppointmentChannelValues($ctx['appointmentChannelValues']);
|
||||||
|
$srcStrings = self::appointmentChannelDictStringValuesForDrawer($ctx['channelInfo']);
|
||||||
|
if ($norm === [] && $srcStrings === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apTable = self::tableWithPrefix('doctor_appointment');
|
||||||
|
$arTable = self::tableWithPrefix('admin_role');
|
||||||
|
|
||||||
|
$hasCh = self::doctorAppointmentHasChannelsColumn();
|
||||||
|
$orParts = [];
|
||||||
|
$channelBind = [];
|
||||||
|
if ($norm !== [] && $hasCh) {
|
||||||
|
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $norm)));
|
||||||
|
$ph = implode(',', array_fill(0, count($strVals), '?'));
|
||||||
|
$orParts[] = "ap.`channels` IN ({$ph})";
|
||||||
|
foreach ($strVals as $sv) {
|
||||||
|
$channelBind[] = $sv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($srcStrings !== [] && self::doctorAppointmentHasChannelSourceColumn()) {
|
||||||
|
$ph = implode(',', array_fill(0, count($srcStrings), '?'));
|
||||||
|
$orParts[] = "ap.`channel_source` IN ({$ph})";
|
||||||
|
foreach ($srcStrings as $s) {
|
||||||
|
$channelBind[] = $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($orParts === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inPh = implode(',', array_fill(0, count($diagnosisIds), '?'));
|
||||||
|
$bind = array_merge($diagnosisIds, $channelBind);
|
||||||
|
|
||||||
|
$orSql = '(' . implode(' OR ', $orParts) . ')';
|
||||||
|
$sql = "SELECT ap.`patient_id` AS dg, MAX(ap.`id`) AS aid FROM `{$apTable}` ap "
|
||||||
|
. "INNER JOIN `{$arTable}` ar ON ar.`admin_id` = ap.`assistant_id` AND ar.`role_id` = 2 "
|
||||||
|
. "WHERE ap.`patient_id` IN ({$inPh}) AND ap.`status` = 3 AND {$orSql} "
|
||||||
|
. 'GROUP BY ap.`patient_id`';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rows = Db::query($sql, $bind);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$dg = (int) ($r['dg'] ?? $r['DG'] ?? 0);
|
||||||
|
$aid = (int) ($r['aid'] ?? $r['AID'] ?? 0);
|
||||||
|
if ($dg > 0 && $aid > 0) {
|
||||||
|
$out[$dg] = $aid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\adminapi\logic\tcm;
|
namespace app\adminapi\logic\tcm;
|
||||||
|
|
||||||
|
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||||
use app\adminapi\logic\auth\AuthLogic;
|
use app\adminapi\logic\auth\AuthLogic;
|
||||||
use app\adminapi\logic\order\OrderLogic;
|
use app\adminapi\logic\order\OrderLogic;
|
||||||
use app\common\model\Order;
|
use app\common\model\Order;
|
||||||
@@ -1049,11 +1050,13 @@ class PrescriptionOrderLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约)
|
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约;
|
||||||
|
* 业绩侧栏且带 channel_code 时优先展示与渠道收窄同源的已完成挂号,避免列表行属自媒体2Q 但展示关联为自媒体1)
|
||||||
*
|
*
|
||||||
* @param array<int, array<string, mixed>> $lists
|
* @param array<int, array<string, mixed>> $lists
|
||||||
|
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(可选)
|
||||||
*/
|
*/
|
||||||
public static function appendLinkedAppointmentsToListRows(array &$lists): void
|
public static function appendLinkedAppointmentsToListRows(array &$lists, array $listsParams = []): void
|
||||||
{
|
{
|
||||||
if ($lists === []) {
|
if ($lists === []) {
|
||||||
return;
|
return;
|
||||||
@@ -1063,6 +1066,23 @@ class PrescriptionOrderLogic
|
|||||||
}
|
}
|
||||||
unset($item);
|
unset($item);
|
||||||
|
|
||||||
|
$yejiChannelHighlightByDiag = [];
|
||||||
|
if (
|
||||||
|
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
|
||||||
|
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
|
||||||
|
) {
|
||||||
|
$allDiagIdsForYeji = array_values(array_unique(array_filter(
|
||||||
|
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
|
||||||
|
static fn (int $id): bool => $id > 0
|
||||||
|
)));
|
||||||
|
if ($allDiagIdsForYeji !== []) {
|
||||||
|
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
|
||||||
|
$allDiagIdsForYeji,
|
||||||
|
$listsParams
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rxIds = array_values(array_unique(array_filter(array_map(
|
$rxIds = array_values(array_unique(array_filter(array_map(
|
||||||
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
|
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
|
||||||
$lists
|
$lists
|
||||||
@@ -1113,6 +1133,10 @@ class PrescriptionOrderLogic
|
|||||||
if ($resolved <= 0 && $dgId > 0) {
|
if ($resolved <= 0 && $dgId > 0) {
|
||||||
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
|
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
|
||||||
}
|
}
|
||||||
|
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
|
||||||
|
if ($preferredYeji > 0) {
|
||||||
|
$resolved = $preferredYeji;
|
||||||
|
}
|
||||||
if ($resolved > 0) {
|
if ($resolved > 0) {
|
||||||
$resolvedPairs[$idx] = [$resolved, $rxApptId];
|
$resolvedPairs[$idx] = [$resolved, $rxApptId];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class AppointmentValidate extends BaseValidate
|
|||||||
'appointment_time' => 'require',
|
'appointment_time' => 'require',
|
||||||
'appointment_type' => 'in:video,text,phone',
|
'appointment_type' => 'in:video,text,phone',
|
||||||
'channel_source' => 'require',
|
'channel_source' => 'require',
|
||||||
|
'channel_source_detail' => 'max:128',
|
||||||
'note_id' => 'require|integer',
|
'note_id' => 'require|integer',
|
||||||
'image_type' => 'require|in:tongue_images,report_files',
|
'image_type' => 'require|in:tongue_images,report_files',
|
||||||
'image_path' => 'require',
|
'image_path' => 'require',
|
||||||
@@ -44,6 +45,7 @@ class AppointmentValidate extends BaseValidate
|
|||||||
'appointment_time' => '预约时间',
|
'appointment_time' => '预约时间',
|
||||||
'appointment_type' => '预约类型',
|
'appointment_type' => '预约类型',
|
||||||
'channel_source' => '渠道来源',
|
'channel_source' => '渠道来源',
|
||||||
|
'channel_source_detail' => '渠道补充说明',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +54,7 @@ class AppointmentValidate extends BaseValidate
|
|||||||
*/
|
*/
|
||||||
public function sceneCreate()
|
public function sceneCreate()
|
||||||
{
|
{
|
||||||
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source']);
|
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source', 'channel_source_detail']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 挂号:自媒体等渠道所需的补充说明(与 channel_source 配合使用)
|
||||||
|
ALTER TABLE `zyt_doctor_appointment`
|
||||||
|
ADD COLUMN `channel_source_detail` varchar(128) NOT NULL DEFAULT '' COMMENT '渠道补充说明(如自媒体账号等)' AFTER `channel_source`;
|
||||||
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS `zyt_doctor_appointment` (
|
|||||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1=已预约,2=已取消,3=已完成',
|
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1=已预约,2=已取消,3=已完成',
|
||||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
`channel_source` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道来源(字典channels)',
|
`channel_source` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道来源(字典channels)',
|
||||||
|
`channel_source_detail` varchar(128) NOT NULL DEFAULT '' COMMENT '渠道补充说明(如自媒体账号等)',
|
||||||
`create_time` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
`create_time` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||||
`update_time` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
|
`update_time` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
|||||||
Reference in New Issue
Block a user