更新
This commit is contained in:
@@ -68,6 +68,21 @@ export function yejiStatsLeadLines(params: {
|
||||
return request.get({ url: '/stats.yejiStats/leadLines', params })
|
||||
}
|
||||
|
||||
/** 医助排行榜「预约诊单」逐条挂号明细;业绩主表部门行传 dept_id(可与 assistant_id 二选一) */
|
||||
export function yejiStatsAppointmentLines(params: {
|
||||
start_date: string
|
||||
end_date: string
|
||||
assistant_id?: number
|
||||
dept_id?: number
|
||||
dept_ids?: string
|
||||
channel_code?: string
|
||||
tag_id?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
return request.get({ url: '/stats.yejiStats/appointmentLines', params })
|
||||
}
|
||||
|
||||
/** 二中心复诊:部门行下钻医助 × 业务订单笔数 */
|
||||
export function yejiStatsRevisitBreakdown(params: {
|
||||
start_date: string
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
<th class="col-consult">接诊单数</th>
|
||||
<th class="col-deal">接诊诊单</th>
|
||||
<th class="col-assign">被指派数</th>
|
||||
<th class="col-appointment">预约诊单</th>
|
||||
<template v-if="lb.er_center_subtree">
|
||||
<th class="col-revisit">复诊合计</th>
|
||||
<th
|
||||
@@ -248,6 +249,15 @@
|
||||
<td class="col-consult">{{ formatLeaderboardInt(r.consult_count) }}</td>
|
||||
<td class="col-deal">{{ formatLeaderboardInt(r.deal_order_count) }}</td>
|
||||
<td class="col-assign">{{ formatLeaderboardInt(r.assign_count ?? 0) }}</td>
|
||||
<td class="col-appointment" @click.stop="onLeaderboardAppointmentCellClick(r)">
|
||||
<span
|
||||
v-if="Number(r.appointment_count ?? 0) > 0"
|
||||
class="yeji-lead-cell--link"
|
||||
>{{ formatLeaderboardInt(r.appointment_count ?? 0) }}</span>
|
||||
<template v-else>{{
|
||||
formatLeaderboardInt(r.appointment_count ?? 0)
|
||||
}}</template>
|
||||
</td>
|
||||
<template v-if="lb.er_center_subtree">
|
||||
<td class="col-revisit" @click.stop="onLeaderboardRevisitTotalClick(r)">
|
||||
<span
|
||||
@@ -284,6 +294,9 @@
|
||||
<span v-if="lb.er_center_subtree">
|
||||
· 复诊列为二中心口径(同一诊单业务单序列第 2 笔起),点击数字查看对应业务订单
|
||||
</span>
|
||||
<span>
|
||||
· 预约诊单:预约日期在统计区间内,状态含已预约/已完成/已过号(不含已取消),医助归属与接诊单数一致;各科组展示全部医助。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -305,7 +318,7 @@
|
||||
|
||||
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
||||
<p class="yeji-zyyt-intro">
|
||||
各中心部门诊金、业绩、进线与 ROI 等,与顶部渠道、展示部门、日期筛选联动。
|
||||
各中心部门诊金、业绩、进线与 ROI 等,与顶部渠道、展示部门、日期筛选联动。本月 / 本周 / 今日等多张业绩表中「预约诊单」列数字可点开查看每条挂号记录。
|
||||
</p>
|
||||
|
||||
<!-- <el-alert
|
||||
@@ -475,8 +488,19 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
已完成挂号条数,与「医生预约 / 已完成」列表同口径。<br />
|
||||
部门按医助→接诊医生归属。
|
||||
已预约 / 已完成 / 已过号(不含已取消),按<b>预约日期</b>落在区间内统计;部门归属医助优先,否则接诊医生;与<b>医助排行榜 · 预约诊单</b>及明细弹窗同口径。<br />
|
||||
点击数字可查看每条挂号记录。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<th>
|
||||
<span>已完成挂号</span>
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
仅<b>已完成</b>(status=3),与 admin「医生预约 / 已完成」列表计数条件一致(appointment_date、诊单未软删);归属规则同「接诊诊单」列数据源说明。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
@@ -588,6 +612,16 @@
|
||||
</td>
|
||||
<td>{{ formatMoney(r.performance_amount) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
|
||||
<td @click.stop="onYejiDeptAppointmentBookedClick(tb, r)">
|
||||
<span
|
||||
v-if="
|
||||
yejiAppointmentBookedCellClickable(r) &&
|
||||
Number(r.appointment_booked_count ?? 0) > 0
|
||||
"
|
||||
class="yeji-lead-cell--link"
|
||||
>{{ formatInt(r.appointment_booked_count ?? 0) }}</span>
|
||||
<template v-else>{{ formatInt(r.appointment_booked_count ?? 0) }}</template>
|
||||
</td>
|
||||
<td>{{ formatInt(r.consult_count) }}</td>
|
||||
<td>{{ formatInt(r.deal_order_count) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatInt(r.channel_consult_count) }}</td>
|
||||
@@ -606,6 +640,7 @@
|
||||
>{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
|
||||
<td>{{ formatMoney(tb.total.performance_amount) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(tb.total.completed_performance_amount) }}</td>
|
||||
<td>{{ formatInt(tb.total.appointment_booked_count ?? 0) }}</td>
|
||||
<td>{{ formatInt(tb.total.consult_count) }}</td>
|
||||
<td>{{ formatInt(tb.total.deal_order_count) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatInt(tb.total.channel_consult_count) }}</td>
|
||||
@@ -1106,6 +1141,70 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="appointmentLinesDialogVisible"
|
||||
width="min(960px, 96vw)"
|
||||
destroy-on-close
|
||||
class="yeji-leadlines-dialog"
|
||||
>
|
||||
<template #header>
|
||||
<div class="yeji-unassigned-dialog__head">
|
||||
<span class="yeji-unassigned-dialog__title">预约诊单 · 挂号明细</span>
|
||||
<p class="yeji-unassigned-dialog__sub">{{ appointmentLinesSubtitle }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<p v-if="appointmentLinesApiNote" class="yeji-unassigned-dialog__note">{{ appointmentLinesApiNote }}</p>
|
||||
<div v-loading="appointmentLinesLoading" class="yeji-unassigned-dialog__body">
|
||||
<el-table
|
||||
v-if="appointmentLinesRows.length > 0 || appointmentLinesLoading"
|
||||
:data="appointmentLinesRows"
|
||||
size="small"
|
||||
stripe
|
||||
border
|
||||
max-height="440"
|
||||
class="yeji-unassigned-dialog__table"
|
||||
:empty-text="appointmentLinesLoading ? '加载中…' : '暂无数据'"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="46" :index="appointmentLinesIndexMethod" />
|
||||
<el-table-column prop="appointment_date" label="预约日期" width="108" />
|
||||
<el-table-column prop="appointment_time" label="时段" min-width="96" show-overflow-tooltip />
|
||||
<el-table-column prop="status_desc" label="状态" width="84" />
|
||||
<el-table-column prop="appointment_type_desc" label="类型" width="92" />
|
||||
<el-table-column prop="patient_name" label="患者" min-width="92" show-overflow-tooltip />
|
||||
<el-table-column prop="patient_phone" label="手机" min-width="116" show-overflow-tooltip />
|
||||
<el-table-column prop="doctor_name" label="接诊医生" width="92" show-overflow-tooltip />
|
||||
<el-table-column label="诊单ID" width="84" align="right">
|
||||
<template #default="{ row }">{{ formatInt(row.diagnosis_id ?? 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="appointmentLinesShowChannelColumn"
|
||||
prop="channel_source"
|
||||
label="渠道来源"
|
||||
width="96"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
<el-empty
|
||||
v-if="!appointmentLinesLoading && appointmentLinesRows.length === 0"
|
||||
description="暂无挂号记录"
|
||||
/>
|
||||
<div v-if="appointmentLinesCount > 0" class="yeji-leadlines-dialog__pager">
|
||||
<el-pagination
|
||||
:current-page="appointmentLinesPage"
|
||||
:page-size="appointmentLinesPageSize"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="appointmentLinesCount"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:disabled="appointmentLinesLoading"
|
||||
:hide-on-single-page="false"
|
||||
@current-change="onAppointmentLinesPageChange"
|
||||
@size-change="onAppointmentLinesPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="revisitBreakdownVisible"
|
||||
width="min(560px, 94vw)"
|
||||
@@ -1168,6 +1267,7 @@ import {
|
||||
yejiStatsOverview,
|
||||
yejiStatsUnassignedBreakdown,
|
||||
yejiStatsLeadLines,
|
||||
yejiStatsAppointmentLines,
|
||||
yejiStatsRevisitBreakdown,
|
||||
} from '@/api/stats'
|
||||
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
|
||||
@@ -1284,6 +1384,8 @@ interface YejiRow {
|
||||
assign_count: number
|
||||
revisit_count?: number
|
||||
revisit_slots?: Record<string, number>
|
||||
/** 预约诊单(status 1/3/4),与医助排行榜「预约诊单」一致 */
|
||||
appointment_booked_count?: number
|
||||
consult_count: number
|
||||
deal_order_count: number
|
||||
channel_consult_count: number
|
||||
@@ -1308,6 +1410,7 @@ interface YejiTable {
|
||||
assign_count: number
|
||||
revisit_count?: number
|
||||
revisit_slots?: Record<string, number>
|
||||
appointment_booked_count?: number
|
||||
consult_count: number
|
||||
deal_order_count: number
|
||||
channel_consult_count: number
|
||||
@@ -1342,6 +1445,7 @@ interface LeaderboardPack {
|
||||
assign_count?: number
|
||||
consult_count: number
|
||||
deal_order_count: number
|
||||
appointment_count?: number
|
||||
consult_rate: number
|
||||
revisit_count?: number
|
||||
revisit_slots?: Record<string, number>
|
||||
@@ -1465,6 +1569,35 @@ const leadLinesPage = ref(1)
|
||||
const leadLinesPageSize = ref(20)
|
||||
const leadLinesContext = ref<{ tb: YejiTable; row: YejiRow } | null>(null)
|
||||
|
||||
const appointmentLinesDialogVisible = ref(false)
|
||||
const appointmentLinesLoading = ref(false)
|
||||
const appointmentLinesSubtitle = ref('')
|
||||
const appointmentLinesApiNote = ref('')
|
||||
const appointmentLinesRows = ref<Array<Record<string, unknown>>>([])
|
||||
const appointmentLinesCount = ref(0)
|
||||
const appointmentLinesPage = ref(1)
|
||||
const appointmentLinesPageSize = ref(20)
|
||||
type AppointmentLinesCtx =
|
||||
| {
|
||||
mode: 'assistant'
|
||||
assistant_id: number
|
||||
name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
| {
|
||||
mode: 'dept'
|
||||
dept_id: number
|
||||
dept_name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
const appointmentLinesContext = ref<AppointmentLinesCtx | null>(null)
|
||||
|
||||
const appointmentLinesShowChannelColumn = computed(() =>
|
||||
appointmentLinesRows.value.some(r => String(r.channel_source ?? '') !== '')
|
||||
)
|
||||
|
||||
/** 图表 / 数据表切换 */
|
||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt'>('zyyt')
|
||||
|
||||
@@ -2752,6 +2885,113 @@ function onLeadLinesPageSizeChange(size: number) {
|
||||
void fetchLeadLinesPage()
|
||||
}
|
||||
|
||||
function appointmentLinesIndexMethod(index: number) {
|
||||
return (appointmentLinesPage.value - 1) * appointmentLinesPageSize.value + index + 1
|
||||
}
|
||||
|
||||
/** 业绩主表中「预约诊单」列是否可点开明细(含未归属中心行 dept_id=0) */
|
||||
function yejiAppointmentBookedCellClickable(r: YejiRow): boolean {
|
||||
if (isUnassignedYejiRow(r)) {
|
||||
return true
|
||||
}
|
||||
return r.dept_id > 0
|
||||
}
|
||||
|
||||
async function fetchAppointmentLinesPage() {
|
||||
const ctx = appointmentLinesContext.value
|
||||
if (!ctx) {
|
||||
return
|
||||
}
|
||||
appointmentLinesLoading.value = true
|
||||
try {
|
||||
const p: Record<string, string | number> = {
|
||||
start_date: ctx.start_date,
|
||||
end_date: ctx.end_date,
|
||||
page: appointmentLinesPage.value,
|
||||
page_size: appointmentLinesPageSize.value,
|
||||
}
|
||||
if (ctx.mode === 'dept') {
|
||||
p.dept_id = ctx.dept_id
|
||||
} else {
|
||||
p.assistant_id = ctx.assistant_id
|
||||
}
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
p.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
if (selectedChannel.value) {
|
||||
p.channel_code = selectedChannel.value
|
||||
}
|
||||
const res: any = await yejiStatsAppointmentLines(p as any)
|
||||
appointmentLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
|
||||
appointmentLinesCount.value = Number(res?.count ?? 0)
|
||||
appointmentLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载挂号明细失败')
|
||||
appointmentLinesRows.value = []
|
||||
appointmentLinesCount.value = 0
|
||||
appointmentLinesApiNote.value = ''
|
||||
} finally {
|
||||
appointmentLinesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onLeaderboardAppointmentCellClick(r: LeaderboardPack['leaderboards'][0]['rows'][0]) {
|
||||
if (!r || r.admin_id <= 0 || !leaderboardBlock.value) {
|
||||
return
|
||||
}
|
||||
if (Number(r.appointment_count ?? 0) <= 0) {
|
||||
return
|
||||
}
|
||||
const lb = leaderboardBlock.value
|
||||
appointmentLinesContext.value = {
|
||||
mode: 'assistant',
|
||||
assistant_id: r.admin_id,
|
||||
name: r.name,
|
||||
start_date: lb.start_date,
|
||||
end_date: lb.end_date,
|
||||
}
|
||||
appointmentLinesSubtitle.value = `${r.name} · 医助 · ${lb.start_date} ~ ${lb.end_date}`
|
||||
appointmentLinesApiNote.value = ''
|
||||
appointmentLinesRows.value = []
|
||||
appointmentLinesCount.value = 0
|
||||
appointmentLinesPage.value = 1
|
||||
appointmentLinesPageSize.value = 20
|
||||
appointmentLinesDialogVisible.value = true
|
||||
void fetchAppointmentLinesPage()
|
||||
}
|
||||
|
||||
function onYejiDeptAppointmentBookedClick(tb: YejiTable, r: YejiRow) {
|
||||
if (!yejiAppointmentBookedCellClickable(r) || Number(r.appointment_booked_count ?? 0) <= 0) {
|
||||
return
|
||||
}
|
||||
appointmentLinesContext.value = {
|
||||
mode: 'dept',
|
||||
dept_id: r.dept_id,
|
||||
dept_name: r.dept_name,
|
||||
start_date: tb.start_date,
|
||||
end_date: tb.end_date,
|
||||
}
|
||||
appointmentLinesSubtitle.value = `${r.dept_name} · ${tb.start_date} ~ ${tb.end_date}`
|
||||
appointmentLinesApiNote.value = ''
|
||||
appointmentLinesRows.value = []
|
||||
appointmentLinesCount.value = 0
|
||||
appointmentLinesPage.value = 1
|
||||
appointmentLinesPageSize.value = 20
|
||||
appointmentLinesDialogVisible.value = true
|
||||
void fetchAppointmentLinesPage()
|
||||
}
|
||||
|
||||
function onAppointmentLinesPageChange(p: number) {
|
||||
appointmentLinesPage.value = p
|
||||
void fetchAppointmentLinesPage()
|
||||
}
|
||||
|
||||
function onAppointmentLinesPageSizeChange(size: number) {
|
||||
appointmentLinesPageSize.value = size
|
||||
appointmentLinesPage.value = 1
|
||||
void fetchAppointmentLinesPage()
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单;「未归属中心」→ 按诊单医助拆解弹窗
|
||||
*/
|
||||
@@ -3831,6 +4071,12 @@ onMounted(async () => {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.col-appointment {
|
||||
width: 88px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.leaderboard-empty {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@ use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
* - GET stats.yejiStats/deptOptions 部门下拉
|
||||
* - GET stats.yejiStats/leaderboard 医助排行榜(按展示部门分表)
|
||||
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
|
||||
* - GET stats.yejiStats/appointmentLines 医助排行榜「预约诊单」挂号逐条明细
|
||||
* - GET stats.yejiStats/revisitBreakdown 二中心复诊下钻:医助 × 业务订单笔数
|
||||
*/
|
||||
class YejiStatsController extends BaseAdminController
|
||||
@@ -123,6 +124,15 @@ class YejiStatsController extends BaseAdminController
|
||||
return $this->data(YejiStatsLogic::leadLineList($params, $this->adminId, $this->adminInfo));
|
||||
}
|
||||
|
||||
/** 医助排行榜「预约诊单」:挂号记录逐条列表(与计数同口径) */
|
||||
public function appointmentLines()
|
||||
{
|
||||
@set_time_limit(120);
|
||||
$params = $this->request->get();
|
||||
|
||||
return $this->data(YejiStatsLogic::leaderboardAppointmentLines($params, $this->adminId, $this->adminInfo));
|
||||
}
|
||||
|
||||
/** 二中心复诊:按部门行 + 复诊分项拆解医助与业务订单笔数 */
|
||||
public function revisitBreakdown()
|
||||
{
|
||||
|
||||
@@ -299,6 +299,14 @@ class YejiStatsLogic
|
||||
);
|
||||
$consults = $consultsBoth['all'];
|
||||
$channelConsults = $consultsBoth['scoped'];
|
||||
$bookedByDept = self::aggregateBookedAppointmentsByDept(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$adminToPrimary,
|
||||
$tagDiagIds,
|
||||
$tagAssistantIds,
|
||||
$dataScopeAdminIds
|
||||
);
|
||||
// 业绩:与 /tcm.prescriptionOrder/lists 同口径(仅剔除已取消=4,含 NULL)
|
||||
// - performance(全量) → "合计业绩" 列;不受所选渠道影响
|
||||
// - channelPerformance(渠道) → "{渠道}业绩" 列 + ROI 分子;未选渠道时与 performance 相同
|
||||
@@ -357,11 +365,13 @@ class YejiStatsLogic
|
||||
$totalCost = 0.0;
|
||||
$unmappedConsult = (int) ($consults[0] ?? 0);
|
||||
$consultSumInTable = 0;
|
||||
$bookedSumInTable = 0;
|
||||
|
||||
foreach ($tableRowDeptIds as $deptId) {
|
||||
$lead = (int) ($leads[$deptId] ?? 0);
|
||||
$cnt = (int) ($consults[$deptId] ?? 0);
|
||||
$consultSumInTable += $cnt;
|
||||
$bookedSumInTable += (int) ($bookedByDept[$deptId] ?? 0);
|
||||
$cCnt = (int) ($channelConsults[$deptId] ?? 0);
|
||||
// 合计业绩:部门下所有订单业绩(≠4),不限渠道
|
||||
$perf = round((float) ($performance[$deptId] ?? 0), 1);
|
||||
@@ -383,6 +393,7 @@ class YejiStatsLogic
|
||||
'assign_count' => $assignCnt,
|
||||
'revisit_count' => $rv,
|
||||
'revisit_slots' => $rvSlots,
|
||||
'appointment_booked_count' => (int) ($bookedByDept[$deptId] ?? 0),
|
||||
'consult_count' => $cnt,
|
||||
'deal_order_count' => $dDeal,
|
||||
// {渠道}成交单:选定渠道时与 completed_performance 同源;未选渠道时与 consult_count 相同
|
||||
@@ -428,6 +439,8 @@ class YejiStatsLogic
|
||||
foreach ($tableRowDeptIds as $rid) {
|
||||
$allocatedDealOrderSum += (int) ($dealOrderByDept[$rid] ?? 0);
|
||||
}
|
||||
$totalBooked = $narrowTotalsToTable ? $bookedSumInTable : array_sum($bookedByDept);
|
||||
|
||||
if ($narrowTotalsToTable) {
|
||||
$totalConsult = $consultSumInTable;
|
||||
$unmappedDealOrder = 0;
|
||||
@@ -451,6 +464,7 @@ class YejiStatsLogic
|
||||
'assign_count' => 0,
|
||||
'revisit_count' => 0,
|
||||
'revisit_slots' => [],
|
||||
'appointment_booked_count' => (int) ($bookedByDept[0] ?? 0),
|
||||
'consult_count' => $unmappedConsult,
|
||||
'deal_order_count' => $unmappedDealOrder,
|
||||
'channel_consult_count' => 0,
|
||||
@@ -511,6 +525,7 @@ class YejiStatsLogic
|
||||
'assign_count' => $totalAssign,
|
||||
'revisit_count' => $totalRevisit,
|
||||
'revisit_slots' => $totalRevisitSlots,
|
||||
'appointment_booked_count' => $totalBooked,
|
||||
'consult_count' => $totalConsult,
|
||||
'deal_order_count' => $totalDealOrderCount,
|
||||
'channel_consult_count' => $totalChannelConsult,
|
||||
@@ -631,6 +646,13 @@ class YejiStatsLogic
|
||||
$channelFilterActive,
|
||||
$dataScopeAdminIds
|
||||
);
|
||||
$appointmentByAdmin = self::countBookedAppointmentsByAdminForLeaderboard(
|
||||
$startTs,
|
||||
$endTs,
|
||||
$tagDiagIds,
|
||||
$tagFallback ? $tagAssistantIds : null,
|
||||
$dataScopeAdminIds
|
||||
);
|
||||
|
||||
if ($channelFilterActive) {
|
||||
self::excludeErCenterFromAdminLeaderboard(
|
||||
@@ -671,8 +693,10 @@ class YejiStatsLogic
|
||||
|
||||
$rangeNote = $channelFilterActive
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩口径(与部门表该渠道业绩列一致)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
. '「二中心」医助在该渠道下诊金、进线与被指派计 0。')
|
||||
: '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。';
|
||||
. '「二中心」医助在该渠道下诊金、进线与被指派计 0。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。')
|
||||
: '排行诊金 = 处方订单列表 assistant_id 口径(诊单医助业绩,与列表按医助筛选一致)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。';
|
||||
|
||||
$leaderboards = [];
|
||||
foreach ($tableRowDeptIds as $deptId) {
|
||||
@@ -690,18 +714,9 @@ class YejiStatsLogic
|
||||
$consult = (int) ($consultsByAdmin[$aid] ?? 0);
|
||||
$dealOrders = (int) ($dealOrderByAdmin[$aid] ?? 0);
|
||||
$assignCnt = (int) ($assignsByAdmin[$aid] ?? 0);
|
||||
$apptCnt = (int) ($appointmentByAdmin[$aid] ?? 0);
|
||||
$rv = $isErCenterLb ? (int) ($revisitByAssistant['totals'][$aid] ?? 0) : 0;
|
||||
$rvSlots = $isErCenterLb ? ($revisitByAssistant['slots'][$aid] ?? []) : [];
|
||||
if (
|
||||
$fee < 0.005
|
||||
&& $lead === 0
|
||||
&& $consult === 0
|
||||
&& $dealOrders === 0
|
||||
&& $assignCnt === 0
|
||||
&& $rv === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$rawFee = (float) ($feeByAdmin[$aid] ?? 0.0);
|
||||
$rate = $lead > 0 ? round($rawFee / $lead, 1) : 0.0;
|
||||
$row = [
|
||||
@@ -712,6 +727,7 @@ class YejiStatsLogic
|
||||
'assign_count' => $assignCnt,
|
||||
'consult_count' => $consult,
|
||||
'deal_order_count' => $dealOrders,
|
||||
'appointment_count' => $apptCnt,
|
||||
'consult_rate' => $rate,
|
||||
'dept_abbr' => $deptAbbr,
|
||||
];
|
||||
@@ -731,7 +747,6 @@ class YejiStatsLogic
|
||||
foreach ($rows as $i => $row) {
|
||||
$rows[$i]['rank'] = $i + 1;
|
||||
}
|
||||
$rows = array_slice($rows, 0, 50);
|
||||
|
||||
$leaderboards[] = [
|
||||
'dept_id' => $deptId,
|
||||
@@ -1588,6 +1603,99 @@ class YejiStatsLogic
|
||||
return $byDept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩表「预约诊单」列:status∈{1,3,4};部门归属规则与 {@see aggregateConsults} 一致。
|
||||
*
|
||||
* @return array<int, int> 展示部门 id => 条数(键 0 为无法归入展示「中心」)
|
||||
*/
|
||||
private static function aggregateBookedAppointmentsByDept(
|
||||
int $startTs,
|
||||
int $endTs,
|
||||
array $adminToPrimary,
|
||||
?array $tagDiagIds,
|
||||
?array $tagAssistantIds,
|
||||
?array $dataScopeAdminIds = null
|
||||
): array {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
return [];
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$startDate = date('Y-m-d', $startTs);
|
||||
$endDate = date('Y-m-d', $endTs);
|
||||
$eff = self::consultEffectiveAssistantSql();
|
||||
|
||||
$query = Db::name('doctor_appointment')->alias('a')
|
||||
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
||||
->whereBetween('a.appointment_date', [$startDate, $endDate])
|
||||
->whereIn('a.status', [1, 3, 4])
|
||||
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
|
||||
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('a.patient_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$ids = array_map('intval', array_keys($tagAssistantIds));
|
||||
$query->whereRaw("({$eff}) IN (" . implode(',', $ids) . ')');
|
||||
}
|
||||
|
||||
$query
|
||||
->field([
|
||||
Db::raw("({$eff}) AS eff_a"),
|
||||
'a.doctor_id',
|
||||
Db::raw('COUNT(*) AS cnt'),
|
||||
])
|
||||
->group([$eff, 'a.doctor_id']);
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$visFlip = $dataScopeAdminIds !== null && $dataScopeAdminIds !== []
|
||||
? array_flip($dataScopeAdminIds)
|
||||
: null;
|
||||
|
||||
$byDept = [];
|
||||
foreach ($rows as $r) {
|
||||
$effId = (int) ($r['eff_a'] ?? 0);
|
||||
$docId = (int) ($r['doctor_id'] ?? 0);
|
||||
$cnt = (int) ($r['cnt'] ?? 0);
|
||||
if ($cnt <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($visFlip !== null) {
|
||||
if ($effId > 0) {
|
||||
if (!isset($visFlip[$effId])) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($docId > 0) {
|
||||
if (!isset($visFlip[$docId])) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$primary = 0;
|
||||
if ($effId > 0) {
|
||||
$primary = (int) ($adminToPrimary[$effId] ?? 0);
|
||||
}
|
||||
if ($primary <= 0 && $docId > 0) {
|
||||
$primary = (int) ($adminToPrimary[$docId] ?? 0);
|
||||
}
|
||||
if ($primary > 0) {
|
||||
$byDept[$primary] = ($byDept[$primary] ?? 0) + $cnt;
|
||||
} else {
|
||||
$byDept[0] = ($byDept[0] ?? 0) + $cnt;
|
||||
}
|
||||
}
|
||||
|
||||
return $byDept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接诊按医助汇总(不按部门归并)。
|
||||
*
|
||||
@@ -1646,6 +1754,72 @@ class YejiStatsLogic
|
||||
return 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))';
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助排行榜「预约诊单」:`doctor_appointment.appointment_date` 落在统计区间内;
|
||||
* 状态为已预约(1)、已完成(3)、已过号(4),不含已取消(2);
|
||||
* 有效医助与 {@see consultEffectiveAssistantSql()} 一致。
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private static function countBookedAppointmentsByAdminForLeaderboard(
|
||||
int $startTs,
|
||||
int $endTs,
|
||||
?array $tagDiagIds,
|
||||
?array $tagAssistantIds,
|
||||
?array $dataScopeAdminIds
|
||||
): array {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
return [];
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$startDate = date('Y-m-d', $startTs);
|
||||
$endDate = date('Y-m-d', $endTs);
|
||||
$eff = self::consultEffectiveAssistantSql();
|
||||
|
||||
$query = Db::name('doctor_appointment')->alias('a')
|
||||
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
||||
->whereBetween('a.appointment_date', [$startDate, $endDate])
|
||||
->whereIn('a.status', [1, 3, 4])
|
||||
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
|
||||
|
||||
if ($tagDiagIds !== null) {
|
||||
$query->whereIn('a.patient_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$ids = array_map('intval', array_keys($tagAssistantIds));
|
||||
$query->whereRaw("({$eff}) IN (" . implode(',', $ids) . ')');
|
||||
}
|
||||
|
||||
$query->field([Db::raw("({$eff}) AS eff_a"), Db::raw('COUNT(*) AS cnt')])
|
||||
->group($eff);
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$visFlip = $dataScopeAdminIds !== null && $dataScopeAdminIds !== []
|
||||
? array_flip($dataScopeAdminIds)
|
||||
: null;
|
||||
|
||||
$byAdmin = [];
|
||||
foreach ($rows as $r) {
|
||||
$aid = (int) ($r['eff_a'] ?? 0);
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($visFlip !== null && !isset($visFlip[$aid])) {
|
||||
continue;
|
||||
}
|
||||
$byAdmin[$aid] = ($byAdmin[$aid] ?? 0) + (int) ($r['cnt'] ?? 0);
|
||||
}
|
||||
|
||||
return $byAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* doctor_appointment 别名 a:与 AppointmentLists 已完成挂号 **计数** 对齐(诊单软删、appointment_date、status=3)。
|
||||
* 不按医助角色或 assistant_id 再收窄(与列表 tab 总数一致)。
|
||||
@@ -1907,6 +2081,401 @@ class YejiStatsLogic
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* admin → 展示「中心」映射:用于挂号明细按部门筛选(与 aggregateConsults 映射同源)。
|
||||
*/
|
||||
private static function buildYejiAdminPrimaryMapUnionSql(array $adminToPrimary): string
|
||||
{
|
||||
if ($adminToPrimary === []) {
|
||||
return 'SELECT 0 AS map_aid, 0 AS map_pid WHERE 0 = 1';
|
||||
}
|
||||
$parts = [];
|
||||
foreach ($adminToPrimary as $aid => $pid) {
|
||||
$parts[] = 'SELECT ' . (int) $aid . ' AS map_aid,' . (int) $pid . ' AS map_pid';
|
||||
}
|
||||
|
||||
return implode(' UNION ALL ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private static function formatYejiAppointmentLinePayloadRows(array $rawList, bool $hasChSrc): array
|
||||
{
|
||||
$statusMap = [
|
||||
1 => '已预约',
|
||||
2 => '已取消',
|
||||
3 => '已完成',
|
||||
4 => '已过号',
|
||||
];
|
||||
$typeMap = [
|
||||
'video' => '视频问诊',
|
||||
'text' => '图文问诊',
|
||||
'phone' => '电话问诊',
|
||||
];
|
||||
$lists = [];
|
||||
foreach ($rawList as $rw) {
|
||||
$st = (int) ($rw['status'] ?? 0);
|
||||
$tp = (string) ($rw['appointment_type'] ?? '');
|
||||
$lists[] = [
|
||||
'id' => (int) ($rw['id'] ?? 0),
|
||||
'appointment_date' => (string) ($rw['appointment_date'] ?? ''),
|
||||
'appointment_time' => (string) ($rw['appointment_time'] ?? ''),
|
||||
'status' => $st,
|
||||
'status_desc' => $statusMap[$st] ?? '未知',
|
||||
'appointment_type' => $tp,
|
||||
'appointment_type_desc' => $typeMap[$tp] ?? '未知',
|
||||
'diagnosis_id' => (int) ($rw['patient_id'] ?? 0),
|
||||
'patient_name' => (string) ($rw['patient_name'] ?? ''),
|
||||
'patient_phone' => (string) ($rw['patient_phone'] ?? ''),
|
||||
'doctor_name' => (string) ($rw['doctor_name'] ?? ''),
|
||||
'channel_source' => $hasChSrc ? (string) ($rw['channel_source'] ?? '') : '',
|
||||
];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩表部门行「预约诊单」挂号明细(与 {@see aggregateBookedAppointmentsByDept} 同口径)。
|
||||
*
|
||||
* @param array<string, mixed> $c resolveYejiContext + applyYejiDataScope 后的上下文
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function yejiDeptAppointmentLinesInner(array $params, array $c): array
|
||||
{
|
||||
$deptIdParam = (int) ($params['dept_id'] ?? 0);
|
||||
$channelCode = (string) $c['channelCode'];
|
||||
$deptName = $deptIdParam === 0
|
||||
? '未归属中心'
|
||||
: self::formatYejiDeptRowDisplayName($deptIdParam, $c['deptById']);
|
||||
|
||||
$empty = static function (string $note) use ($c, $channelCode, $deptIdParam, $deptName): array {
|
||||
return [
|
||||
'start_date' => (string) $c['startDate'],
|
||||
'end_date' => (string) $c['endDate'],
|
||||
'assistant_id' => 0,
|
||||
'assistant_name' => '',
|
||||
'dept_id' => $deptIdParam,
|
||||
'dept_name' => $deptName,
|
||||
'channel_code' => $channelCode,
|
||||
'count' => 0,
|
||||
'lists' => [],
|
||||
'note' => $note,
|
||||
];
|
||||
};
|
||||
|
||||
if ($deptIdParam < 0) {
|
||||
return $empty('请指定有效部门。');
|
||||
}
|
||||
if ($deptIdParam > 0 && !in_array($deptIdParam, $c['tableRowDeptIds'], true)) {
|
||||
return $empty('该部门不在当前展示部门筛选或数据权限范围内。');
|
||||
}
|
||||
|
||||
$tagDiagIds = $c['tagDiagIds'];
|
||||
$tagAssistantIds = $c['tagAssistantIds'];
|
||||
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
return $empty('当前渠道标签下无关联诊单,无挂号明细。');
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||
return $empty('当前渠道标签下无关联医助,无挂号明细。');
|
||||
}
|
||||
|
||||
$startTs = (int) $c['startTs'];
|
||||
$endTs = (int) $c['endTs'];
|
||||
$startDate = date('Y-m-d', $startTs);
|
||||
$endDate = date('Y-m-d', $endTs);
|
||||
$eff = self::consultEffectiveAssistantSql();
|
||||
|
||||
$page = max(1, (int) ($params['page'] ?? 1));
|
||||
$limit = min(100, max(1, (int) ($params['page_size'] ?? 20)));
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$hasChSrc = self::doctorAppointmentHasChannelSourceColumn();
|
||||
|
||||
$mapWrapped = '(' . self::buildYejiAdminPrimaryMapUnionSql($c['adminToPrimary']) . ')';
|
||||
|
||||
$dataScopeRestricted = !empty($c['dataScopeRestricted']);
|
||||
$dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
|
||||
? $c['dataScopeVisibleAdminIds']
|
||||
: null;
|
||||
|
||||
$buildBaseQuery = static function () use (
|
||||
$eff,
|
||||
$startDate,
|
||||
$endDate,
|
||||
$tagDiagIds,
|
||||
$tagAssistantIds,
|
||||
$mapWrapped,
|
||||
$deptIdParam,
|
||||
$dataScopeAdminIds
|
||||
) {
|
||||
$q = Db::name('doctor_appointment')->alias('a')
|
||||
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
||||
->leftJoin('admin doc', 'a.doctor_id = doc.id AND doc.delete_time IS NULL')
|
||||
->whereBetween('a.appointment_date', [$startDate, $endDate])
|
||||
->whereIn('a.status', [1, 3, 4])
|
||||
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)');
|
||||
|
||||
if ($tagDiagIds !== null) {
|
||||
$q->whereIn('a.patient_id', $tagDiagIds);
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$ids = array_map('intval', array_keys($tagAssistantIds));
|
||||
if ($ids !== []) {
|
||||
$q->whereRaw("({$eff}) IN (" . implode(',', $ids) . ')');
|
||||
}
|
||||
}
|
||||
|
||||
$primaryPred = 'COALESCE('
|
||||
. "IF(({$eff}) > 0, (SELECT m.map_pid FROM {$mapWrapped} m WHERE m.map_aid = ({$eff}) LIMIT 1), NULL),"
|
||||
. " IF(a.doctor_id > 0, (SELECT m2.map_pid FROM {$mapWrapped} m2 WHERE m2.map_aid = a.doctor_id LIMIT 1), NULL),"
|
||||
. ' 0)';
|
||||
$q->whereRaw("{$primaryPred} = ?", [$deptIdParam]);
|
||||
|
||||
if ($dataScopeAdminIds !== null && $dataScopeAdminIds !== []) {
|
||||
$visIn = implode(',', array_map('intval', $dataScopeAdminIds));
|
||||
$q->whereRaw(
|
||||
"(({$eff}) > 0 AND ({$eff}) IN ({$visIn})) OR (({$eff}) <= 0 AND a.doctor_id > 0 AND a.doctor_id IN ({$visIn}))"
|
||||
);
|
||||
}
|
||||
|
||||
return $q;
|
||||
};
|
||||
|
||||
$count = (int) $buildBaseQuery()->count();
|
||||
|
||||
$fieldParts = [
|
||||
'a.id',
|
||||
'a.appointment_date',
|
||||
'a.appointment_time',
|
||||
'a.status',
|
||||
'a.appointment_type',
|
||||
'a.patient_id',
|
||||
'u.patient_name',
|
||||
'u.phone AS patient_phone',
|
||||
'doc.name AS doctor_name',
|
||||
];
|
||||
if ($hasChSrc) {
|
||||
$fieldParts[] = 'a.channel_source';
|
||||
}
|
||||
|
||||
$rawList = $buildBaseQuery()
|
||||
->field(implode(',', $fieldParts))
|
||||
->order('a.appointment_date', 'asc')
|
||||
->order('a.appointment_time', 'asc')
|
||||
->order('a.id', 'asc')
|
||||
->limit($offset, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$lists = self::formatYejiAppointmentLinePayloadRows($rawList, $hasChSrc);
|
||||
|
||||
$note = '与业绩表「预约诊单」列同口径:预约日期在统计区间内,状态为已预约 / 已完成 / 已过号(不含已取消);部门归属医助优先,否则接诊医生。';
|
||||
|
||||
return [
|
||||
'start_date' => (string) $c['startDate'],
|
||||
'end_date' => (string) $c['endDate'],
|
||||
'assistant_id' => 0,
|
||||
'assistant_name' => '',
|
||||
'dept_id' => $deptIdParam,
|
||||
'dept_name' => $deptName,
|
||||
'channel_code' => $channelCode,
|
||||
'count' => $count,
|
||||
'lists' => $lists,
|
||||
'note' => $note,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助排行榜「预约诊单」逐条挂号明细(与 {@see countBookedAppointmentsByAdminForLeaderboard} 同口径)。
|
||||
*
|
||||
* @param array{
|
||||
* start_date?: string,
|
||||
* end_date?: string,
|
||||
* dept_ids?: int[]|string,
|
||||
* channel_code?: string,
|
||||
* tag_id?: string,
|
||||
* assistant_id?: int|string,
|
||||
* admin_id?: int|string,
|
||||
* dept_id?: int|string,
|
||||
* page?: int|string,
|
||||
* page_size?: int|string
|
||||
* } $params
|
||||
*
|
||||
* @return array{
|
||||
* start_date: string,
|
||||
* end_date: string,
|
||||
* assistant_id: int,
|
||||
* assistant_name: string,
|
||||
* dept_id: int,
|
||||
* dept_name: string,
|
||||
* channel_code: string,
|
||||
* count: int,
|
||||
* lists: list<array<string, mixed>>,
|
||||
* note: string
|
||||
* }
|
||||
*/
|
||||
public static function leaderboardAppointmentLines(
|
||||
array $params,
|
||||
int $viewerAdminId = 0,
|
||||
array $viewerAdminInfo = []
|
||||
): array {
|
||||
$c = self::resolveYejiContext($params);
|
||||
if ($viewerAdminId > 0) {
|
||||
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
|
||||
}
|
||||
|
||||
$assistantId = (int) ($params['assistant_id'] ?? $params['admin_id'] ?? 0);
|
||||
|
||||
if ($assistantId <= 0 && array_key_exists('dept_id', $params)) {
|
||||
return self::yejiDeptAppointmentLinesInner($params, $c);
|
||||
}
|
||||
|
||||
$channelCode = (string) $c['channelCode'];
|
||||
|
||||
$assistantName = '';
|
||||
if ($assistantId > 0) {
|
||||
$assistantName = (string) (Db::name('admin')->where('id', $assistantId)->whereNull('delete_time')->value('name') ?: '');
|
||||
}
|
||||
|
||||
$baseOut = static function (array $c, string $note) use ($assistantId, $assistantName, $channelCode): array {
|
||||
return [
|
||||
'start_date' => (string) $c['startDate'],
|
||||
'end_date' => (string) $c['endDate'],
|
||||
'assistant_id' => $assistantId,
|
||||
'assistant_name' => $assistantName,
|
||||
'dept_id' => 0,
|
||||
'dept_name' => '',
|
||||
'channel_code' => $channelCode,
|
||||
'count' => 0,
|
||||
'lists' => [],
|
||||
'note' => $note,
|
||||
];
|
||||
};
|
||||
|
||||
if ($assistantId <= 0) {
|
||||
return $baseOut($c, '请指定有效医助,或传入 dept_id 查看部门汇总明细。');
|
||||
}
|
||||
|
||||
$dataScopeRestricted = !empty($c['dataScopeRestricted']);
|
||||
$dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
|
||||
? $c['dataScopeVisibleAdminIds']
|
||||
: null;
|
||||
if ($dataScopeRestricted && $dataScopeAdminIds !== null) {
|
||||
$flip = array_flip($dataScopeAdminIds);
|
||||
if (!isset($flip[$assistantId])) {
|
||||
return $baseOut($c, '当前账号数据权限下不可查看该医助挂号明细。');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($c['adminToPrimary'][$assistantId])) {
|
||||
return $baseOut($c, '该医助不在当前业绩看板展示范围内。');
|
||||
}
|
||||
|
||||
$tagDiagIds = $c['tagDiagIds'];
|
||||
$tagAssistantIds = $c['tagAssistantIds'];
|
||||
$tagFallback = $c['tagFallback'];
|
||||
$scopedAssistants = $tagFallback ? $tagAssistantIds : null;
|
||||
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
return $baseOut($c, '当前渠道标签下无关联诊单,无挂号明细。');
|
||||
}
|
||||
if ($scopedAssistants !== null && $scopedAssistants === []) {
|
||||
return $baseOut($c, '当前渠道标签下无关联医助,无挂号明细。');
|
||||
}
|
||||
if ($scopedAssistants !== null && !isset($scopedAssistants[$assistantId])) {
|
||||
return $baseOut($c, '当前渠道标签口径下该医助无归属挂号(与排行榜「预约诊单」一致)。');
|
||||
}
|
||||
|
||||
$startTs = (int) $c['startTs'];
|
||||
$endTs = (int) $c['endTs'];
|
||||
$startDate = date('Y-m-d', $startTs);
|
||||
$endDate = date('Y-m-d', $endTs);
|
||||
$eff = self::consultEffectiveAssistantSql();
|
||||
|
||||
$page = max(1, (int) ($params['page'] ?? 1));
|
||||
$limit = min(100, max(1, (int) ($params['page_size'] ?? 20)));
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$hasChSrc = self::doctorAppointmentHasChannelSourceColumn();
|
||||
|
||||
$buildBaseQuery = static function () use (
|
||||
$eff,
|
||||
$assistantId,
|
||||
$startDate,
|
||||
$endDate,
|
||||
$tagDiagIds,
|
||||
$scopedAssistants
|
||||
) {
|
||||
$q = Db::name('doctor_appointment')->alias('a')
|
||||
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
||||
->leftJoin('admin doc', 'a.doctor_id = doc.id AND doc.delete_time IS NULL')
|
||||
->whereBetween('a.appointment_date', [$startDate, $endDate])
|
||||
->whereIn('a.status', [1, 3, 4])
|
||||
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)')
|
||||
->whereRaw("({$eff}) = ?", [$assistantId]);
|
||||
|
||||
if ($tagDiagIds !== null) {
|
||||
$q->whereIn('a.patient_id', $tagDiagIds);
|
||||
}
|
||||
if ($scopedAssistants !== null) {
|
||||
$ids = array_map('intval', array_keys($scopedAssistants));
|
||||
if ($ids !== []) {
|
||||
$q->whereRaw("({$eff}) IN (" . implode(',', $ids) . ')');
|
||||
}
|
||||
}
|
||||
|
||||
return $q;
|
||||
};
|
||||
|
||||
$count = (int) $buildBaseQuery()->count();
|
||||
|
||||
$fieldParts = [
|
||||
'a.id',
|
||||
'a.appointment_date',
|
||||
'a.appointment_time',
|
||||
'a.status',
|
||||
'a.appointment_type',
|
||||
'a.patient_id',
|
||||
'u.patient_name',
|
||||
'u.phone AS patient_phone',
|
||||
'doc.name AS doctor_name',
|
||||
];
|
||||
if ($hasChSrc) {
|
||||
$fieldParts[] = 'a.channel_source';
|
||||
}
|
||||
|
||||
$rawList = $buildBaseQuery()
|
||||
->field(implode(',', $fieldParts))
|
||||
->order('a.appointment_date', 'asc')
|
||||
->order('a.appointment_time', 'asc')
|
||||
->order('a.id', 'asc')
|
||||
->limit($offset, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$lists = self::formatYejiAppointmentLinePayloadRows($rawList, $hasChSrc);
|
||||
|
||||
$note = '与排行榜「预约诊单」同口径:预约日期在统计区间内,状态为已预约 / 已完成 / 已过号(不含已取消);'
|
||||
. '有效医助优先取挂号表创建医助,否则取诊单医助。';
|
||||
|
||||
return [
|
||||
'start_date' => (string) $c['startDate'],
|
||||
'end_date' => (string) $c['endDate'],
|
||||
'assistant_id' => $assistantId,
|
||||
'assistant_name' => $assistantName,
|
||||
'dept_id' => 0,
|
||||
'dept_name' => '',
|
||||
'channel_code' => $channelCode,
|
||||
'count' => $count,
|
||||
'lists' => $lists,
|
||||
'note' => $note,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩看板:二中心复诊按部门行下钻 —— 医助姓名与业务订单笔数(与表格「复诊」列同口径)。
|
||||
*
|
||||
|
||||
@@ -545,6 +545,10 @@ class DiagnosisLogic extends BaseLogic
|
||||
if (!empty($r['to_assistant_id'])) {
|
||||
$adminIds[] = (int) $r['to_assistant_id'];
|
||||
}
|
||||
$rcp = (int) ($r['related_po_creator_id'] ?? 0);
|
||||
if ($rcp > 0) {
|
||||
$adminIds[] = $rcp;
|
||||
}
|
||||
}
|
||||
$adminIds = array_values(array_unique(array_filter($adminIds)));
|
||||
$nameMap = [];
|
||||
@@ -564,6 +568,14 @@ class DiagnosisLogic extends BaseLogic
|
||||
$r['create_time_text'] = !empty($r['create_time'])
|
||||
? date('Y-m-d H:i:s', (int) $r['create_time'])
|
||||
: '';
|
||||
$rcpId = (int) ($r['related_po_creator_id'] ?? 0);
|
||||
$r['related_po_creator_name'] = $rcpId > 0
|
||||
? (string) ($nameMap[$rcpId] ?? ('ID:' . $rcpId))
|
||||
: '';
|
||||
$rpct = (int) ($r['related_po_create_time'] ?? 0);
|
||||
$r['related_po_create_time_text'] = $rpct > 0 ? date('Y-m-d H:i:s', $rpct) : '';
|
||||
$r['related_po_is_cleared_assistant_creator'] =
|
||||
($rcpId > 0 && $fromId > 0 && $rcpId === $fromId) ? 1 : 0;
|
||||
}
|
||||
unset($r);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class ExpressAutoUpdate extends Command
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$result = ExpressTrackingService::autoUpdateBatch(200);
|
||||
$result = ExpressTrackingService::autoUpdateBatch(500);
|
||||
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(200);
|
||||
|
||||
$duration = round(microtime(true) - $startTime, 2);
|
||||
|
||||
@@ -246,7 +246,7 @@ class ExpressTrackingService
|
||||
}
|
||||
|
||||
/**
|
||||
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log。
|
||||
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。
|
||||
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
|
||||
* 例外:指派日志中若存在「从某医助改派到另一医助」(原、新医助 ID 均大于 0)的记录,视为人工指派链,自动释放跳过。
|
||||
* 例外:患者当前医助在「二中心」及其全部子部门(与后台部门树一致)下任职时,不自动清空医助(按 admin_dept 与 dept 树判定)。
|
||||
@@ -323,19 +323,24 @@ class ExpressTrackingService
|
||||
default => '系统·订单已发货/签收',
|
||||
};
|
||||
|
||||
$relatedPoCreatorId = (int) ($order->getAttr('creator_id') ?? 0);
|
||||
$relatedPoCreateTime = (int) ($order->getAttr('create_time') ?? 0);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->update(['assistant_id' => 0]);
|
||||
|
||||
DiagnosisAssignLog::create([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'from_assistant_id' => $fromAssistantId,
|
||||
'to_assistant_id' => 0,
|
||||
'operator_admin_id' => 0,
|
||||
'operator_name' => $operatorName,
|
||||
'operator_account' => '',
|
||||
'ip' => '',
|
||||
'create_time' => time(),
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'from_assistant_id' => $fromAssistantId,
|
||||
'to_assistant_id' => 0,
|
||||
'operator_admin_id' => 0,
|
||||
'operator_name' => $operatorName,
|
||||
'operator_account' => '',
|
||||
'ip' => '',
|
||||
'related_po_creator_id' => $relatedPoCreatorId,
|
||||
'related_po_create_time' => $relatedPoCreateTime,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
@@ -345,12 +350,15 @@ class ExpressTrackingService
|
||||
}
|
||||
|
||||
Log::info('Cleared diagnosis assistant (prescription shipped/signed), assign log written', [
|
||||
'source' => $source,
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'prescription_order_id' => $orderId,
|
||||
'tracking_id' => $trackingId,
|
||||
'fulfillment_status' => $fs,
|
||||
'former_assistant_id' => $fromAssistantId,
|
||||
'source' => $source,
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'prescription_order_id' => $orderId,
|
||||
'tracking_id' => $trackingId,
|
||||
'fulfillment_status' => $fs,
|
||||
'former_assistant_id' => $fromAssistantId,
|
||||
'related_po_creator_id' => $relatedPoCreatorId,
|
||||
'related_po_create_time' => $relatedPoCreateTime,
|
||||
'assistant_created_order' => ($relatedPoCreatorId === $fromAssistantId && $relatedPoCreateTime > 0) ? 1 : 0,
|
||||
]);
|
||||
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 自动释放诊单医助(已发货/已签收)时,在指派日志中快照关联业务订单创建人与创建时间
|
||||
-- 首次执行即可;若列已存在重复执行会报错,可忽略或手动注释掉本文件
|
||||
|
||||
ALTER TABLE `zyt_tcm_diagnosis_assign_log`
|
||||
ADD COLUMN `related_po_creator_id` int unsigned NOT NULL DEFAULT 0 COMMENT '快照:触发本次释放的业务订单 creator_id' AFTER `ip`,
|
||||
ADD COLUMN `related_po_create_time` int unsigned NOT NULL DEFAULT 0 COMMENT '快照:触发本次释放的业务订单 create_time(unix);与 from_assistant_id 一致时为医助创建订单时间' AFTER `related_po_creator_id`;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 业绩看板「预约诊单」挂号明细接口(fans/yeji · pid=293)
|
||||
-- 幂等:按 perms 判重
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
293, 'A', '业绩-预约挂号明细', '', 12,
|
||||
'stats.yejiStats/appointmentLines', '', '', '', '',
|
||||
1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` p WHERE p.id = 293)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.perms = 'stats.yejiStats/appointmentLines'
|
||||
);
|
||||
Reference in New Issue
Block a user