diff --git a/admin/src/api/stats.ts b/admin/src/api/stats.ts
index ac7a4b02..b421914d 100644
--- a/admin/src/api/stats.ts
+++ b/admin/src/api/stats.ts
@@ -96,6 +96,21 @@ export function yejiStatsRevisitBreakdown(params: {
return request.get({ url: '/stats.yejiStats/revisitBreakdown', params })
}
+/** 被指派数明细:与看板「被指派数」同口径;部门行传 dept_id,医助排行榜传 assistant_id */
+export function yejiStatsAssignLines(params: {
+ start_date: string
+ end_date: string
+ dept_id?: number
+ assistant_id?: number
+ dept_ids?: string
+ channel_code?: string
+ tag_id?: string
+ page?: number
+ page_size?: number
+}) {
+ return request.get({ url: '/stats.yejiStats/assignLines', params })
+}
+
export function doctorDailyStatsOverview(params: {
start_date?: string
end_date?: string
diff --git a/admin/src/views/fans/yeji.vue b/admin/src/views/fans/yeji.vue
index 5fa14b08..ef971032 100644
--- a/admin/src/views/fans/yeji.vue
+++ b/admin/src/views/fans/yeji.vue
@@ -283,7 +283,15 @@
{{ r.name }} |
{{ formatLeaderboardInt(r.consult_count) }} |
{{ formatLeaderboardInt(r.deal_order_count) }} |
- {{ formatLeaderboardInt(r.assign_count ?? 0) }} |
+
+ {{ formatLeaderboardInt(r.assign_count ?? 0) }}
+ {{
+ formatLeaderboardInt(r.assign_count ?? 0)
+ }}
+ |
{{ formatInt(r.lead_count) }}
{{ formatInt(r.lead_count) }}
|
- {{ formatInt(r.assign_count ?? 0) }} |
+
+ {{ formatInt(r.assign_count ?? 0) }}
+ {{ formatInt(r.assign_count ?? 0) }}
+ |
+
+
+
+
+ 被指派明细
+ {{ assignLinesSubtitle }}
+
+
+ {{ assignLinesApiNote }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1376,6 +1443,7 @@ import {
yejiStatsLeadLines,
yejiStatsAppointmentLines,
yejiStatsRevisitBreakdown,
+ yejiStatsAssignLines,
} from '@/api/stats'
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
import { prescriptionOrderLists } from '@/api/tcm'
@@ -1491,6 +1559,17 @@ interface YejiLeadLineRow {
external_contact_name: string
}
+interface YejiAssignLineRow {
+ diagnosis_id: number
+ patient_name: string
+ patient_phone: string
+ assistant_id: number
+ assistant_name: string
+ assign_count: number
+ last_assign_time: number
+ last_assign_time_text: string
+}
+
interface YejiRow {
dept_id: number
dept_name: string
@@ -1709,6 +1788,31 @@ type AppointmentLinesCtx =
}
const appointmentLinesContext = ref(null)
+const assignLinesDialogVisible = ref(false)
+const assignLinesLoading = ref(false)
+const assignLinesSubtitle = ref('')
+const assignLinesApiNote = ref('')
+const assignLinesRows = ref([])
+const assignLinesCount = ref(0)
+const assignLinesPage = ref(1)
+const assignLinesPageSize = ref(20)
+type AssignLinesCtx =
+ | {
+ 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 assignLinesContext = ref(null)
+
const appointmentLinesShowChannelColumn = computed(() =>
appointmentLinesRows.value.some(r => String(r.channel_source ?? '') !== '')
)
@@ -3193,6 +3297,124 @@ function onLeadCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) {
void openLeadLinesDialog(tb, row)
}
+function yejiAssignCountCellClickable(r: YejiRow): boolean {
+ return r.dept_id > 0 && Number(r.assign_count ?? 0) > 0
+}
+
+function onAssignCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) {
+ if (!yejiAssignCountCellClickable(row)) {
+ return
+ }
+ ev.stopPropagation()
+ void openAssignLinesDialogForDept(tb, row)
+}
+
+function assignLinesIndexMethod(index: number) {
+ return (assignLinesPage.value - 1) * assignLinesPageSize.value + index + 1
+}
+
+function buildAssignLinesRequestParams(page: number, pageSize: number): Record | null {
+ const ctx = assignLinesContext.value
+ if (!ctx) {
+ return null
+ }
+ const p: Record = {
+ start_date: ctx.start_date,
+ end_date: ctx.end_date,
+ page,
+ page_size: pageSize,
+ }
+ 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
+ }
+ return p
+}
+
+async function fetchAssignLinesPage() {
+ const params = buildAssignLinesRequestParams(assignLinesPage.value, assignLinesPageSize.value)
+ if (!params) {
+ return
+ }
+ assignLinesLoading.value = true
+ try {
+ const res: any = await yejiStatsAssignLines(params as any)
+ assignLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
+ assignLinesCount.value = Number(res?.count ?? 0)
+ assignLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
+ } catch (e: unknown) {
+ if (axios.isCancel(e)) return
+ const any = e as any
+ ElMessage.error(any?.msg || any?.message || '加载被指派明细失败')
+ assignLinesRows.value = []
+ assignLinesCount.value = 0
+ assignLinesApiNote.value = ''
+ } finally {
+ assignLinesLoading.value = false
+ }
+}
+
+async function openAssignLinesDialogForDept(tb: YejiTable, row: YejiRow) {
+ assignLinesContext.value = {
+ mode: 'dept',
+ dept_id: row.dept_id,
+ dept_name: row.dept_name,
+ start_date: tb.start_date,
+ end_date: tb.end_date,
+ }
+ assignLinesSubtitle.value = `${row.dept_name} · ${tb.start_date} ~ ${tb.end_date}`
+ assignLinesApiNote.value = ''
+ assignLinesRows.value = []
+ assignLinesCount.value = 0
+ assignLinesPage.value = 1
+ assignLinesPageSize.value = 20
+ assignLinesDialogVisible.value = true
+ await fetchAssignLinesPage()
+}
+
+function onLeaderboardAssignCellClick(r: LeaderboardPack['leaderboards'][0]['rows'][0]) {
+ if (!r || r.admin_id <= 0 || !leaderboardBlock.value) {
+ return
+ }
+ if (Number(r.assign_count ?? 0) <= 0) {
+ return
+ }
+ const lb = leaderboardBlock.value
+ assignLinesContext.value = {
+ mode: 'assistant',
+ assistant_id: r.admin_id,
+ name: r.name,
+ start_date: lb.start_date,
+ end_date: lb.end_date,
+ }
+ assignLinesSubtitle.value = `${r.name} · 医助 · ${lb.start_date} ~ ${lb.end_date}`
+ assignLinesApiNote.value = ''
+ assignLinesRows.value = []
+ assignLinesCount.value = 0
+ assignLinesPage.value = 1
+ assignLinesPageSize.value = 20
+ assignLinesDialogVisible.value = true
+ void fetchAssignLinesPage()
+}
+
+function onAssignLinesPageChange(p: number) {
+ assignLinesPage.value = p
+ void fetchAssignLinesPage()
+}
+
+function onAssignLinesPageSizeChange(size: number) {
+ assignLinesPageSize.value = size
+ assignLinesPage.value = 1
+ void fetchAssignLinesPage()
+}
+
const LEAD_LINES_EXPORT_COLUMNS: { key: keyof YejiLeadLineRow; label: string }[] = [
{ key: 'event_time_text', label: '进线时间' },
{ key: 'reception_admin_name', label: '接待' },
diff --git a/server/app/adminapi/controller/stats/YejiStatsController.php b/server/app/adminapi/controller/stats/YejiStatsController.php
index abf44a1d..dd0ca7a1 100755
--- a/server/app/adminapi/controller/stats/YejiStatsController.php
+++ b/server/app/adminapi/controller/stats/YejiStatsController.php
@@ -17,6 +17,7 @@ use app\adminapi\logic\stats\YejiStatsLogic;
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
* - GET stats.yejiStats/appointmentLines 医助排行榜「预约诊单」挂号逐条明细
* - GET stats.yejiStats/revisitBreakdown 二中心复诊下钻:医助 × 业务订单笔数
+ * - GET stats.yejiStats/assignLines 被指派数明细(医助×诊单去重,与看板同口径)
*/
class YejiStatsController extends BaseAdminController
{
@@ -141,4 +142,13 @@ class YejiStatsController extends BaseAdminController
return $this->data(YejiStatsLogic::revisitDeptAssistantBreakdown($params, $this->adminId, $this->adminInfo));
}
+
+ /** 被指派数明细:与看板「被指派数」同口径(区间内非继承指派,医助×诊单去重) */
+ public function assignLines()
+ {
+ @set_time_limit(120);
+ $params = $this->request->get();
+
+ return $this->data(YejiStatsLogic::assignLines($params, $this->adminId, $this->adminInfo));
+ }
}
diff --git a/server/app/adminapi/logic/stats/YejiStatsLogic.php b/server/app/adminapi/logic/stats/YejiStatsLogic.php
index 1c034e07..aea34465 100755
--- a/server/app/adminapi/logic/stats/YejiStatsLogic.php
+++ b/server/app/adminapi/logic/stats/YejiStatsLogic.php
@@ -2616,6 +2616,420 @@ class YejiStatsLogic
];
}
+ /**
+ * 被指派明细:与看板「被指派数」同口径(区间内非继承指派,医助×诊单去重)。
+ *
+ * @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
+ */
+ public static function assignLines(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::yejiDeptAssignLinesInner($params, $c);
+ }
+
+ return self::yejiAssistantAssignLinesInner($params, $c, $assistantId);
+ }
+
+ /**
+ * @param array $params
+ * @param array $c
+ *
+ * @return array
+ */
+ private static function yejiDeptAssignLinesInner(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 self::yejiAssignLinesEmptyPayload($c, $note, 0, '', $deptIdParam, $deptName, $channelCode);
+ };
+
+ if ($deptIdParam <= 0) {
+ return $empty('未归属中心无被指派明细(看板该行为 0)。');
+ }
+ if (!in_array($deptIdParam, $c['tableRowDeptIds'], true)) {
+ return $empty('该部门不在当前展示部门筛选或数据权限范围内。');
+ }
+
+ $erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
+ if ($channelCode !== '' && isset($erCenterDeptSet[$deptIdParam])) {
+ return $empty('与看板一致:二中心及其组织下级在选定渠道下被指派数计 0,无明细。');
+ }
+
+ $targetAssistantIds = [];
+ foreach ($c['adminToPrimary'] as $aid => $primary) {
+ if ((int) $primary === $deptIdParam) {
+ $targetAssistantIds[] = (int) $aid;
+ }
+ }
+ if ($targetAssistantIds === []) {
+ return $empty('当前部门下没有可映射的被指派医助,无明细。');
+ }
+
+ return self::yejiAssignLinesFetch($c, $params, $targetAssistantIds, 0, '', $deptIdParam, $deptName);
+ }
+
+ /**
+ * @param array $params
+ * @param array $c
+ *
+ * @return array
+ */
+ private static function yejiAssistantAssignLinesInner(array $params, array $c, int $assistantId): array
+ {
+ $channelCode = (string) $c['channelCode'];
+
+ $assistantName = '';
+ if ($assistantId > 0) {
+ $assistantName = (string) (Db::name('admin')->where('id', $assistantId)->whereNull('delete_time')->value('name') ?: '');
+ }
+
+ $empty = static function (string $note) use ($c, $assistantId, $assistantName, $channelCode): array {
+ return self::yejiAssignLinesEmptyPayload($c, $note, $assistantId, $assistantName, 0, '', $channelCode);
+ };
+
+ if ($assistantId <= 0) {
+ return $empty('请指定有效医助,或传入 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 $empty('当前账号数据权限下不可查看该医助被指派明细。');
+ }
+ }
+
+ if (!isset($c['adminToPrimary'][$assistantId])) {
+ return $empty('该医助不在当前业绩看板展示范围内。');
+ }
+
+ $tagDiagIds = $c['tagDiagIds'];
+ $tagAssistantIds = $c['tagAssistantIds'];
+ $tagFallback = $c['tagFallback'];
+ $scopedAssistants = $tagFallback ? $tagAssistantIds : null;
+
+ if ($tagDiagIds !== null && $tagDiagIds === []) {
+ return $empty('当前渠道标签下无关联诊单,无被指派明细。');
+ }
+ if ($scopedAssistants !== null && $scopedAssistants === []) {
+ return $empty('当前渠道标签下无关联医助,无被指派明细。');
+ }
+ if ($scopedAssistants !== null && !isset($scopedAssistants[$assistantId])) {
+ return $empty('当前渠道标签口径下该医助无被指派记录(与排行榜「被指派数」一致)。');
+ }
+
+ $erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
+ $primaryDept = (int) ($c['adminToPrimary'][$assistantId] ?? 0);
+ if ($channelCode !== '' && $primaryDept > 0 && isset($erCenterDeptSet[$primaryDept])) {
+ return $empty('与看板一致:二中心及其组织下级医助在选定渠道下被指派数计 0,无明细。');
+ }
+
+ return self::yejiAssignLinesFetch(
+ $c,
+ $params,
+ [$assistantId],
+ $assistantId,
+ $assistantName,
+ 0,
+ ''
+ );
+ }
+
+ /**
+ * @param array $c
+ * @param array $params
+ * @param list $targetAssistantIds
+ *
+ * @return array
+ */
+ private static function yejiAssignLinesFetch(
+ array $c,
+ array $params,
+ array $targetAssistantIds,
+ int $assistantIdOut,
+ string $assistantNameOut,
+ int $deptIdOut,
+ string $deptNameOut
+ ): array {
+ $channelCode = (string) $c['channelCode'];
+ $startTs = (int) $c['startTs'];
+ $endTs = (int) $c['endTs'];
+ $page = max(1, (int) ($params['page'] ?? 1));
+ $limit = min(100, max(1, (int) ($params['page_size'] ?? 20)));
+ $offset = ($page - 1) * $limit;
+
+ $dataScopeRestricted = !empty($c['dataScopeRestricted']);
+ $dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
+ ? $c['dataScopeVisibleAdminIds']
+ : null;
+ if ($dataScopeAdminIds !== null && $dataScopeAdminIds !== []) {
+ $visFlip = array_flip($dataScopeAdminIds);
+ $targetAssistantIds = array_values(array_filter(
+ $targetAssistantIds,
+ static fn (int $aid): bool => isset($visFlip[$aid])
+ ));
+ }
+
+ if ($targetAssistantIds === []) {
+ return self::yejiAssignLinesEmptyPayload(
+ $c,
+ '当前数据权限下无可查看的被指派医助。',
+ $assistantIdOut,
+ $assistantNameOut,
+ $deptIdOut,
+ $deptNameOut,
+ $channelCode
+ );
+ }
+
+ $tagDiagIds = $c['tagDiagIds'];
+ $tagAssistantIds = $c['tagAssistantIds'];
+ $tagFallback = $c['tagFallback'];
+ $scopedAssistants = $tagFallback ? $tagAssistantIds : null;
+
+ if ($tagDiagIds !== null && $tagDiagIds === []) {
+ return self::yejiAssignLinesEmptyPayload(
+ $c,
+ '当前渠道标签下无关联诊单,无被指派明细。',
+ $assistantIdOut,
+ $assistantNameOut,
+ $deptIdOut,
+ $deptNameOut,
+ $channelCode
+ );
+ }
+ if ($scopedAssistants !== null && $scopedAssistants === []) {
+ return self::yejiAssignLinesEmptyPayload(
+ $c,
+ '当前渠道标签下无关联医助,无被指派明细。',
+ $assistantIdOut,
+ $assistantNameOut,
+ $deptIdOut,
+ $deptNameOut,
+ $channelCode
+ );
+ }
+
+ $buildBaseQuery = static function () use (
+ $c,
+ $targetAssistantIds,
+ $startTs,
+ $endTs,
+ $tagDiagIds,
+ $scopedAssistants
+ ) {
+ $diagTable = self::tableWithPrefix('tcm_diagnosis');
+ $q = Db::name('tcm_diagnosis_assign_log')
+ ->alias('lg')
+ ->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
+ ->where('lg.create_time', 'between', [$startTs, $endTs])
+ ->where('lg.to_assistant_id', '>', 0)
+ ->where('lg.is_inherit', 0)
+ ->whereIn('lg.to_assistant_id', $targetAssistantIds);
+
+ self::applyYejiAssignLogChannelFilters(
+ $q,
+ $tagDiagIds,
+ $scopedAssistants,
+ $c['appointmentChannelValues'],
+ $c['channelFilterActive'],
+ $c['channelInfo']
+ );
+
+ return $q;
+ };
+
+ $countRows = $buildBaseQuery()
+ ->field(['lg.to_assistant_id', 'lg.diagnosis_id'])
+ ->group('lg.to_assistant_id,lg.diagnosis_id')
+ ->select()
+ ->toArray();
+ $count = \count($countRows);
+
+ $rawList = $buildBaseQuery()
+ ->field([
+ 'lg.to_assistant_id',
+ 'lg.diagnosis_id',
+ Db::raw('COUNT(*) AS assign_count'),
+ Db::raw('MAX(lg.create_time) AS last_assign_time'),
+ ])
+ ->group('lg.to_assistant_id,lg.diagnosis_id')
+ ->order('last_assign_time', 'desc')
+ ->order('lg.diagnosis_id', 'desc')
+ ->limit($offset, $limit)
+ ->select()
+ ->toArray();
+
+ $diagIds = [];
+ $assistantIds = [];
+ foreach ($rawList as $rw) {
+ $diagIds[(int) ($rw['diagnosis_id'] ?? 0)] = true;
+ $assistantIds[(int) ($rw['to_assistant_id'] ?? 0)] = true;
+ }
+ $diagInfo = self::fetchYejiAssignDiagnosisInfo(array_keys($diagIds));
+ $nameMap = $assistantIds !== []
+ ? Db::name('admin')->whereIn('id', array_keys($assistantIds))->column('name', 'id')
+ : [];
+
+ $lists = [];
+ foreach ($rawList as $rw) {
+ $did = (int) ($rw['diagnosis_id'] ?? 0);
+ $aid = (int) ($rw['to_assistant_id'] ?? 0);
+ $lastTs = (int) ($rw['last_assign_time'] ?? 0);
+ $lists[] = [
+ 'diagnosis_id' => $did,
+ 'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
+ 'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
+ 'assistant_id' => $aid,
+ 'assistant_name' => $aid > 0 ? (string) ($nameMap[$aid] ?? ('#' . $aid)) : '—',
+ 'assign_count' => (int) ($rw['assign_count'] ?? 0),
+ 'last_assign_time' => $lastTs,
+ 'last_assign_time_text' => $lastTs > 0 ? date('Y-m-d H:i:s', $lastTs) : '',
+ ];
+ }
+
+ $note = '与看板「被指派数」同口径:区间内 `tcm_diagnosis_assign_log` 成功指派(to_assistant_id>0,剔除勾选「继承」),'
+ . '按指派操作时间落区间,「医助 × 诊单」去重、剔除已删诊单。';
+
+ return [
+ 'start_date' => (string) $c['startDate'],
+ 'end_date' => (string) $c['endDate'],
+ 'assistant_id' => $assistantIdOut,
+ 'assistant_name' => $assistantNameOut,
+ 'dept_id' => $deptIdOut,
+ 'dept_name' => $deptNameOut,
+ 'channel_code' => $channelCode,
+ 'count' => $count,
+ 'lists' => $lists,
+ 'note' => $note,
+ ];
+ }
+
+ /**
+ * @param array $c
+ *
+ * @return array
+ */
+ private static function yejiAssignLinesEmptyPayload(
+ array $c,
+ string $note,
+ int $assistantId,
+ string $assistantName,
+ int $deptId,
+ string $deptName,
+ string $channelCode
+ ): array {
+ return [
+ 'start_date' => (string) $c['startDate'],
+ 'end_date' => (string) $c['endDate'],
+ 'assistant_id' => $assistantId,
+ 'assistant_name' => $assistantName,
+ 'dept_id' => $deptId,
+ 'dept_name' => $deptName,
+ 'channel_code' => $channelCode,
+ 'count' => 0,
+ 'lists' => [],
+ 'note' => $note,
+ ];
+ }
+
+ /**
+ * @param \think\db\Query $query
+ */
+ private static function applyYejiAssignLogChannelFilters(
+ $query,
+ ?array $tagDiagIds,
+ ?array $tagAssistantIds,
+ array $appointmentChannelValues,
+ bool $channelFilterActive,
+ ?array $channelInfo
+ ): void {
+ if (!$channelFilterActive) {
+ return;
+ }
+ if ($appointmentChannelValues !== []) {
+ $norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
+ if ($norm === []) {
+ $query->whereRaw('0 = 1');
+
+ return;
+ }
+ $pack = self::buildChannelScopedAppointmentExistsSqlAndBindings('dg.id', $norm, $channelInfo);
+ if ($pack === null) {
+ $query->whereRaw('0 = 1');
+
+ return;
+ }
+ $query->whereRaw($pack[0], $pack[1]);
+ } elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
+ if ($tagDiagIds !== null && $tagDiagIds === []) {
+ $query->whereRaw('0 = 1');
+
+ return;
+ }
+ if ($tagAssistantIds !== null && $tagAssistantIds === []) {
+ $query->whereRaw('0 = 1');
+
+ return;
+ }
+ if ($tagDiagIds !== null) {
+ $query->whereIn('lg.diagnosis_id', $tagDiagIds);
+ }
+ if ($tagAssistantIds !== null) {
+ $query->whereIn('lg.to_assistant_id', array_map('intval', array_keys($tagAssistantIds)));
+ }
+ } else {
+ $query->whereRaw('0 = 1');
+ }
+ }
+
+ /**
+ * @param list $diagIds
+ *
+ * @return array
+ */
+ private static function fetchYejiAssignDiagnosisInfo(array $diagIds): array
+ {
+ if ($diagIds === []) {
+ return [];
+ }
+ $rows = Db::name('tcm_diagnosis')
+ ->whereIn('id', $diagIds)
+ ->field(['id', 'patient_name', 'phone'])
+ ->select()
+ ->toArray();
+ $out = [];
+ foreach ($rows as $r) {
+ $out[(int) $r['id']] = [
+ 'patient_name' => trim((string) ($r['patient_name'] ?? '')),
+ 'phone' => trim((string) ($r['phone'] ?? '')),
+ ];
+ }
+
+ return $out;
+ }
+
/**
* admin → 展示「中心」映射:用于挂号明细按部门筛选(与 aggregateConsults 映射同源)。
*/
diff --git a/server/sql/1.9.20260616/add_yeji_assign_lines_menu.sql b/server/sql/1.9.20260616/add_yeji_assign_lines_menu.sql
new file mode 100644
index 00000000..e9fc7b37
--- /dev/null
+++ b/server/sql/1.9.20260616/add_yeji_assign_lines_menu.sql
@@ -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/assignLines', '', '', '', '',
+ 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/assignLines'
+);
|