<template>
    <div class="yeji-page">
        <header class="yeji-masthead">
            <div class="yeji-masthead__inner">
                <h1 class="yeji-masthead__title">业绩看板</h1>
                <p class="yeji-masthead__desc">渠道 · 部门 · 日期维度的业绩分析</p>
            </div>
        </header>

        <!-- ── 顶部筛选 ────────────────────────────── -->
        <el-card class="yeji-panel filter-card" shadow="never">
            <div class="filter-surface">
                <div class="filter-row filter-row--top">
                    <div class="filter-field">
                        <span class="lbl">渠道来源</span>
                        <el-select
                            v-model="selectedChannel"
                            filterable
                            placeholder="渠道筛选"
                            class="filter-select filter-select--channel"
                            :loading="channelOptionsLoading"
                            @change="loadData"
                        >
                            <el-option label="全部" value="" />
                            <el-option-group
                                v-for="g in channelGroups"
                                :key="g.group_name"
                                :label="g.group_name"
                            >
                                <el-option
                                    v-for="ch in g.channels"
                                    :key="ch.channel_code"
                                    :label="ch.channel_name"
                                    :value="ch.channel_code"
                                >
                                    <div class="channel-opt-row">
                                        <span class="opt-name">{{ ch.channel_name }}</span>
                                        <span class="opt-count">{{
                                            ch.customer_count > 0 ? ch.customer_count : ''
                                        }}</span>
                                    </div>
                                </el-option>
                            </el-option-group>
                        </el-select>
                    </div>

                    <div class="filter-field filter-field--wide">
                        <span class="lbl">展示部门</span>
                        <div
                            class="filter-select filter-select--dept filter-dept-tree-wrap"
                            v-loading="deptOptionsLoading"
                        >
                            <el-tree-select
                                v-model="selectedDeptIds"
                                :data="deptTreeOptions"
                                multiple
                                collapse-tags
                                collapse-tags-tooltip
                                filterable
                                check-strictly
                                :render-after-expand="false"
                                :default-expand-all="true"
                                placeholder="默认：所有「中心」部门"
                                class="filter-dept-tree-select"
                                node-key="id"
                                :props="deptTreeSelectProps"
                            />
                        </div>
                    </div>

                    <div class="filter-field filter-field--date-top">
                        <span class="lbl">日期范围</span>
                        <el-date-picker
                            v-model="customRange"
                            type="daterange"
                            value-format="YYYY-MM-DD"
                            start-placeholder="开始"
                            end-placeholder="结束"
                            :clearable="true"
                            :shortcuts="dateShortcuts"
                            class="filter-date-picker"
                        />
                    </div>

                    <div class="filter-actions">
                        <el-button
                            type="primary"
                            class="filter-btn filter-btn--query"
                            :loading="loading"
                            @click="loadData"
                        >
                            <el-icon class="filter-btn__icon"><Search /></el-icon>
                            查询
                        </el-button>
                        <el-button
                            class="filter-btn filter-btn--reset"
                            :disabled="loading"
                            @click="handleReset"
                        >
                            <el-icon class="filter-btn__icon"><RefreshRight /></el-icon>
                            重置
                        </el-button>
                    </div>
                </div>



                <div class="filter-quick">
                    <span class="filter-quick__title">快捷日期</span>
                    <div class="date-pills" role="group" aria-label="快捷日期">
                        <button
                            v-for="sc in dateShortcuts"
                            :key="sc.text"
                            type="button"
                            class="date-pill"
                            @click="applyDateShortcut(sc)"
                        >
                            {{ sc.text }}
                        </button>
                    </div>
                </div>
            </div>
        </el-card>

        <el-card class="yeji-panel yeji-main-tabs-card" shadow="never">
            <div class="yeji-table-tabs-head">
                <span class="yeji-table-tabs-head__title">表格统计</span>
                <span class="yeji-table-tabs-head__hint">列较多时可横向滚动浏览；「部门 / 排名」列在滚动时保持固定。</span>
            </div>
            <el-tabs v-model="yejiDisplayTab" class="yeji-view-tabs">
                <el-tab-pane label="医生统计" name="doctor" lazy>
                    <el-card
                        v-if="canViewDoctorDailyStats"
                        class="yeji-panel doctor-daily-card doctor-daily-card--nested"
                        shadow="never"
                    >
                        <template #header>
                            <div class="doctor-daily-card__head">
                                <div class="doctor-daily-card__title-block">
                                    <span class="doctor-daily-card__title">医生明细</span>
                                    <span v-if="doctorDailyRangeText" class="doctor-daily-card__range">{{
                                        doctorDailyRangeText
                                    }}</span>
                                </div>
                                <div class="doctor-daily-card__tools">
                                    <el-button
                                        type="primary"
                                        size="small"
                                        :loading="doctorDailyLoading"
                                        @click="loadDoctorDailyStats"
                                    >
                                        刷新
                                    </el-button>
                                </div>
                            </div>
                        </template>
                        <el-table
                            v-loading="doctorDailyLoading"
                            :data="doctorDailyRows"
                            size="small"
                            border
                            stripe
                            class="doctor-daily-table"
                            show-summary
                            :summary-method="getDoctorDailySummaries"
                        >
                            <el-table-column prop="doctor_name" label="医生" min-width="100" fixed />
                            <el-table-column
                                prop="system_prescription_count"
                                label="系统开方"
                                min-width="88"
                                align="right"
                            />
                            <el-table-column
                                prop="manual_prescription_count"
                                label="手动开方"
                                min-width="88"
                                align="right"
                            />
                            <el-table-column prop="deal_amount" label="成交金额" min-width="110" align="right">
                                <template #default="{ row }">¥ {{ formatDoctorDailyMoney(row.deal_amount) }}</template>
                            </el-table-column>
                            <el-table-column prop="deal_order_count" label="接诊诊单" min-width="88" align="right" />
                            <el-table-column prop="appointment_completed" label="挂号完成" min-width="88" align="right" />
                            <el-table-column prop="appointment_missed" label="过号" min-width="72" align="right" />
                            <el-table-column prop="appointment_cancelled" label="取消" min-width="72" align="right" />
                        </el-table>
                    </el-card>
                    <el-empty v-else description="当前账号无医生统计权限" />
                </el-tab-pane>

                <el-tab-pane label="医助排行榜" name="leaderboard" lazy>
                    <div
                        v-if="leaderboardBlock || leaderboardsLoading"
                        class="leaderboards-wrap"
                        v-loading="leaderboardsLoading"
                    >
                        <template v-if="leaderboardBlock">
                            <p class="leaderboard-hint">
                                {{ leaderboardBlock.range_note }}
                                <span class="muted">
                                    {{ leaderboardBlock.start_date }} ～ {{ leaderboardBlock.end_date }}
                                </span>
                            </p>
                            <div class="tables-wrap">
                                <template v-for="lb in leaderboardBlock.leaderboards" :key="lb.dept_id">
                                    <div v-if="lb.rows.length > 0" class="yeji-table leaderboard-table">
                                        <div class="yeji-table__header">
                                            {{ lb.dept_name }} · 医助排行榜
                                            <span v-if="leaderboardBlock.channel_name" class="leaderboard-sub">
                                                （{{ leaderboardBlock.channel_name }}）
                                            </span>
                                        </div>
                                        <div class="yeji-table__scroll" tabindex="0" role="region" aria-label="排行榜，可横向滚动">
                                            <table class="yeji-table__body yeji-table__body--leaderboard">
                                            <thead>
                                                <tr>
                                                    <th class="col-rank">排名</th>
                                                    <th class="col-abbr">部门</th>
                                                    <th class="col-name">姓名</th>
                                                    <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
                                                            v-for="slot in leaderboardRevisitSlotColumnsForLb(lb)"
                                                            :key="'lbh-' + lb.dept_id + '-' + slot"
                                                            class="col-revisit-slot"
                                                        >
                                                            {{ yejiRevisitSlotColumnTitle(slot) }}
                                                        </th>
                                                    </template>
                                                    <th>诊金</th>
                                                    <th>接诊率<br /><span class="th-sub">（元/进线）</span></th>
                                                </tr>
                                            </thead>
                                            <tbody>
                                                <tr
                                                    v-for="r in lb.rows"
                                                    :key="r.admin_id"
                                                    class="yeji-row--action"
                                                    @click="onLeaderboardAssistantRowClick(r)"
                                                >
                                                    <td class="col-rank">{{ r.rank }}</td>
                                                    <td class="col-abbr">{{ r.dept_abbr }}</td>
                                                    <td class="col-name">{{ r.name }}</td>
                                                    <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
                                                                v-if="Number(r.revisit_count ?? 0) > 0"
                                                                class="yeji-lead-cell--link"
                                                            >{{ formatLeaderboardInt(r.revisit_count ?? 0) }}</span>
                                                            <template v-else>{{
                                                                formatLeaderboardInt(r.revisit_count ?? 0)
                                                            }}</template>
                                                        </td>
                                                        <td
                                                            v-for="slot in leaderboardRevisitSlotColumnsForLb(lb)"
                                                            :key="'lbc-' + r.admin_id + '-' + slot"
                                                            class="col-revisit-slot"
                                                            @click.stop="onLeaderboardRevisitSlotClick(r, slot)"
                                                        >
                                                            <span
                                                                v-if="yejiRevisitSlotAt(r, slot) > 0"
                                                                class="yeji-lead-cell--link"
                                                            >{{ formatLeaderboardInt(yejiRevisitSlotAt(r, slot)) }}</span>
                                                            <template v-else>{{
                                                                formatLeaderboardInt(yejiRevisitSlotAt(r, slot))
                                                            }}</template>
                                                        </td>
                                                    </template>
                                                    <td>{{ formatMoney(r.fee_amount) }}</td>
                                                    <td>{{ formatMoney(r.consult_rate) }}</td>
                                                </tr>
                                            </tbody>
                                        </table>
                                        </div>
                                        <div class="yeji-table__foot yeji-table__foot--left">
                                            <span>诊金单位：元 · 诊金/接诊诊单/复诊均按「订单创建人 = 该医助」归属，与部门「合计业绩 / 接诊诊单 / 复诊」同口径 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线（元/进线）</span>
                                            <span v-if="lb.er_center_subtree">
                                                · 复诊列为二中心口径（同一诊单业务单序列第 2 笔起），点击数字查看对应业务订单
                                            </span>
                                            <span>
                                                · 预约诊单：预约日期在统计区间内，状态含已预约/已完成/已过号（不含已取消），医助归属与接诊单数一致（按挂号 assistant_id 优先，否则诊单 assistant_id）；各科组展示全部医助。
                                            </span>
                                        </div>
                                    </div>
                                </template>
                            </div>
                            <div
                                v-if="!leaderboardsLoading && leaderboardVisibleTables === 0"
                                class="empty-tip leaderboard-empty"
                            >
                                <el-empty description="当前筛选下暂无医助排行数据" />
                            </div>
                        </template>
                    </div>
                    <el-empty
                        v-else-if="!leaderboardsLoading"
                        class="yeji-charts-empty"
                        description="当前筛选下暂无医助排行数据"
                    />
                </el-tab-pane>

                <el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
        <!-- ── 业绩表（4 张/N 张）；目标看板见「目标看板（卡片）」Tab ── -->
        <div v-loading="loading" class="tables-wrap">
            <div v-if="!loading && tables.length === 0" class="empty-tip">
                <el-empty description="暂无数据" />
            </div>

            <div
                v-for="(tb, idx) in tables"
                :key="idx"
                class="yeji-table"
            >
                <div class="yeji-table__header">
                    {{ buildTableTitle(tb) }}
                </div>
                <div class="yeji-table__scroll" tabindex="0" role="region" aria-label="业绩数据表，可横向滚动">
                    <table class="yeji-table__body yeji-table__body--data">
                    <thead>
                        <tr>
                            <th class="col-dept">部门</th>
                            <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">
                                            add_external_contact 事件数，按接待企微成员归属部门；定渠道时须客户带对应企微标签。<br />
                                            <template v-if="tb.channel_name">
                                                <b>二中心及其组织下级</b>（与部门 ErCenter 子树一致）在选定渠道下进线计 <b>0</b>。
                                            </template>
                                        </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>tcm_diagnosis_assign_log</b> 行数（成功指派：<b>to_assistant_id &gt; 0</b>），**仅按 <b>related_po_create_time</b>** 落在所选区间内（与日志表快照一致；诊单手动指派已写入该字段）。按<b>被指派医助</b>归属展示部门。<template v-if="tb.channel_name">
                                                选定渠道时收窄与「{{ tb.channel_name }}」业绩同口径（标签诊单或 channels EXISTS）。<br />
                                                <b>二中心及其组织下级</b>在选定渠道下本列计 <b>0</b>。
                                            </template>
                                            <template v-else><br />未选渠道时不限客户来源，与「合计业绩」列渠道无关。</template>
                                        </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>的部门及其组织下级：候选订单为有诊单医助的非取消业务单，按患者 <b>create_time</b> 排序，<b>第 1 笔不计</b>；第 2 笔起计入合计，分列复诊2、复诊三…。<b>归属</b>按订单创建人的人事部门，与<b>合计业绩</b>列同口径。非二中心子树部门为 0。
                                        </div>
                                    </template>
                                    <el-icon class="col-info"><InfoFilled /></el-icon>
                                </el-tooltip>
                            </th>
                            <th
                                v-for="slot in yejiRevisitSlotColumns"
                                :key="'yjv-h-' + slot"
                            >
                                <span>{{ yejiRevisitSlotColumnTitle(slot) }}</span>
                                <el-tooltip placement="top" effect="dark" :show-after="200">
                                    <template #content>
                                        <div style="max-width: 280px; line-height: 1.7; font-size: 12px">
                                            该列统计区间内患者第 <b>{{ slot }}</b> 笔业务单条数（第 1 笔初诊单列不计）；归属与合计列相同。
                                        </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: 300px; line-height: 1.7; font-size: 12px">
                                            按订单<b>创建时间</b>统计（剔除履约已取消），金额与<b>接诊诊单</b>列一致：仅按<b>订单创建人</b>的人事部门落在本展示行（含下级）计入，多行命中时取最深的展示部门。与<b>医助排行榜诊金</b>同口径。<br />
                                            <b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表对齐。<br />
                                            点击金额可查看对应业务订单明细（侧栏默认「合计业绩」全量列表）。
                                        </div>
                                    </template>
                                    <el-icon class="col-info"><InfoFilled /></el-icon>
                                </el-tooltip>
                            </th>
                            <th v-if="tb.channel_name" class="col-completed col-narrow-head">
                                <span class="yeji-th-label">{{ yejiChannelMetricShort('perf') }}</span>
                                <el-tooltip placement="top" effect="dark" :show-after="200">
                                    <template #content>
                                        <div style="max-width: 340px; line-height: 1.7; font-size: 12px">
                                            「{{ tb.channel_name }}」渠道业绩（与卡片标题渠道一致）。与「处方订单列表」业绩<b>同口径</b>：按订单
                                            <b>create_time</b> 归日，剔除履约已取消(4)。<br />
                                            渠道判定用 EXISTS：该患者有任一挂号 <b>channels 命中字典 + status=3</b>（不限挂号时点）。<br />
                                            点击金额可查看该部门本渠道订单明细（侧栏「渠道筛选」列表）。
                                        </div>
                                    </template>
                                    <el-icon class="col-info" aria-label="说明"><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>落在区间内统计；部门归属医助优先，否则接诊医生；与<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>
                                </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">
<<<<<<< HEAD
                                            <b>业务订单条数</b>（非取消）：订单 <b>create_time</b> 落入区间、<b>fulfillment_status ≠ 4</b>；按<b>订单创建人</b>的人事部门落在该部门子树即计入（表格多行命中时取最深的展示部门）。与<b>合计业绩 / 医助排行榜接诊诊单</b>同口径。选定渠道时本列仍为全量。
=======
                                            <b>业务订单条数</b>（非取消）：订单 <b>create_time</b> 落入区间、<b>fulfillment_status ≠ 4</b>；与列表筛选
                                            <b>assistant_dept_id</b> 时一致——<b>创建人</b>人事部门优先，无创建人则诊单 <b>医助</b>；落在该部门子树即计入（表格多行命中时取最深的展示部门）。与侧栏勾选「与表格业绩对齐」时的集合可能略有差异。选定渠道时本列仍为全量。
>>>>>>> master
                                        </div>
                                    </template>
                                    <el-icon class="col-info"><InfoFilled /></el-icon>
                                </el-tooltip>
                            </th>
                            <th v-if="tb.channel_name" class="col-completed col-narrow-head">
                                <span class="yeji-th-label">{{ yejiChannelMetricShort('deal') }}</span>
                                <el-tooltip placement="top" effect="dark" :show-after="200">
                                    <template #content>
                                        <div style="max-width: 320px; line-height: 1.7; font-size: 12px">
                                            「{{ tb.channel_name }}」渠道成交单数；与渠道业绩同源：区间内 <b>非取消业务订单</b> 条数，按订单
                                            <b>create_time</b> 归日；渠道判定同业绩列。
                                        </div>
                                    </template>
                                    <el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
                                </el-tooltip>
                            </th>
                            <th v-if="tb.channel_name" class="col-completed col-narrow-head">
                                <span class="yeji-th-label">{{ yejiChannelMetricShort('aov') }}</span>
                                <el-tooltip placement="top" effect="dark" :show-after="200">
                                    <template #content>
                                        <div style="max-width: 280px; line-height: 1.7; font-size: 12px">
                                            「{{ tb.channel_name }}」渠道客单价 ≈ 渠道业绩 ÷ 渠道成交单。
                                        </div>
                                    </template>
                                    <el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
                                </el-tooltip>
                            </th>
                            <th>
                                <span>ROI</span>
                                <el-tooltip placement="top" effect="dark" :show-after="200">
                                    <template #content>
                                        <div style="max-width: 320px; line-height: 1.7; font-size: 12px">
                                            <template v-if="tb.channel_name">
                                                有渠道时：<b>合计行 ROI</b> = <b>{{ tb.channel_name }}业绩合计</b>（与上表该渠道业绩列合计一致，<b>不含二中心及其组织下级</b>）÷ <b>投放成本合计</b>。<br />
                                                部门行 ROI = 该部门 <b>{{ tb.channel_name }}业绩</b> ÷ 该部门 <b>投放成本</b>；<b>二中心子树</b>部门该渠道业绩与进线为 0、投放成本分摊进线计 0，故无 ROI。与「合计业绩」列无关。
                                            </template>
                                            <template v-else>
                                                <b>合计行 ROI</b> = 全表业绩合计（<b>不含二中心子树</b>各部门行业绩）÷ <b>投放成本合计</b>；<br />
                                                部门行 = 该部门「合计业绩」口径业绩 ÷ 该部门均摊投放成本；<b>二中心子树</b>部门不参与成本分摊（进线占比计 0），故无 ROI。
                                            </template>
                                        </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>；<b>单进线成本</b> = 总成本 ÷ 各展示部门<b>进线合计</b>；<br />
                                            各部门行投放成本 = 单进线成本 × 该部门进线（四舍五入尾差并入进线最多的部门）。<b>二中心及其组织下级</b>进线在分摊中计 <b>0</b>（与 ROI 分子扣除范围一致）。
                                        </div>
                                    </template>
                                    <el-icon class="col-info"><InfoFilled /></el-icon>
                                </el-tooltip>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr
                            v-for="r in tb.rows"
                            :key="`${idx}-${r.dept_id}-${r.dept_name}`"
                            class="yeji-row--action"
                            :class="{
                                'yeji-row--unassigned': isUnassignedYejiRow(r),
                                'yeji-row--idle': r.dept_id <= 0 && !isUnassignedYejiRow(r),
                            }"
                            @click="onYejiDeptRowClick(tb, r)"
                        >
                            <td class="col-dept">{{ r.dept_name }}</td>
                            <td @click="onLeadCountCellClick($event, tb, r)">
                                <span
                                    v-if="r.dept_id > 0 && Number(r.lead_count) > 0"
                                    class="yeji-lead-cell--link"
                                >{{ formatInt(r.lead_count) }}</span>
                                <template v-else>{{ formatInt(r.lead_count) }}</template>
                            </td>
                            <td>{{ formatInt(r.assign_count ?? 0) }}</td>
                            <td @click.stop="onYejiRevisitTotalCellClick(tb, r)">
                                <span
                                    v-if="r.dept_id > 0 && Number(r.revisit_count ?? 0) > 0"
                                    class="yeji-lead-cell--link"
                                >{{ formatInt(r.revisit_count ?? 0) }}</span>
                                <template v-else>{{ formatInt(r.revisit_count ?? 0) }}</template>
                            </td>
                            <td
                                v-for="slot in yejiRevisitSlotColumns"
                                :key="'yjv-' + idx + '-' + r.dept_id + '-' + slot"
                                @click.stop="onYejiRevisitSlotCellClick(tb, r, slot)"
                            >
                                <span
                                    v-if="r.dept_id > 0 && yejiRevisitSlotAt(r, slot) > 0"
                                    class="yeji-lead-cell--link"
                                >{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</span>
                                <template v-else>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</template>
                            </td>
                            <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)">
                                <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>
                            <td v-if="tb.channel_name" class="col-completed">{{ formatNumber(r.channel_avg_price) }}</td>
                            <td :class="roiClass(r.roi)">{{ formatRoi(r.roi) }}</td>
                            <td>{{ formatMoney(r.cost_amount) }}</td>
                        </tr>
                        <tr class="total-row">
                            <td class="col-dept">合计</td>
                            <td>{{ formatInt(tb.total.lead_count) }}</td>
                            <td>{{ formatInt(tb.total.assign_count ?? 0) }}</td>
                            <td>{{ formatInt(tb.total.revisit_count ?? 0) }}</td>
                            <td
                                v-for="slot in yejiRevisitSlotColumns"
                                :key="'yjvt-' + idx + '-' + slot"
                            >{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
                            <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.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>
                            <td v-if="tb.channel_name" class="col-completed">{{ formatNumber(tb.total.channel_avg_price) }}</td>
                            <td :class="roiClass(tb.total.roi)">{{ formatRoi(tb.total.roi) }}</td>
                            <td>{{ formatMoney(tb.total.cost_amount) }}</td>
                        </tr>
                    </tbody>
                </table>
                </div>
                <div class="yeji-table__foot">
                    <span>区间：{{ tb.start_date }} ~ {{ tb.end_date }}</span>
                    <span v-if="tb.channel_name"> · 渠道：{{ tb.channel_name }}</span>
                    <span v-if="tb.total.cost_per_lead != null && tb.total.cost_per_lead > 0">
                        · 单进线成本：{{ formatMoney(tb.total.cost_per_lead) }} 元
                    </span>
                </div>
            </div>
        </div>

                </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-card>

        <el-card class="yeji-panel yeji-charts-only-card" shadow="never">
            <div class="yeji-charts-only-head">统计图</div>
            <div
                v-loading="loading || (canViewDoctorDailyStats && doctorDailyLoading) || leaderboardsLoading"
                class="yeji-charts-stack"
            >
                <template v-if="chartsHasAnyData">
                    <section
                        v-if="
                            canViewDoctorDailyStats &&
                            (doctorDealBarHasData ||
                                doctorRxStackHasData ||
                                doctorAppointmentPieHasData ||
                                doctorScatterHasData ||
                                doctorRadarHasData)
                        "
                        class="yeji-chart-block"
                    >
                        <h3 class="yeji-chart-block__title">
                            医生统计
                            <span v-if="doctorDailyRangeText" class="yeji-chart-block__sub">{{
                                doctorDailyRangeText
                            }}</span>
                        </h3>
                        <el-row :gutter="16">
                            <el-col :xs="24" :lg="9">
                                <div class="yeji-chart-panel">
                                    <div class="yeji-chart-panel__label">成交金额 TOP（条形）</div>
                                    <v-charts
                                        v-if="doctorDealBarHasData"
                                        class="yeji-echart"
                                        :option="doctorDealBarOption"
                                        autoresize
                                    />
                                    <el-empty
                                        v-else
                                        description="暂无成交金额数据"
                                        :image-size="72"
                                    />
                                </div>
                            </el-col>
                            <el-col :xs="24" :lg="8">
                                <div class="yeji-chart-panel">
                                    <div class="yeji-chart-panel__label">开方构成（堆叠柱）</div>
                                    <v-charts
                                        v-if="doctorRxStackHasData"
                                        class="yeji-echart yeji-echart--short"
                                        :option="doctorRxStackOption"
                                        autoresize
                                    />
                                    <el-empty
                                        v-else
                                        description="暂无开方数据"
                                        :image-size="72"
                                    />
                                </div>
                            </el-col>
                            <el-col :xs="24" :lg="7">
                                <div class="yeji-chart-panel">
                                    <div class="yeji-chart-panel__label">挂号结果（环形 · 表尾合计）</div>
                                    <v-charts
                                        v-if="doctorAppointmentPieHasData"
                                        class="yeji-echart yeji-echart--short"
                                        :option="doctorAppointmentPieOption"
                                        autoresize
                                    />
                                    <el-empty
                                        v-else
                                        description="暂无挂号汇总"
                                        :image-size="72"
                                    />
                                </div>
                            </el-col>
                        </el-row>
                        <el-row :gutter="16" class="yeji-chart-subrow">
                            <el-col :xs="24" :lg="12">
                                <div class="yeji-chart-panel">
                                    <div class="yeji-chart-panel__label">接诊诊单 × 金额（散点）</div>
                                    <v-charts
                                        v-if="doctorScatterHasData"
                                        class="yeji-echart yeji-echart--compact"
                                        :option="doctorScatterOption"
                                        autoresize
                                    />
                                    <el-empty
                                        v-else
                                        description="至少两名医生同时有接诊诊单与金额时可展示"
                                        :image-size="72"
                                    />
                                </div>
                            </el-col>
                            <el-col :xs="24" :lg="12">
                                <div class="yeji-chart-panel">
                                    <div class="yeji-chart-panel__label">TOP 医生雷达（相对最高值归一）</div>
                                    <v-charts
                                        v-if="doctorRadarHasData"
                                        class="yeji-echart yeji-echart--compact"
                                        :option="doctorRadarOption"
                                        autoresize
                                    />
                                    <el-empty
                                        v-else
                                        description="至少两名医生有数据时可展示"
                                        :image-size="72"
                                    />
                                </div>
                            </el-col>
                        </el-row>
                    </section>

                    <template v-for="(tb, cidx) in tables" :key="'dept-chart-' + cidx">
                        <section v-if="yejiDeptChartHasData(tb)" class="yeji-chart-block">
                            <h3 class="yeji-chart-block__title">{{ buildTableTitle(tb) }}</h3>
                            <v-charts
                                class="yeji-echart yeji-echart--tall"
                                :option="getYejiDeptComboOption(tb)"
                                autoresize
                            />
                            <el-row
                                v-if="deptFunnelHasData(tb) || deptPerformancePieHasData(tb)"
                                :gutter="16"
                                class="yeji-chart-subrow"
                            >
                                <el-col v-if="deptFunnelHasData(tb)" :xs="24" :md="12">
                                    <div class="yeji-chart-panel">
                                        <div class="yeji-chart-panel__label">
                                            进线 → 接诊 → 成交（汇总漏斗）
                                        </div>
                                        <v-charts
                                            class="yeji-echart yeji-echart--compact"
                                            :option="getDeptFunnelOption(tb)"
                                            autoresize
                                        />
                                    </div>
                                </el-col>
                                <el-col v-if="deptPerformancePieHasData(tb)" :xs="24" :md="12">
                                    <div class="yeji-chart-panel">
                                        <div class="yeji-chart-panel__label">
                                            部门合计业绩占比（TOP10 + 其它）
                                        </div>
                                        <v-charts
                                            class="yeji-echart yeji-echart--compact"
                                            :option="getDeptPerformancePieOption(tb)"
                                            autoresize
                                        />
                                    </div>
                                </el-col>
                            </el-row>
                        </section>
                    </template>

                    <template v-if="leaderboardBlock">
                        <section
                            v-for="lb in leaderboardChartBlocks"
                            :key="'lb-chart-' + lb.dept_id"
                            class="yeji-chart-block"
                        >
                            <h3 class="yeji-chart-block__title">
                                {{ lb.dept_name }} · 医助
                                <span
                                    v-if="leaderboardBlock.channel_name"
                                    class="yeji-chart-block__sub"
                                >
                                    （{{ leaderboardBlock.channel_name }}）
                                </span>
                                <span class="yeji-chart-block__sub yeji-chart-block__sub--muted">
                                    {{ leaderboardBlock.start_date }}～{{ leaderboardBlock.end_date }}
                                </span>
                            </h3>
                            <el-row :gutter="16">
                                <el-col :xs="24" :lg="leaderboardPieHasData(lb) ? 14 : 24">
                                    <v-charts
                                        class="yeji-echart"
                                        :option="getLeaderboardBarOption(lb, leaderboardBlock)"
                                        autoresize
                                    />
                                </el-col>
                                <el-col v-if="leaderboardPieHasData(lb)" :xs="24" :lg="10">
                                    <div class="yeji-chart-panel">
                                        <div class="yeji-chart-panel__label">诊金占比</div>
                                        <v-charts
                                            class="yeji-echart yeji-echart--compact"
                                            :option="getLeaderboardPieOption(lb, leaderboardBlock)"
                                            autoresize
                                        />
                                    </div>
                                </el-col>
                            </el-row>
                        </section>
                    </template>
                </template>
                <el-empty
                    v-else-if="
                        !loading &&
                        (!canViewDoctorDailyStats || !doctorDailyLoading) &&
                        !leaderboardsLoading
                    "
                    class="yeji-charts-empty"
                    description="当前筛选下暂无图表数据，请调整条件后查询"
                />
            </div>
        </el-card>

        <!-- 末级部门 / 医助排行：侧栏业务订单（不跳转路由） -->
        <el-drawer
            v-model="orderDrawerVisible"
            direction="rtl"
            size="min(1120px, 98vw)"
            destroy-on-close
            class="yeji-order-drawer"
            :show-close="true"
        >
            <template #header>
                <div class="yeji-order-drawer__header">
                    <h3 class="yeji-order-drawer__heading">{{ orderDrawerTitle }}</h3>
                    <p class="yeji-order-drawer__sub">{{ orderDrawerHint }}</p>
                </div>
            </template>

            <div class="yeji-order-drawer__shell">
                <section
                    class="yeji-order-drawer__stats"
                    role="region"
                    aria-label="订单汇总"
                >
                    <div class="yeji-order-drawer__stat-card">
                        <span class="yeji-order-drawer__stat-label">符合条件的订单</span>
                        <span class="yeji-order-drawer__stat-value">{{ orderDrawerCount.toLocaleString('zh-CN') }} 笔</span>
                    </div>
                    <div class="yeji-order-drawer__stat-card">
                        <span class="yeji-order-drawer__stat-label">
                            约诊
                            <span class="yeji-order-drawer__stat-note">（已完成挂号；日期按挂号 appointment_date；医助=挂号或诊单，与看板接诊一致）</span>
                        </span>
                        <span class="yeji-order-drawer__stat-value">{{
                            orderDrawerConsultCount === null ? '—' : orderDrawerConsultCount.toLocaleString('zh-CN')
                        }} 次</span>
                    </div>
                    <template v-if="orderDrawerChannelSplitActive && orderDrawerSplitPerfCache">
                        <div class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--total">
                            <span class="yeji-order-drawer__stat-label">
                                合计业绩
                                <span class="yeji-order-drawer__stat-note"
                                    >（与看板「合计业绩」列同口径，不限渠道；{{ orderDrawerSplitPerfCache.totalOrderCount.toLocaleString('zh-CN') }} 笔）</span
                                >
                            </span>
                            <span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
                                ¥{{ formatOrderDrawerMoney(orderDrawerSplitPerfCache.totalAmount) }}
                            </span>
                        </div>
                        <div class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--accent">
                            <span class="yeji-order-drawer__stat-label">
                                {{ orderDrawerChannelLabel }}业绩
                                <span class="yeji-order-drawer__stat-note"
                                    >（与看板「{{ orderDrawerChannelLabel }}业绩」同口径；{{
                                        orderDrawerSplitPerfCache.channelOrderCount.toLocaleString('zh-CN')
                                    }} 笔）</span
                                >
                            </span>
                            <span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
                                ¥{{ formatOrderDrawerMoney(orderDrawerChannelStatAmount) }}
                            </span>
                        </div>
                    </template>
                    <div
                        v-else
                        class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--accent"
                    >
                        <span class="yeji-order-drawer__stat-label">
                            业务订单金额合计
                            <span class="yeji-order-drawer__stat-note">（不含履约已取消，与上方业绩口径一致）</span>
                        </span>
                        <span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
                            ¥{{ formatOrderDrawerMoney(orderDrawerTotalAmount) }}
                        </span>
                    </div>
                    <div
                        v-if="orderDrawerLists.length > 0"
                        class="yeji-order-drawer__stat-card yeji-order-drawer__stat-card--muted"
                    >
                        <span class="yeji-order-drawer__stat-label">本页小计</span>
                        <span class="yeji-order-drawer__stat-value">¥{{ formatOrderDrawerMoney(orderDrawerPageAmountSum) }}</span>
                    </div>
                </section>

                <el-tabs
                    v-if="orderDrawerChannelSplitActive"
                    v-model="orderDrawerListTab"
                    class="yeji-order-drawer__list-tabs"
                    @tab-change="onOrderDrawerListTabChange"
                >
                    <el-tab-pane label="合计业绩（全量订单）" name="total" />
                    <el-tab-pane :label="`渠道筛选（${orderDrawerChannelLabel}）`" name="channel" />
                </el-tabs>

                <div v-loading="orderDrawerLoading" class="yeji-order-drawer__body">
                    <el-table
                        v-if="orderDrawerLists.length > 0 || orderDrawerLoading"
                        :data="orderDrawerLists"
                        size="small"
                        stripe
                        border
                        max-height="58vh"
                        class="yeji-order-drawer__table"
                        :empty-text="orderDrawerLoading ? '加载中…' : '暂无数据'"
                    >
                        <el-table-column type="index" label="#" width="46" :index="orderDrawerIndexMethod" />
                        <el-table-column prop="order_no" label="订单号" min-width="152" show-overflow-tooltip />
                        <el-table-column label="关联挂号" min-width="218">
                            <template #default="{ row }">
                                <div v-if="row.linked_appointment" class="yeji-order-appt-lines">
                                    <div class="yeji-order-appt-line">
                                        <span class="yeji-order-appt-k">日期</span>
                                        {{ orderDrawerApptSchedule(row.linked_appointment) }}
                                    </div>
                                    <div class="yeji-order-appt-line">
                                        <span class="yeji-order-appt-k">状态</span>
                                        {{ row.linked_appointment.status_desc || '—' }}
                                        <span
                                            v-if="row.linked_appointment.doctor_name"
                                            class="yeji-order-appt-sub"
                                        >
                                            · {{ row.linked_appointment.doctor_name }}
                                        </span>
                                    </div>
                                    <div class="yeji-order-appt-line">
                                        <span class="yeji-order-appt-k">类型</span>
                                        {{ row.linked_appointment.appointment_type_desc || '—' }}
                                    </div>
                                    <div class="yeji-order-appt-line yeji-order-appt-line--channel">
                                        <span class="yeji-order-appt-k">渠道</span>
                                        <span class="yeji-order-appt-channel-text">
                                            {{
                                                row.linked_appointment.channel_source_desc ||
                                                row.linked_appointment.channel_source ||
                                                '—'
                                            }}
                                            <span
                                                v-if="
                                                    orderDrawerLinkedApptChannelDictValue(row.linked_appointment) !==
                                                    ''
                                                "
                                                class="yeji-order-appt-sub"
                                            >
                                                （字典 value
                                                {{ orderDrawerLinkedApptChannelDictValue(row.linked_appointment) }}
                                                ）
                                            </span>
                                        </span>
                                        <el-tooltip placement="top" effect="dark" :show-after="200">
                                            <template #content>
                                                <div style="max-width: 280px; line-height: 1.65; font-size: 12px">
                                                    「字典 value」= 系统字典
                                                    <b>channels</b> 的 value，与业绩表里按渠道收窄时所用<b>挂号 channels</b>一致；便于和进线列所选渠道对照，<b>不是</b>企微
                                                    add_external_contact 单上的独立编号。
                                                </div>
                                            </template>
                                            <el-icon class="yeji-order-appt-info" aria-label="说明"><InfoFilled /></el-icon>
                                        </el-tooltip>
                                    </div>
                                </div>
                                <span v-else class="yeji-order-appt-empty">—</span>
                            </template>
                        </el-table-column>
                        <el-table-column label="金额" width="108" align="right">
                            <template #default="{ row }">
                                <span class="yeji-order-amt">¥{{ row.amount ?? '—' }}</span>
                            </template>
                        </el-table-column>
                        <el-table-column prop="recipient_name" label="收货人" width="96" show-overflow-tooltip />
                        <el-table-column label="履约" width="104" show-overflow-tooltip>
                            <template #default="{ row }">
                                {{ orderFulfillmentBrief(row.fulfillment_status) }}
                            </template>
                        </el-table-column>
                        <el-table-column label="创建时间" min-width="148">
                            <template #default="{ row }">
                                {{ formatOrderDrawerTime(row.create_time) }}
                            </template>
                        </el-table-column>
                        <el-table-column prop="doctor_name" label="开方人" width="88" show-overflow-tooltip />
                        <el-table-column prop="assistant_name" label="诊单医助" width="88" show-overflow-tooltip />
                        <el-table-column prop="creator_name" label="创建人" width="88" show-overflow-tooltip />
                    </el-table>
                    <el-empty v-if="!orderDrawerLoading && orderDrawerLists.length === 0" description="暂无业务订单" />

                    <div class="yeji-order-drawer__pager">
                        <el-pagination
                            :current-page="orderDrawerPage"
                            :page-size="orderDrawerPageSize"
                            background
                            layout="total, sizes, prev, pager, next, jumper"
                            :total="orderDrawerCount"
                            :page-sizes="[10, 15, 20, 50]"
                            :disabled="orderDrawerLoading"
                            :hide-on-single-page="false"
                            @current-change="onOrderDrawerPageChange"
                            @size-change="onOrderDrawerPageSizeChange"
                        />
                    </div>
                </div>
            </div>
        </el-drawer>

        <el-dialog
            v-model="unassignedDialogVisible"
            width="min(680px, 94vw)"
            destroy-on-close
            class="yeji-unassigned-dialog"
        >
            <template #header>
                <div class="yeji-unassigned-dialog__head">
                    <span class="yeji-unassigned-dialog__title">「未归属中心」业绩拆解</span>
                    <p class="yeji-unassigned-dialog__sub">{{ unassignedDialogSubtitle }}</p>
                </div>
            </template>
            <p v-if="unassignedDialogNote" class="yeji-unassigned-dialog__note">{{ unassignedDialogNote }}</p>
            <div v-loading="unassignedDialogLoading" class="yeji-unassigned-dialog__body">
                <el-table
                    v-if="unassignedDialogRows.length > 0 || unassignedDialogLoading"
                    :data="unassignedDialogRows"
                    size="small"
                    stripe
                    border
                    max-height="440"
                    class="yeji-unassigned-dialog__table"
                    :empty-text="unassignedDialogLoading ? '加载中…' : '暂无数据'"
                >
                    <el-table-column prop="name" label="诊单医助" min-width="120" show-overflow-tooltip />
                    <el-table-column label="账号" width="88" align="right">
                        <template #default="{ row }">
                            {{ row.admin_id > 0 ? row.admin_id : '—' }}
                        </template>
                    </el-table-column>
                    <el-table-column label="订单数" width="88" align="right">
                        <template #default="{ row }">{{ formatInt(row.order_count) }}</template>
                    </el-table-column>
                    <el-table-column label="业绩金额" min-width="112" align="right">
                        <template #default="{ row }">{{ formatMoney(row.amount) }}</template>
                    </el-table-column>
                </el-table>
                <el-empty
                    v-if="!unassignedDialogLoading && unassignedDialogRows.length === 0"
                    description="暂无拆解数据"
                />
            </div>
        </el-dialog>

        <el-dialog
            v-model="leadLinesDialogVisible"
            width="min(920px, 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">{{ leadLinesSubtitle }}</p>
                </div>
            </template>
            <p v-if="leadLinesApiNote" class="yeji-unassigned-dialog__note">{{ leadLinesApiNote }}</p>
            <div v-loading="leadLinesLoading" class="yeji-unassigned-dialog__body">
                <el-table
                    v-if="leadLinesRows.length > 0 || leadLinesLoading"
                    :data="leadLinesRows"
                    size="small"
                    stripe
                    border
                    max-height="440"
                    class="yeji-unassigned-dialog__table"
                    :empty-text="leadLinesLoading ? '加载中…' : '暂无数据'"
                >
                    <el-table-column prop="event_time_text" label="进线时间" min-width="156" show-overflow-tooltip />
                    <el-table-column prop="reception_admin_name" label="接待" width="88" show-overflow-tooltip />
                    <el-table-column prop="external_contact_name" label="客户" min-width="100" show-overflow-tooltip />
                    <el-table-column prop="external_userid" label="外部联系人ID" min-width="140" show-overflow-tooltip />
                    <el-table-column prop="user_id" label="企微成员ID" min-width="120" show-overflow-tooltip />
                    <el-table-column prop="state" label="渠道参数" width="96" show-overflow-tooltip />
                </el-table>
                <el-empty
                    v-if="!leadLinesLoading && leadLinesRows.length === 0"
                    description="暂无进线明细"
                />
                <div v-if="leadLinesCount > 0" class="yeji-leadlines-dialog__pager">
                    <el-pagination
                        :current-page="leadLinesPage"
                        :page-size="leadLinesPageSize"
                        background
                        layout="total, sizes, prev, pager, next, jumper"
                        :total="leadLinesCount"
                        :page-sizes="[10, 20, 50, 100]"
                        :disabled="leadLinesLoading"
                        :hide-on-single-page="false"
                        @current-change="onLeadLinesPageChange"
                        @size-change="onLeadLinesPageSizeChange"
                    />
                </div>
            </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)"
            destroy-on-close
            class="yeji-unassigned-dialog"
        >
            <template #header>
                <div class="yeji-unassigned-dialog__head">
                    <span class="yeji-unassigned-dialog__title">复诊 · 医助拆解</span>
                    <p class="yeji-unassigned-dialog__sub">{{ revisitBreakdownSubtitle }}</p>
                </div>
            </template>
            <p v-if="revisitBreakdownNote" class="yeji-unassigned-dialog__note">{{ revisitBreakdownNote }}</p>
            <div v-loading="revisitBreakdownLoading" class="yeji-unassigned-dialog__body">
                <el-table
                    v-if="revisitBreakdownRows.length > 0 || revisitBreakdownLoading"
                    :data="revisitBreakdownRows"
                    size="small"
                    stripe
                    border
                    max-height="440"
                    class="yeji-unassigned-dialog__table"
                    :empty-text="revisitBreakdownLoading ? '加载中…' : '暂无数据'"
                >
                    <el-table-column prop="name" label="医助" min-width="120" show-overflow-tooltip />
                    <el-table-column label="业务订单" width="108" align="right">
                        <template #default="{ row }">
                            <span
                                v-if="Number(row.order_count) > 0"
                                class="yeji-lead-cell--link"
                                role="button"
                                tabindex="0"
                                @click="onYejiRevisitBreakdownOrderClick(row)"
                                @keydown.enter.prevent="onYejiRevisitBreakdownOrderClick(row)"
                            >{{ formatInt(row.order_count) }} 笔</span>
                            <template v-else>{{ formatInt(row.order_count) }}</template>
                        </template>
                    </el-table-column>
                </el-table>
                <el-empty
                    v-if="!revisitBreakdownLoading && revisitBreakdownRows.length === 0"
                    description="暂无拆解数据"
                />
            </div>
        </el-dialog>
    </div>
</template>

<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { Search, RefreshRight, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import vCharts from 'vue-echarts'
import {
    doctorDailyStatsOverview,
    yejiStatsChannelOptions,
    yejiStatsDeptOptions,
    yejiStatsLeaderboard,
    yejiStatsMulti,
    yejiStatsOverview,
    yejiStatsUnassignedBreakdown,
    yejiStatsLeadLines,
    yejiStatsAppointmentLines,
    yejiStatsRevisitBreakdown,
} from '@/api/stats'
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
import { prescriptionOrderLists } from '@/api/tcm'
import useUserStore from '@/stores/modules/user'

interface DeptOption {
    id: number
    name: string
    pid: number
    path: string
}

interface DeptTreeNode extends DeptOption {
    children?: DeptTreeNode[]
}

/** 与组织树一致：value 为部门 id，树形展示上下级 */
const deptTreeSelectProps = {
    value: 'id',
    label: 'name',
    children: 'children',
}

function buildDeptTreeFromFlat(flat: DeptOption[]): DeptTreeNode[] {
    if (!flat.length) {
        return []
    }
    const map = new Map<number, DeptTreeNode>()
    for (const d of flat) {
        map.set(d.id, { ...d, children: [] })
    }
    const roots: DeptTreeNode[] = []
    for (const d of flat) {
        const node = map.get(d.id)!
        const pid = Number(d.pid)
        if (pid > 0 && map.has(pid)) {
            map.get(pid)!.children!.push(node)
        } else {
            roots.push(node)
        }
    }
    const stripEmptyChildren = (n: DeptTreeNode) => {
        if (n.children?.length) {
            n.children.forEach(stripEmptyChildren)
        } else {
            delete n.children
        }
    }
    roots.forEach(stripEmptyChildren)
    const sortLevel = (nodes: DeptTreeNode[]) => {
        nodes.sort((a, b) => {
            const ac = a.name.includes('中心') ? -1 : 1
            const bc = b.name.includes('中心') ? -1 : 1
            if (ac !== bc) {
                return ac - bc
            }
            return a.id - b.id
        })
        for (const n of nodes) {
            if (n.children?.length) {
                sortLevel(n.children)
            }
        }
    }
    sortLevel(roots)
    return roots
}

interface ChannelOption {
    channel_code: string
    channel_name: string
    source_tag_id: string
    source_group_name: string
    customer_count: number
}

interface ChannelGroup {
    group_name: string
    channels: ChannelOption[]
}

interface YejiUnassignedBreakdownRow {
    admin_id: number
    name: string
    order_count: number
    amount: number
}

interface YejiRevisitBreakdownRow {
    admin_id: number
    name: string
    order_count: number
}

/** 侧栏「诊单医助」引用（排行榜行与复诊拆解行通用） */
type YejiAssistantDrawerRef = { admin_id: number; name: string }

interface YejiLeadLineRow {
    id: number
    event_time: number
    event_time_text: string
    external_userid: string
    user_id: string
    state: string
    reception_admin_name: string
    external_contact_name: string
}

interface YejiRow {
    dept_id: number
    dept_name: string
    lead_count: number
    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
    performance_amount: number
    completed_performance_amount: number
    channel_avg_price: number | null
    cost_amount: number
    roi: number | null
}

interface YejiTable {
    label: string
    start_date: string
    end_date: string
    channel_code: string
    channel_name: string
    channel_group: string
    channel_filter_note: string
    rows: YejiRow[]
    total: {
        lead_count: number
        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
        performance_amount: number
        completed_performance_amount: number
        channel_avg_price: number | null
        cost_amount: number
        cost_per_lead: number | null
        roi: number | null
    }
}

interface LeaderboardPack {
    start_date: string
    end_date: string
    channel_code: string
    channel_name: string
    channel_filter_note: string
    range_note: string
    leaderboards: Array<{
        dept_id: number
        dept_name: string
        dept_abbr: string
        er_center_subtree?: boolean
        rows: Array<{
            rank: number
            admin_id: number
            name: string
            dept_abbr: string
            fee_amount: number
            lead_count?: number
            assign_count?: number
            consult_count: number
            deal_order_count: number
            appointment_count?: number
            consult_rate: number
            revisit_count?: number
            revisit_slots?: Record<string, number>
        }>
    }>
}

const loading = ref(false)
const deptOptions = ref<DeptOption[]>([])
const deptOptionsLoading = ref(false)
const deptTreeOptions = computed(() => buildDeptTreeFromFlat(deptOptions.value))
const selectedDeptIds = ref<number[]>([])
const channelGroups = ref<ChannelGroup[]>([])
const channelOptionsLoading = ref(false)
const selectedChannel = ref<string>('')
const customRange = ref<[string, string] | null>(null)
const tables = ref<YejiTable[]>([])
const YEJI_REVISIT_SLOT_CAP = 30

function maxFromRevisitSlots(rs: unknown, m: number): number {
    if (!rs || typeof rs !== 'object') {
        return m
    }
    for (const k of Object.keys(rs as Record<string, unknown>)) {
        const num = parseInt(k, 10)
        if (!Number.isNaN(num) && num >= 2) {
            m = Math.max(m, num)
        }
    }
    return m
}

function maxRevisitSlotInYejiTables(tbs: YejiTable[]): number {
    let mm = 0
    for (const tb of tbs) {
        for (const r of tb.rows) {
            mm = maxFromRevisitSlots(r.revisit_slots, mm)
        }
        mm = maxFromRevisitSlots(tb.total?.revisit_slots, mm)
    }
    return mm
}

const yejiRevisitSlotColumns = computed(() => {
    const max = Math.min(maxRevisitSlotInYejiTables(tables.value), YEJI_REVISIT_SLOT_CAP)
    if (max < 2) {
        return []
    }
    return Array.from({ length: max - 1 }, (_, i) => i + 2)
})

function maxRevisitSlotInLeaderboardLb(lb: LeaderboardPack['leaderboards'][number]): number {
    if (!lb.er_center_subtree) {
        return 0
    }
    let mm = 0
    for (const r of lb.rows) {
        mm = maxFromRevisitSlots(r.revisit_slots, mm)
    }
    return mm
}

function leaderboardRevisitSlotColumnsForLb(lb: LeaderboardPack['leaderboards'][number]): number[] {
    const max = Math.min(maxRevisitSlotInLeaderboardLb(lb), YEJI_REVISIT_SLOT_CAP)
    if (max < 2) {
        return []
    }
    return Array.from({ length: max - 1 }, (_, i) => i + 2)
}

function yejiRevisitSlotColumnTitle(slot: number): string {
    if (slot === 2) {
        return '复诊2'
    }
    const cn = ['三', '四', '五', '六', '七', '八', '九', '十']
    if (slot >= 3 && slot <= 10) {
        return `复诊${cn[slot - 3]}`
    }
    return `复诊${slot}`
}

function yejiRevisitSlotAt(
    row:
        | Pick<YejiRow, 'revisit_slots'>
        | Pick<YejiTable['total'], 'revisit_slots'>
        | Pick<LeaderboardPack['leaderboards'][0]['rows'][0], 'revisit_slots'>,
    slot: number
): number {
    const rs = row?.revisit_slots
    if (!rs || typeof rs !== 'object') {
        return 0
    }
    const v = rs[slot] ?? rs[String(slot)]
    const n = Number(v)
    return Number.isFinite(n) ? n : 0
}

const leaderboardBlock = ref<LeaderboardPack | null>(null)
const leaderboardsLoading = ref(false)

const unassignedDialogVisible = ref(false)
const unassignedDialogLoading = ref(false)
const unassignedDialogSubtitle = ref('')
const unassignedDialogNote = ref('')
const unassignedDialogRows = ref<YejiUnassignedBreakdownRow[]>([])

const revisitBreakdownVisible = ref(false)
const revisitBreakdownLoading = ref(false)
const revisitBreakdownSubtitle = ref('')
const revisitBreakdownNote = ref('')
const revisitBreakdownRows = ref<YejiRevisitBreakdownRow[]>([])
const revisitBreakdownMeta = ref<{ start: string; end: string; revisitSlot: number } | null>(null)

const leadLinesDialogVisible = ref(false)
const leadLinesLoading = ref(false)
const leadLinesSubtitle = ref('')
const leadLinesApiNote = ref('')
const leadLinesRows = ref<YejiLeadLineRow[]>([])
const leadLinesCount = ref(0)
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_matrix'>('zyyt')

/** 与菜单 stats.doctorDailyStats/overview 一致；无权限则不展示医生统计表/图、不请求接口 */
const userStore = useUserStore()
const DOCTOR_DAILY_STATS_PERM = 'stats.doctorDailyStats/overview'
const canViewDoctorDailyStats = computed(() => {
    const perms = userStore.perms || []
    return perms.some(p => p === '*' || p === DOCTOR_DAILY_STATS_PERM)
})

const leaderboardChartBlocks = computed(() =>
    (leaderboardBlock.value?.leaderboards ?? []).filter(lb => lb.rows.length > 0)
)

function yejiDeptChartHasData(tb: YejiTable): boolean {
    return tb.rows.some(
        r =>
            r.dept_id > 0 &&
            (Number(r.lead_count) > 0 ||
                Number(r.consult_count) > 0 ||
                Number(r.deal_order_count) > 0 ||
                Number(r.performance_amount) > 0 ||
                Number(r.completed_performance_amount) > 0)
    )
}

/** 医生统计（与筛选项联动） */
const doctorDailyLoading = ref(false)
const doctorDailyRows = ref<Array<Record<string, any>>>([])
const doctorDailyTotal = ref<Record<string, any>>({})
const doctorDailyRange = ref<{ start: string; end: string } | null>(null)

const doctorDailyRangeText = computed(() => {
    const r = doctorDailyRange.value
    if (!r?.start || !r?.end) return ''
    return r.start === r.end ? `区间 ${r.start}` : `区间 ${r.start} ～ ${r.end}`
})

const doctorDealBarHasData = computed(() =>
    doctorDailyRows.value.some(r => Number(r.deal_amount ?? 0) > 0)
)

const doctorRxStackHasData = computed(() =>
    doctorDailyRows.value.some(
        r =>
            Number(r.system_prescription_count ?? 0) > 0 ||
            Number(r.manual_prescription_count ?? 0) > 0
    )
)

const doctorChartsHasData = computed(() => {
    if (!canViewDoctorDailyStats.value) {
        return false
    }
    return (
        doctorDealBarHasData.value ||
        doctorRxStackHasData.value ||
        doctorAppointmentPieHasData.value ||
        doctorScatterHasData.value ||
        doctorRadarHasData.value
    )
})

const chartsHasAnyData = computed(() => {
    if (doctorChartsHasData.value) {
        return true
    }
    for (const tb of tables.value) {
        if (yejiDeptChartHasData(tb)) {
            return true
        }
    }
    if (leaderboardChartBlocks.value.length > 0) {
        return true
    }
    return false
})

const doctorAppointmentPieHasData = computed(() => {
    const t = doctorDailyTotal.value
    return (
        Number(t.appointment_completed ?? 0) +
            Number(t.appointment_missed ?? 0) +
            Number(t.appointment_cancelled ?? 0) >
        0
    )
})

const doctorAppointmentPieOption = computed(() => {
    const t = doctorDailyTotal.value
    const data = [
        { value: Number(t.appointment_completed ?? 0), name: '挂号完成' },
        { value: Number(t.appointment_missed ?? 0), name: '过号' },
        { value: Number(t.appointment_cancelled ?? 0), name: '取消' },
    ].filter(d => d.value > 0)
    return {
        color: ['#0d9488', '#d97706', '#94a3b8'],
        tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
        legend: { bottom: 0, type: 'scroll', textStyle: { fontSize: 10 } },
        series: [
            {
                name: '挂号',
                type: 'pie',
                radius: ['42%', '68%'],
                center: ['50%', '46%'],
                avoidLabelOverlap: true,
                itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
                label: { formatter: '{b}\n{c}', fontSize: 11 },
                data,
            },
        ],
    }
})

const doctorScatterHasData = computed(
    () =>
        doctorDailyRows.value.filter(
            r => Number(r.deal_order_count ?? 0) > 0 && Number(r.deal_amount ?? 0) > 0
        ).length >= 2
)

const doctorScatterOption = computed(() => {
    const rows = doctorDailyRows.value.filter(
        r => Number(r.deal_order_count ?? 0) > 0 && Number(r.deal_amount ?? 0) > 0
    )
    const maxAmt = Math.max(...rows.map(r => Number(r.deal_amount)), 1)
    const data = rows.map(r => {
        const amt = Number(r.deal_amount)
        return {
            name: String(r.doctor_name || '—'),
            value: [Number(r.deal_order_count), amt],
            symbolSize: 12 + Math.sqrt(amt / maxAmt) * 26,
        }
    })
    return {
        color: ['#2563eb'],
        tooltip: {
            trigger: 'item',
            formatter: (p: any) => {
                const v = p.data?.value as number[] | undefined
                if (!Array.isArray(v)) return ''
                return `${p.data.name as string}<br/>接诊诊单：${v[0]}<br/>金额：${Number(v[1]).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} 元`
            },
        },
        grid: { left: 48, right: 24, top: 28, bottom: 32, containLabel: true },
        xAxis: {
            type: 'value',
            name: '接诊诊单',
            nameLocation: 'middle',
            nameGap: 28,
            splitLine: { lineStyle: { type: 'dashed', opacity: 0.55 } },
        },
        yAxis: {
            type: 'value',
            name: '成交金额',
            nameLocation: 'middle',
            nameGap: 40,
            splitLine: { lineStyle: { type: 'dashed', opacity: 0.55 } },
        },
        series: [
            {
                type: 'scatter',
                data,
            },
        ],
    }
})

const doctorRadarHasData = computed(() => {
    const rows = doctorDailyRows.value.filter(
        r =>
            Number(r.deal_amount ?? 0) +
                Number(r.deal_order_count ?? 0) +
                Number(r.system_prescription_count ?? 0) +
                Number(r.manual_prescription_count ?? 0) >
            0
    )
    return rows.length >= 2
})

const doctorRadarOption = computed(() => {
    const rows = [...doctorDailyRows.value]
        .filter(
            r =>
                Number(r.deal_amount ?? 0) +
                    Number(r.deal_order_count ?? 0) +
                    Number(r.system_prescription_count ?? 0) +
                    Number(r.manual_prescription_count ?? 0) >
                0
        )
        .sort((a, b) => Number(b.deal_amount) - Number(a.deal_amount))
        .slice(0, 5)
    if (rows.length < 2) {
        return { radar: { indicator: [] }, series: [{ type: 'radar', data: [] }] }
    }
    const lab = (r: Record<string, any>) => {
        const n = String(r.doctor_name || '—')
        return n.length > 7 ? `${n.slice(0, 7)}…` : n
    }
    const rx = (r: Record<string, any>) =>
        Number(r.system_prescription_count ?? 0) + Number(r.manual_prescription_count ?? 0)
    const avg = (r: Record<string, any>) => {
        const v = Number(r.avg_deal_amount)
        if (Number.isFinite(v) && v > 0) return v
        const o = Number(r.deal_order_count)
        return o > 0 ? Number(r.deal_amount) / o : 0
    }
    const maxDeal = Math.max(...rows.map(r => Number(r.deal_amount)), 1e-9)
    const maxOrd = Math.max(...rows.map(r => Number(r.deal_order_count)), 1e-9)
    const maxRx = Math.max(...rows.map(rx), 1e-9)
    const maxAp = Math.max(...rows.map(r => Number(r.appointment_completed ?? 0)), 1e-9)
    const maxAvg = Math.max(...rows.map(avg), 1e-9)

    const data = rows.map(r => ({
        name: lab(r),
        value: [
            (Number(r.deal_amount) / maxDeal) * 100,
            (Number(r.deal_order_count) / maxOrd) * 100,
            (rx(r) / maxRx) * 100,
            (Number(r.appointment_completed ?? 0) / maxAp) * 100,
            (avg(r) / maxAvg) * 100,
        ],
    }))

    return {
        color: ['#2563eb', '#0d9488', '#d97706', '#7c3aed', '#64748b'],
        tooltip: { trigger: 'item' },
        legend: { bottom: 0, type: 'scroll', textStyle: { fontSize: 10 } },
        radar: {
            indicator: [
                { name: '成交金额', max: 100 },
                { name: '接诊诊单', max: 100 },
                { name: '开方量', max: 100 },
                { name: '挂号完成', max: 100 },
                { name: '客单价', max: 100 },
            ],
            radius: '58%',
            center: ['50%', '48%'],
            splitNumber: 4,
            axisName: { fontSize: 10, color: '#64748b' },
        },
        series: [
            {
                type: 'radar',
                data,
                emphasis: {
                    lineStyle: { width: 3 },
                },
            },
        ],
    }
})

const doctorDealBarOption = computed(() => {
    const rows = [...doctorDailyRows.value]
        .filter(r => Number(r.deal_amount ?? 0) > 0)
        .sort((a, b) => Number(b.deal_amount) - Number(a.deal_amount))
        .slice(0, 20)
    const names = rows.map(r => {
        const n = String(r.doctor_name || '—')
        return n.length > 10 ? `${n.slice(0, 10)}…` : n
    })
    const vals = rows.map(r => Number(r.deal_amount ?? 0))
    return {
        color: ['#2563eb'],
        tooltip: {
            trigger: 'axis',
            axisPointer: { type: 'shadow' },
            valueFormatter: (v: number) =>
                `${v.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} 元`,
        },
        grid: { left: 8, right: 16, top: 28, bottom: 8, containLabel: true },
        xAxis: {
            type: 'value',
            axisLabel: { fontSize: 11 },
            splitLine: { lineStyle: { type: 'dashed', opacity: 0.6 } },
        },
        yAxis: {
            type: 'category',
            data: [...names].reverse(),
            axisLabel: { fontSize: 11 },
        },
        series: [
            {
                name: '成交金额',
                type: 'bar',
                data: [...vals].reverse(),
                barMaxWidth: 22,
                itemStyle: { borderRadius: [0, 6, 6, 0] },
            },
        ],
    }
})

const doctorRxStackOption = computed(() => {
    const rows = [...doctorDailyRows.value]
        .filter(
            r =>
                Number(r.system_prescription_count ?? 0) + Number(r.manual_prescription_count ?? 0) >
                0
        )
        .sort(
            (a, b) =>
                Number(b.system_prescription_count ?? 0) +
                Number(b.manual_prescription_count ?? 0) -
                (Number(a.system_prescription_count ?? 0) + Number(a.manual_prescription_count ?? 0))
        )
        .slice(0, 14)
    const cats = rows.map(r => {
        const n = String(r.doctor_name || '—')
        return n.length > 6 ? `${n.slice(0, 6)}…` : n
    })
    return {
        tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
        legend: { top: 0, textStyle: { fontSize: 11 } },
        grid: { left: 44, right: 16, top: 32, bottom: cats.length > 10 ? 72 : 28 },
        xAxis: {
            type: 'category',
            data: cats,
            axisLabel: { rotate: cats.length > 8 ? 32 : 0, fontSize: 10 },
        },
        yAxis: {
            type: 'value',
            splitLine: { lineStyle: { type: 'dashed', opacity: 0.6 } },
        },
        series: [
            {
                name: '系统开方',
                type: 'bar',
                stack: 'rx',
                barMaxWidth: 18,
                data: rows.map(r => Number(r.system_prescription_count ?? 0)),
                itemStyle: { color: '#2563eb' },
            },
            {
                name: '手动开方',
                type: 'bar',
                stack: 'rx',
                barMaxWidth: 18,
                data: rows.map(r => Number(r.manual_prescription_count ?? 0)),
                itemStyle: { color: '#d97706' },
            },
        ],
    }
})

function buildDoctorDailyRequestParams(): Record<string, any> {
    const p: Record<string, any> = {}
    if (selectedChannel.value) {
        p.channel_code = selectedChannel.value
    }
    if (customRange.value?.[0] && customRange.value?.[1]) {
        p.start_date = customRange.value[0]
        p.end_date = customRange.value[1]
    } else {
        const ymd = formatLocalYmd(new Date())
        p.start_date = ymd
        p.end_date = ymd
    }

    return p
}

function formatDoctorDailyMoney(n: number | string | undefined) {
    const x = Number(n ?? 0)

    return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}

function getDoctorDailySummaries(param: { columns: any[] }) {
    const t = doctorDailyTotal.value
    const sums: string[] = []
    param.columns.forEach((column, index) => {
        if (index === 0) {
            sums[index] = '合计'

            return
        }
        switch (column.property) {
            case 'system_prescription_count':
                sums[index] = String(t.system_prescription_count ?? 0)
                break
            case 'manual_prescription_count':
                sums[index] = String(t.manual_prescription_count ?? 0)
                break
            case 'deal_amount':
                sums[index] = `¥ ${formatDoctorDailyMoney(t.deal_amount)}`
                break
            case 'deal_order_count':
                sums[index] = String(t.deal_order_count ?? 0)
                break
            case 'appointment_completed':
                sums[index] = String(t.appointment_completed ?? 0)
                break
            case 'appointment_missed':
                sums[index] = String(t.appointment_missed ?? 0)
                break
            case 'appointment_cancelled':
                sums[index] = String(t.appointment_cancelled ?? 0)
                break
            default:
                sums[index] = ''
        }
    })

    return sums
}

async function loadDoctorDailyStats() {
    doctorDailyLoading.value = true
    try {
        const res: any = await doctorDailyStatsOverview(buildDoctorDailyRequestParams())
        doctorDailyRows.value = res?.rows ?? []
        doctorDailyTotal.value = res?.total ?? {}
        if (res?.start_date && res?.end_date) {
            doctorDailyRange.value = { start: res.start_date, end: res.end_date }
        }
    } catch (_e) {
        doctorDailyRows.value = []
        doctorDailyTotal.value = {}
        doctorDailyRange.value = null
    } finally {
        doctorDailyLoading.value = false
    }
}

// ── 业务订单侧栏（末级部门 / 医助行点击，接口同处方业务订单列表） ──
const orderDrawerVisible = ref(false)
const orderDrawerLoading = ref(false)
const orderDrawerTitle = ref('')
const orderDrawerHint = ref('')
const orderDrawerLists = ref<Array<Record<string, any>>>([])
const orderDrawerCount = ref(0)
const orderDrawerPage = ref(1)
const orderDrawerPageSize = ref(15)
/** 侧栏列表：有渠道筛选时，合计业绩=全量订单 / 渠道=收窄订单 */
const orderDrawerListTab = ref<'total' | 'channel'>('channel')
/** 渠道模式下接口返回的合计 vs 渠道业绩快照（切换 Tab 时用于对照卡片） */
const orderDrawerSplitPerfCache = ref<{
    totalAmount: number
    totalOrderCount: number
    channelAmount: number
    channelOrderCount: number
} | null>(null)

type OrderDrawerFilter =
    | {
          mode: 'dept'
          deptId: number
          deptName: string
          start: string
          end: string
          /** 当前业绩表各展示行 dept_id，用于侧栏与「接诊诊单」列对齐（创建人优先 + 最深展示行） */
          yejiTableRowDeptIds?: number[]
      }
    | {
          mode: 'assistant'
          assistantId: number
          assistantName: string
          start: string
          end: string
          /** 未传=普通全量业务单；传 0=二中心复诊全部；≥2=对应复诊分项 */
          erCenterRevisitSlot?: number
      }

const orderDrawerFilter = ref<OrderDrawerFilter | null>(null)
/** 列表接口 extend，含 stats_order_amount_performance（与当前筛选、不含已取消口径一致） */
const orderDrawerExtend = ref<Record<string, any> | null>(null)

const orderDrawerTotalAmount = computed(() => {
    const ex = orderDrawerExtend.value
    if (!ex) return null
    const raw = ex.stats_order_amount_performance ?? ex.stats_order_amount_not_cancelled
    if (raw === undefined || raw === null || raw === '') return null
    const n = Number(raw)
    return Number.isFinite(n) ? n : null
})

/** 约诊/接诊条数（与业绩看板「预约诊单」同口径，来自列表 extend） */
const orderDrawerConsultCount = computed(() => {
    const ex = orderDrawerExtend.value
    if (!ex || ex.stats_yeji_consult_count === undefined || ex.stats_yeji_consult_count === null) {
        return null
    }
    const n = Number(ex.stats_yeji_consult_count)
    return Number.isFinite(n) ? n : null
})

const orderDrawerPageAmountSum = computed(() =>
    orderDrawerLists.value.reduce((s, row) => s + Number(row.amount ?? 0), 0)
)

const orderDrawerChannelSplitActive = computed(
    () => Boolean(selectedChannel.value) && orderDrawerVisible.value
)

const orderDrawerChannelLabel = computed(() => {
    const code = selectedChannel.value
    if (!code) return ''
    for (const g of channelGroups.value) {
        const c = g.channels?.find((x: ChannelOption) => x.channel_code === code)
        if (c?.channel_name) return String(c.channel_name)
    }
    return code
})

const orderDrawerChannelStatAmount = computed(() => {
    const c = orderDrawerSplitPerfCache.value
    if (c && orderDrawerListTab.value === 'total') {
        return Number.isFinite(c.channelAmount) ? c.channelAmount : null
    }
    return orderDrawerTotalAmount.value
})

// ── 目标进度卡片 ──────────────────────────────
interface TargetProgressSection {
    dept_id: number
    dept_name: string
    /** 组织树上直接上级部门名称（便于区分同名组） */
    parent_dept_name: string
    target_amount: number
    total_amount: number
    yesterday_amount: number
    douyin_amount: number | null   // 名称含「自媒体1」的子部门合计
    tuifen_amount: number | null   // 名称含「自媒体3」或「自媒体4」的子部门合计
    target_progress: number        // 按已过天数均摊的目标进度
    completion_rate: number | null // 总诊金 / 目标金额
}
interface TargetProgressCard {
    title: string
    sections: TargetProgressSection[]
}
const targetProgressCard = ref<TargetProgressCard | null>(null)
const targetProgressLoading = ref(false)

function ymCurrent(): string {
    const d = new Date()
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
function daysInMonth(ym: string): number {
    const [y, m] = ym.split('-').map(Number)
    return new Date(y, m, 0).getDate()
}

async function loadTargetProgress() {
    targetProgressLoading.value = true
    targetProgressCard.value = null
    try {
        const ym = ymCurrent()
        const targetRes: any = await deptPerformanceTargetMonthMatrix({ year_month: ym })
        const treeRows: any[] = targetRes?.rows ?? []
        if (!treeRows.length) return

        // 从目标树平铺，找到 depth>=1 且目标>0 的节点作为卡片分区
        function flatWithDepth(nodes: any[], depth = 0): Array<{ node: any; depth: number }> {
            const r: Array<{ node: any; depth: number }> = []
            for (const n of nodes ?? []) {
                r.push({ node: n, depth })
                r.push(...flatWithDepth(n.children ?? [], depth + 1))
            }
            return r
        }
        const all = flatWithDepth(treeRows)
        const cardDepts = all
            .filter(x => x.depth >= 1 && Number(x.node.target_amount) > 0)
            .map(x => x.node)
        if (!cardDepts.length) return

        function resolveParentDeptName(deptId: number): string {
            const self = deptOptions.value.find(d => Number(d.id) === deptId)
            if (!self) return ''
            const pid = Number(self.pid)
            if (!pid) return ''
            const parent = deptOptions.value.find(d => Number(d.id) === pid)
            return parent?.name ? String(parent.name) : ''
        }

        // 用 deptOptions（完整部门列表，含 pid）做层级查找
        // 目标树只包含有目标的节点，无法得到 0 目标的子部门（如自媒体1/3/4）
        function descIdsFromOptions(deptId: number): number[] {
            const result: number[] = []
            const queue = [deptId]
            while (queue.length) {
                const pid = queue.shift()!
                for (const d of deptOptions.value) {
                    if (Number(d.pid) === pid) {
                        result.push(d.id)
                        queue.push(d.id)
                    }
                }
            }
            return result
        }

        const allIds = new Set<number>()
        const specs = cardDepts.map(dept => {
            const deptId = Number(dept.dept_id)
            const childIds = descIdsFromOptions(deptId)
            const totalIds = childIds.length ? childIds : [deptId]
            totalIds.forEach(id => allIds.add(id))
            return {
                dept_id: deptId,
                dept_name: String(dept.dept_name),
                target_amount: Number(dept.target_amount),
                totalIds,
            }
        })

        if (!allIds.size) return

        // 从 channelGroups 找自媒体1/3/4 的 channel_code
        const allChannels = channelGroups.value.flatMap(g => g.channels)
        const douyinCode = allChannels.find(c => c.channel_name.includes('自媒体1'))?.channel_code
        const t3Code = allChannels.find(c => c.channel_name.includes('自媒体3'))?.channel_code
        const t4Code = allChannels.find(c => c.channel_name.includes('自媒体4'))?.channel_code

        const deptIdsStr = Array.from(allIds).join(',')

        // 并发拉取：无渠道（总诊金/昨日）+ 各渠道（completed_performance_amount）
        const [perfRes, douyinRes, t3Res, t4Res] = await Promise.all([
            yejiStatsMulti({ dept_ids: deptIdsStr }) as any,
            douyinCode ? yejiStatsMulti({ dept_ids: deptIdsStr, channel_code: douyinCode }) as any : Promise.resolve(null),
            t3Code ? yejiStatsMulti({ dept_ids: deptIdsStr, channel_code: t3Code }) as any : Promise.resolve(null),
            t4Code ? yejiStatsMulti({ dept_ids: deptIdsStr, channel_code: t4Code }) as any : Promise.resolve(null),
        ])

        const getMonthTbl = (res: any) => (res?.tables ?? []).find((t: any) => String(t.label).includes('月')) ?? res?.tables?.[0]
        const getYdayTbl  = (res: any) => (res?.tables ?? []).find((t: any) => String(t.label).includes('昨')) ?? res?.tables?.[res?.tables?.length - 1]

        function buildMap(tbl: any, field: string): Map<number, number> {
            const m = new Map<number, number>()
            for (const r of tbl?.rows ?? []) m.set(Number(r.dept_id), Number(r[field] ?? 0))
            return m
        }

        const monthPerf  = buildMap(getMonthTbl(perfRes),   'performance_amount')
        const ydayPerf   = buildMap(getYdayTbl(perfRes),    'performance_amount')
        const douyinPerf = douyinRes ? buildMap(getMonthTbl(douyinRes), 'completed_performance_amount') : null
        const t3Perf     = t3Res     ? buildMap(getMonthTbl(t3Res),     'completed_performance_amount') : null
        const t4Perf     = t4Res     ? buildMap(getMonthTbl(t4Res),     'completed_performance_amount') : null

        const sumIds = (m: Map<number, number>, ids: number[]) => ids.reduce((s, id) => s + (m.get(id) ?? 0), 0)

        const today = new Date()
        const totalDays = daysInMonth(ym)
        const elapsed = today.getDate()

        const sections: TargetProgressSection[] = specs.map(spec => {
            const total     = sumIds(monthPerf, spec.totalIds)
            const yesterday = sumIds(ydayPerf,  spec.totalIds)

            const douyinAmt = douyinPerf ? sumIds(douyinPerf, spec.totalIds) : null
            const t3Amt     = t3Perf     ? sumIds(t3Perf,     spec.totalIds) : null
            const t4Amt     = t4Perf     ? sumIds(t4Perf,     spec.totalIds) : null
            // 推粉 = 自媒体3 + 自媒体4，有任一即显示
            const tuifenAmt = (t3Amt !== null || t4Amt !== null) ? (t3Amt ?? 0) + (t4Amt ?? 0) : null

            const targetProgress = Math.round(spec.target_amount * (elapsed / totalDays) * 100) / 100
            const completion = spec.target_amount > 0 ? total / spec.target_amount : null
            return {
                dept_id: spec.dept_id,
                dept_name: spec.dept_name,
                parent_dept_name: resolveParentDeptName(spec.dept_id),
                target_amount: spec.target_amount,
                total_amount: total,
                yesterday_amount: yesterday,
                douyin_amount: douyinAmt && douyinAmt > 0 ? douyinAmt : null,
                tuifen_amount: tuifenAmt && tuifenAmt > 0 ? tuifenAmt : null,
                target_progress: targetProgress,
                completion_rate: completion,
            }
        })

        // 取根部门名作为卡片标题前缀
        const rootName = treeRows[0]?.dept_name ?? ''
        const m = today.getMonth() + 1
        const d = today.getDate()
        targetProgressCard.value = {
            title: `${rootName}诊金-${m}.${d}`,
            sections,
        }
    } catch {
        // 静默失败，不影响主页面
    } finally {
        targetProgressLoading.value = false
    }
}

const channelFilterNote = computed(() => {
    for (const t of tables.value) {
        if (t.channel_filter_note) return t.channel_filter_note
    }
    return ''
})

const leaderboardVisibleTables = computed(() => {
    const list = leaderboardBlock.value?.leaderboards
    if (!list) return 0
    return list.filter((x) => x.rows.length > 0).length
})

const dateShortcuts = [
    {
        text: '昨天',
        value: () => {
            const d = new Date()
            d.setDate(d.getDate() - 1)
            return [d, d]
        },
    },
    {
        text: '最近七天',
        value: () => {
            const e = new Date()
            const s = new Date()
            s.setDate(s.getDate() - 6)
            return [s, e]
        },
    },
    {
        text: '本月',
        value: () => {
            const e = new Date()
            const s = new Date(e.getFullYear(), e.getMonth(), 1)
            return [s, e]
        },
    },
    {
        text: '今日',
        value: () => {
            const d = new Date()
            return [d, d]
        },
    },
    {
        text: '本周',
        value: () => {
            const e = new Date()
            const s = new Date()
            const day = s.getDay() || 7
            s.setDate(s.getDate() - (day - 1))
            return [s, e]
        },
    },
    {
        text: '上月',
        value: () => {
            const e0 = new Date()
            const s = new Date(e0.getFullYear(), e0.getMonth() - 1, 1)
            const e = new Date(e0.getFullYear(), e0.getMonth(), 0)
            return [s, e]
        },
    },
]

function applyDateShortcut(shortcut: (typeof dateShortcuts)[number]) {
    const pair = shortcut.value() as [Date, Date]
    customRange.value = [formatLocalYmd(pair[0]), formatLocalYmd(pair[1])]
    loadData()
}

async function loadDeptOptions() {
    deptOptionsLoading.value = true
    try {
        const res: DeptOption[] = await yejiStatsDeptOptions()
        deptOptions.value = (res || []).sort((a, b) => {
            const aIsCenter = a.name.includes('中心') ? -1 : 1
            const bIsCenter = b.name.includes('中心') ? -1 : 1
            if (aIsCenter !== bIsCenter) return aIsCenter - bIsCenter
            return a.id - b.id
        })
    } catch (_e) {
        // ignore
    } finally {
        deptOptionsLoading.value = false
    }
}

async function loadChannelOptions() {
    channelOptionsLoading.value = true
    try {
        const res: any = await yejiStatsChannelOptions()
        const groups: ChannelGroup[] = res?.groups || []
        channelGroups.value = groups
    } catch (_e) {
        // ignore
    } finally {
        channelOptionsLoading.value = false
    }
}

async function loadLeaderboard(range: { start: string; end: string }) {
    leaderboardsLoading.value = true
    try {
        const p: Record<string, any> = {
            start_date: range.start,
            end_date: range.end,
        }
        if (selectedDeptIds.value.length > 0) {
            p.dept_ids = selectedDeptIds.value.join(',')
        }
        if (selectedChannel.value) {
            p.channel_code = selectedChannel.value
        }
        const res: any = await yejiStatsLeaderboard(p as any)
        leaderboardBlock.value = {
            start_date: res.start_date,
            end_date: res.end_date,
            channel_code: res.channel_code || '',
            channel_name: res.channel_name || '',
            channel_filter_note: res.channel_filter_note || '',
            range_note: res.range_note || '',
            leaderboards: res.leaderboards || [],
        }
    } catch (_e) {
        leaderboardBlock.value = null
    } finally {
        leaderboardsLoading.value = false
    }
}

async function loadData() {
    loading.value = true
    leaderboardBlock.value = null
    try {
        const baseParams: Record<string, any> = {}
        if (selectedDeptIds.value.length > 0) {
            baseParams.dept_ids = selectedDeptIds.value.join(',')
        }
        if (selectedChannel.value) {
            baseParams.channel_code = selectedChannel.value
        }

        // 自定义日期 → 单表 overview 模式；否则 → 多表 multi
        if (customRange.value && customRange.value[0] && customRange.value[1]) {
            const [s, e] = customRange.value
            const params: Record<string, any> = { ...baseParams, start_date: s, end_date: e }
            const res: any = await yejiStatsOverview(params as any)
            tables.value = [
                {
                    label: s === e ? s : `${s} ~ ${e}`,
                    start_date: res.start_date,
                    end_date: res.end_date,
                    channel_code: res.channel_code || '',
                    channel_name: res.channel_name || '',
                    channel_group: res.channel_group || '',
                    channel_filter_note: res.channel_filter_note || '',
                    rows: res.rows || [],
                    total: res.total,
                },
            ]
            await loadLeaderboard({ start: res.start_date, end: res.end_date })
            return
        }

        const res: any = await yejiStatsMulti(baseParams as any)
        tables.value = (res?.tables || []).map((t: any) => ({
            label: t.label,
            start_date: t.start_date,
            end_date: t.end_date,
            channel_code: t.channel_code || '',
            channel_name: t.channel_name || '',
            channel_group: t.channel_group || '',
            channel_filter_note: t.channel_filter_note || '',
            rows: t.rows || [],
            total: t.total,
        }))
        const ymd = formatLocalYmd(new Date())
        await loadLeaderboard({ start: ymd, end: ymd })
    } catch (e: any) {
        ElMessage.error(e?.msg || e?.message || '加载失败')
    } finally {
        loading.value = false
        if (canViewDoctorDailyStats.value) {
            void loadDoctorDailyStats()
        } else {
            doctorDailyRows.value = []
            doctorDailyTotal.value = {}
            doctorDailyRange.value = null
        }
    }
}

function handleReset() {
    selectedDeptIds.value = []
    customRange.value = null
    selectedChannel.value = ''
    loadData()
}

/** 是否存在子部门（用于下钻 / 末级打开业务订单侧栏） */
function deptHasChildren(deptId: number): boolean {
    if (deptId <= 0) return false
    return deptOptions.value.some(d => Number(d.pid) === deptId)
}

function orderFulfillmentBrief(s: unknown): string {
    const m: Record<number, string> = {
        1: '待双审通过',
        2: '待发货',
        3: '已完成',
        4: '已取消',
        5: '已发货',
        6: '已签收',
        7: '进行中',
        8: '暂不制药',
        9: '拒收',
        10: '退款',
        11: '保留药方',
        12: '制药缓发',
    }
    return m[Number(s)] ?? '—'
}

function formatOrderDrawerMoney(v: number | null | undefined) {
    if (v === null || v === undefined || !Number.isFinite(Number(v))) {
        return '—'
    }
    return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}

function orderDrawerIndexMethod(index: number) {
    return (orderDrawerPage.value - 1) * orderDrawerPageSize.value + index + 1
}

/** 挂号展示：预约日期 + 时段 + 时点 */
function orderDrawerApptSchedule(a: Record<string, any>): string {
    const date = a?.appointment_date != null && String(a.appointment_date) !== '' ? String(a.appointment_date) : ''
    const period = a?.period != null && String(a.period) !== '' ? String(a.period) : ''
    const time = a?.appointment_time != null && String(a.appointment_time) !== '' ? String(a.appointment_time) : ''
    const tail = [period, time].filter(Boolean).join(' ')
    if (date && tail) return `${date} ${tail}`
    return date || tail || '—'
}

/** 挂号 channels 字典 value（后端 channel_dict_value，与业绩/进线所用字典同源）；无数值则空串 */
function orderDrawerLinkedApptChannelDictValue(a: Record<string, any> | null | undefined): string {
    if (!a) return ''
    const v = a.channel_dict_value
    if (v !== null && v !== undefined && v !== '') {
        const n = Number(v)
        if (Number.isFinite(n)) {
            return String(Math.trunc(n))
        }
    }
    const raw = a.channel_source
    if (raw === null || raw === undefined) return ''
    const s = String(raw).trim()
    if (s === '' || !/^\d+$/.test(s)) return ''
    return String(parseInt(s, 10))
}

function formatOrderDrawerTime(t: unknown): string {
    if (t == null || t === '') return '—'
    const n = Number(t)
    if (Number.isFinite(n) && n >= 1e9 && n < 1e15) {
        const ms = n < 1e12 ? n * 1000 : n
        const d = new Date(ms)
        if (!Number.isNaN(d.getTime())) {
            const y = d.getFullYear()
            const m = String(d.getMonth() + 1).padStart(2, '0')
            const day = String(d.getDate()).padStart(2, '0')
            const h = String(d.getHours()).padStart(2, '0')
            const min = String(d.getMinutes()).padStart(2, '0')
            return `${y}-${m}-${day} ${h}:${min}`
        }
    }
    const s = String(t)
    return s.length >= 16 ? s.slice(0, 16) : s
}

/** 业务订单侧栏列表筛选（不含分页） */
function buildOrderDrawerListParams(): Record<string, any> | null {
    const f = orderDrawerFilter.value
    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,
        /**
         * 业绩看板入口标志：后端 PrescriptionOrderLists::applyDoctorAssistantFilters 看到此标志后，
         * assistant_id / assistant_dept_id 一律按订单创建人收窄（与表格诊金/合计业绩/复诊同口径），
         * 而非默认的「创建人 ∪ 诊单医助」并集。同时启用 extend 字段（约诊数、合计业绩对照）。
         */
        yeji_order_drawer: 1,
    }
    if (f.mode === 'dept') {
        params.assistant_dept_id = f.deptId
        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
    try {
        const params: Record<string, any> = {
            ...base,
            page_no: orderDrawerPage.value,
            page_size: orderDrawerPageSize.value,
        }
        const res: any = await prescriptionOrderLists(params)
        orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
        orderDrawerCount.value = Number(res?.count ?? 0)
        orderDrawerExtend.value = res?.extend && typeof res.extend === 'object' ? res.extend : null
        if (
            selectedChannel.value &&
            orderDrawerListTab.value === 'channel' &&
            orderDrawerExtend.value &&
            orderDrawerExtend.value.stats_yeji_drawer_total_performance_amount != null
        ) {
            orderDrawerSplitPerfCache.value = {
                totalAmount: Number(orderDrawerExtend.value.stats_yeji_drawer_total_performance_amount),
                totalOrderCount: Number(
                    orderDrawerExtend.value.stats_yeji_drawer_total_performance_order_count ?? 0
                ),
                channelAmount: Number(orderDrawerExtend.value.stats_order_amount_performance ?? 0),
                channelOrderCount: Number(res?.count ?? 0),
            }
        }
        if (
            selectedChannel.value &&
            orderDrawerVisible.value &&
            orderDrawerListTab.value === 'total'
        ) {
            await hydrateOrderDrawerSplitPerfCache()
        }
    } catch (e: any) {
        ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
        orderDrawerLists.value = []
        orderDrawerCount.value = 0
        orderDrawerExtend.value = null
    } finally {
        orderDrawerLoading.value = false
    }
}

function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: 'total' | 'channel' }) {
    const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
    orderDrawerListTab.value = channelSplit ? opts?.listTab ?? 'channel' : 'channel'
    orderDrawerSplitPerfCache.value = null
    orderDrawerFilter.value = {
        mode: 'dept',
        deptId: row.dept_id,
        deptName: row.dept_name,
        start: tb.start_date,
        end: tb.end_date,
        yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
    }
    orderDrawerTitle.value = `业务订单 · ${row.dept_name}`
    orderDrawerHint.value = `创建时间：${tb.start_date} ～ ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径：订单创建人人事部门 ∈ 本部门子树 · 不含履约已取消`
    orderDrawerPage.value = 1
    orderDrawerVisible.value = true
    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,
        yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
    }
    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) {
    orderDrawerListTab.value = 'channel'
    orderDrawerSplitPerfCache.value = null
    orderDrawerFilter.value = {
        mode: 'assistant',
        assistantId: row.admin_id,
        assistantName: row.name,
        start,
        end,
    }
    orderDrawerTitle.value = `业务订单 · ${row.name}`
    orderDrawerHint.value = `创建时间：${start} ～ ${end} · 创建人=${row.name}（与排行榜诊金同口径）· 不含履约已取消`
    orderDrawerPage.value = 1
    orderDrawerVisible.value = true
    void fetchOrderDrawerPage()
}

function openOrderDrawerByAssistantErCenterRevisit(
    assistant: YejiAssistantDrawerRef,
    start: string,
    end: string,
    revisitSlot: number
) {
    orderDrawerListTab.value = 'channel'
    orderDrawerSplitPerfCache.value = null
    orderDrawerFilter.value = {
        mode: 'assistant',
        assistantId: assistant.admin_id,
        assistantName: assistant.name,
        start,
        end,
        erCenterRevisitSlot: revisitSlot,
    }
    const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
    orderDrawerTitle.value = `业务订单 · ${assistant.name} · ${slotLbl}`
    orderDrawerHint.value = `创建时间：${start} ～ ${end} · 创建人=${assistant.name}（与部门表 / 排行榜复诊同口径）· 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
    orderDrawerPage.value = 1
    orderDrawerVisible.value = true
    void fetchOrderDrawerPage()
}

function onLeaderboardRevisitTotalClick(r: LeaderboardPack['leaderboards'][0]['rows'][0]) {
    if (!r || r.admin_id <= 0 || !leaderboardBlock.value) {
        return
    }
    if (Number(r.revisit_count ?? 0) <= 0) {
        return
    }
    const { start_date, end_date } = leaderboardBlock.value
    openOrderDrawerByAssistantErCenterRevisit(
        { admin_id: r.admin_id, name: r.name },
        start_date,
        end_date,
        0
    )
}

function onLeaderboardRevisitSlotClick(r: LeaderboardPack['leaderboards'][0]['rows'][0], slot: number) {
    if (!r || r.admin_id <= 0 || !leaderboardBlock.value) {
        return
    }
    if (yejiRevisitSlotAt(r, slot) <= 0) {
        return
    }
    const { start_date, end_date } = leaderboardBlock.value
    openOrderDrawerByAssistantErCenterRevisit(
        { admin_id: r.admin_id, name: r.name },
        start_date,
        end_date,
        slot
    )
}

function onOrderDrawerListTabChange() {
    orderDrawerPage.value = 1
    void fetchOrderDrawerPage()
}

function onOrderDrawerPageChange(p: number) {
    orderDrawerPage.value = p
    void fetchOrderDrawerPage()
}

function onOrderDrawerPageSizeChange(size: number) {
    orderDrawerPageSize.value = size
    orderDrawerPage.value = 1
    void fetchOrderDrawerPage()
}

function isUnassignedYejiRow(row: YejiRow): boolean {
    return row.dept_id <= 0 && row.dept_name === '未归属中心'
}

function onYejiRevisitTotalCellClick(tb: YejiTable, row: YejiRow) {
    if (row.dept_id <= 0 || isUnassignedYejiRow(row) || Number(row.revisit_count ?? 0) <= 0) {
        return
    }
    void openYejiRevisitDeptBreakdown(tb, row, 0)
}

function onYejiRevisitSlotCellClick(tb: YejiTable, row: YejiRow, slot: number) {
    if (row.dept_id <= 0 || isUnassignedYejiRow(row) || yejiRevisitSlotAt(row, slot) <= 0) {
        return
    }
    void openYejiRevisitDeptBreakdown(tb, row, slot)
}

function onYejiRevisitBreakdownOrderClick(row: YejiRevisitBreakdownRow) {
    const meta = revisitBreakdownMeta.value
    if (!meta || Number(row.order_count) <= 0) {
        return
    }
    openOrderDrawerByAssistantErCenterRevisit(
        { admin_id: row.admin_id, name: row.name },
        meta.start,
        meta.end,
        meta.revisitSlot
    )
}

async function openYejiRevisitDeptBreakdown(tb: YejiTable, row: YejiRow, revisitSlot: number) {
    const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
    revisitBreakdownSubtitle.value = `${row.dept_name} · ${slotLbl} · ${tb.start_date} ～ ${tb.end_date}`
    revisitBreakdownMeta.value = {
        start: tb.start_date,
        end: tb.end_date,
        revisitSlot,
    }
    revisitBreakdownNote.value = ''
    revisitBreakdownRows.value = []
    revisitBreakdownVisible.value = true
    revisitBreakdownLoading.value = true
    try {
        const p: Record<string, string | number> = {
            start_date: tb.start_date,
            end_date: tb.end_date,
            dept_id: row.dept_id,
            revisit_slot: revisitSlot,
        }
        if (selectedDeptIds.value.length > 0) {
            p.dept_ids = selectedDeptIds.value.join(',')
        }
        if (selectedChannel.value) {
            p.channel_code = selectedChannel.value
        }
        const res: any = await yejiStatsRevisitBreakdown(p as any)
        revisitBreakdownRows.value = Array.isArray(res?.rows) ? res.rows : []
        revisitBreakdownNote.value = typeof res?.note === 'string' ? res.note : ''
    } catch (e: any) {
        ElMessage.error(e?.msg || e?.message || '加载复诊拆解失败')
        revisitBreakdownRows.value = []
        revisitBreakdownNote.value = ''
    } finally {
        revisitBreakdownLoading.value = false
    }
}

function buildUnassignedBreakdownSubtitle(tb: YejiTable): string {
    const parts = [`区间：${tb.start_date} ～ ${tb.end_date}`]
    if (selectedChannel.value) {
        parts.push(`渠道：${tb.channel_name || selectedChannel.value}`)
    }
    if (selectedDeptIds.value.length > 0) {
        parts.push(`已选展示部门 ${selectedDeptIds.value.length} 个`)
    }
    return parts.join(' · ')
}

async function openUnassignedBreakdown(tb: YejiTable) {
    unassignedDialogSubtitle.value = buildUnassignedBreakdownSubtitle(tb)
    unassignedDialogNote.value = ''
    unassignedDialogRows.value = []
    unassignedDialogVisible.value = true
    unassignedDialogLoading.value = true
    try {
        const p: Record<string, string> = {
            start_date: tb.start_date,
            end_date: tb.end_date,
        }
        if (selectedDeptIds.value.length > 0) {
            p.dept_ids = selectedDeptIds.value.join(',')
        }
        if (selectedChannel.value) {
            p.channel_code = selectedChannel.value
        }
        const res: any = await yejiStatsUnassignedBreakdown(p as any)
        unassignedDialogRows.value = Array.isArray(res?.rows) ? res.rows : []
        unassignedDialogNote.value = typeof res?.note === 'string' ? res.note : ''
    } catch (e: any) {
        ElMessage.error(e?.msg || e?.message || '加载未归属拆解失败')
        unassignedDialogRows.value = []
        unassignedDialogNote.value = ''
    } finally {
        unassignedDialogLoading.value = false
    }
}

function onLeadCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) {
    if (row.dept_id <= 0 || Number(row.lead_count) <= 0) {
        return
    }
    ev.stopPropagation()
    void openLeadLinesDialog(tb, row)
}

async function fetchLeadLinesPage() {
    const ctx = leadLinesContext.value
    if (!ctx) {
        return
    }
    const { tb, row } = ctx
    leadLinesLoading.value = true
    try {
        const p: Record<string, string | number> = {
            start_date: tb.start_date,
            end_date: tb.end_date,
            dept_id: row.dept_id,
            page: leadLinesPage.value,
            page_size: leadLinesPageSize.value,
        }
        if (selectedDeptIds.value.length > 0) {
            p.dept_ids = selectedDeptIds.value.join(',')
        }
        if (selectedChannel.value) {
            p.channel_code = selectedChannel.value
        }
        const res: any = await yejiStatsLeadLines(p as any)
        leadLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
        leadLinesCount.value = Number(res?.count ?? 0)
        leadLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
    } catch (e: any) {
        ElMessage.error(e?.msg || e?.message || '加载进线明细失败')
        leadLinesRows.value = []
        leadLinesCount.value = 0
        leadLinesApiNote.value = ''
    } finally {
        leadLinesLoading.value = false
    }
}

async function openLeadLinesDialog(tb: YejiTable, row: YejiRow) {
    leadLinesContext.value = { tb, row }
    leadLinesSubtitle.value = `${row.dept_name} · ${buildUnassignedBreakdownSubtitle(tb)}`
    leadLinesApiNote.value = ''
    leadLinesRows.value = []
    leadLinesCount.value = 0
    leadLinesPage.value = 1
    leadLinesPageSize.value = 20
    leadLinesDialogVisible.value = true
    await fetchLeadLinesPage()
}

function onLeadLinesPageChange(p: number) {
    leadLinesPage.value = p
    void fetchLeadLinesPage()
}

function onLeadLinesPageSizeChange(size: number) {
    leadLinesPageSize.value = size
    leadLinesPage.value = 1
    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()
}

/**
 * 部门业绩行：有子部门 → 仅筛选该节点（后端展开子行）；末级 → 侧栏业务订单；「未归属中心」→ 按诊单医助拆解弹窗
 */
function onYejiDeptRowClick(tb: YejiTable, row: YejiRow) {
    if (isUnassignedYejiRow(row)) {
        void openUnassignedBreakdown(tb)
        return
    }
    if (row.dept_id <= 0) return
    if (deptHasChildren(row.dept_id)) {
        selectedDeptIds.value = [row.dept_id]
        loadData()
        return
    }
    openOrderDrawerByDept(tb, row)
}

/** 医助排行：侧栏按诊单医助展示业务订单 */
function onLeaderboardAssistantRowClick(r: LeaderboardPack['leaderboards'][0]['rows'][0]) {
    if (!r || r.admin_id <= 0 || !leaderboardBlock.value) return
    const { start_date, end_date } = leaderboardBlock.value
    openOrderDrawerByAssistant(r, start_date, end_date)
}

// 表标题：参考截图「【4月】各分部 自媒体1 数据」格式
function buildTableTitle(t: YejiTable): string {
    const channelLbl = t.channel_name ? ` · ${t.channel_name}` : ''
    return `${t.label}${channelLbl}`
}

function getYejiDeptComboOption(tb: YejiTable) {
    const rows = tb.rows.filter(r => r.dept_id > 0)
    const cats = rows.map(r =>
        r.dept_name.length > 10 ? `${r.dept_name.slice(0, 10)}…` : r.dept_name
    )
    const palette = ['#64748b', '#38bdf8', '#6366f1']
    const series: any[] = [
        {
            name: '进线',
            type: 'bar',
            data: rows.map(r => r.lead_count),
            yAxisIndex: 0,
            barMaxWidth: 16,
        },
        {
            name: '接诊',
            type: 'bar',
            data: rows.map(r => r.consult_count),
            yAxisIndex: 0,
            barMaxWidth: 16,
        },
        {
            name: '成交单',
            type: 'bar',
            data: rows.map(r => r.deal_order_count),
            yAxisIndex: 0,
            barMaxWidth: 16,
        },
        {
            name: '合计业绩',
            type: 'bar',
            data: rows.map(r => r.performance_amount),
            yAxisIndex: 1,
            barMaxWidth: 14,
            itemStyle: { color: '#d97706' },
        },
    ]
    if (tb.channel_name) {
        series.push({
            name: `${tb.channel_name}业绩`,
            type: 'bar',
            data: rows.map(r => r.completed_performance_amount),
            yAxisIndex: 1,
            barMaxWidth: 14,
            itemStyle: { color: '#0d9488' },
        })
    }
    const needZoom = rows.length > 10
    return {
        color: [...palette, '#d97706', '#0d9488'],
        tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
        legend: { top: 8, type: 'scroll', textStyle: { fontSize: 11 } },
        grid: {
            left: 52,
            right: 52,
            top: 48,
            bottom: needZoom ? 56 : 28,
            containLabel: false,
        },
        dataZoom: needZoom
            ? [
                  {
                      type: 'slider',
                      bottom: 4,
                      start: 0,
                      end: Math.min(100, Math.ceil((12 / Math.max(rows.length, 1)) * 100)),
                  },
              ]
            : [],
        xAxis: {
            type: 'category',
            data: cats,
            axisLabel: { rotate: cats.length > 8 ? 28 : 0, fontSize: 10 },
        },
        yAxis: [
            {
                type: 'value',
                name: '数量',
                splitLine: { lineStyle: { type: 'dashed', opacity: 0.55 } },
            },
            { type: 'value', name: '金额', splitLine: { show: false } },
        ],
        series,
    }
}

function getLeaderboardBarOption(
    lb: LeaderboardPack['leaderboards'][number],
    _pack: LeaderboardPack
) {
    const rows = [...lb.rows].sort((a, b) => Number(b.fee_amount) - Number(a.fee_amount)).slice(0, 15)
    return {
        color: ['#2563eb'],
        tooltip: {
            trigger: 'axis',
            axisPointer: { type: 'shadow' },
            valueFormatter: (v: number) => `${Number(v).toLocaleString('zh-CN')} 元`,
        },
        grid: { left: 8, right: 20, top: 12, bottom: 8, containLabel: true },
        xAxis: {
            type: 'value',
            splitLine: { lineStyle: { type: 'dashed', opacity: 0.6 } },
        },
        yAxis: {
            type: 'category',
            data: rows
                .map(r => (r.name.length > 8 ? `${r.name.slice(0, 8)}…` : r.name))
                .reverse(),
            axisLabel: { fontSize: 11 },
        },
        series: [
            {
                name: '诊金',
                type: 'bar',
                data: rows.map(r => Number(r.fee_amount ?? 0)).reverse(),
                barMaxWidth: 20,
                itemStyle: { borderRadius: [0, 6, 6, 0] },
            },
        ],
    }
}

function leaderboardPieHasData(lb: LeaderboardPack['leaderboards'][number]): boolean {
    return lb.rows.some(r => Number(r.fee_amount) > 0)
}

function getLeaderboardPieOption(
    lb: LeaderboardPack['leaderboards'][number],
    _pack: LeaderboardPack
) {
    const rows = [...lb.rows]
        .filter(r => Number(r.fee_amount) > 0)
        .sort((a, b) => Number(b.fee_amount) - Number(a.fee_amount))
        .slice(0, 10)
    const data = rows.map(r => ({
        name: r.name.length > 6 ? `${r.name.slice(0, 6)}…` : r.name,
        value: Number(r.fee_amount),
    }))
    return {
        color: [
            '#2563eb',
            '#0d9488',
            '#d97706',
            '#7c3aed',
            '#64748b',
            '#0891b2',
            '#65a30d',
            '#c026d3',
            '#ea580c',
            '#4f46e5',
        ],
        tooltip: { trigger: 'item', formatter: '{b}: {c} 元 ({d}%)' },
        legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 9 } },
        series: [
            {
                name: '诊金',
                type: 'pie',
                radius: ['38%', '64%'],
                center: ['50%', '42%'],
                data,
                itemStyle: { borderRadius: 5, borderColor: '#fff', borderWidth: 2 },
                label: { formatter: '{b}\n{d}%', fontSize: 10 },
            },
        ],
    }
}

function deptFunnelHasData(tb: YejiTable): boolean {
    return (
        Number(tb.total.lead_count) + Number(tb.total.consult_count) + Number(tb.total.deal_order_count) >
        0
    )
}

function getDeptFunnelOption(tb: YejiTable) {
    const lead = Number(tb.total.lead_count ?? 0)
    const consult = Number(tb.total.consult_count ?? 0)
    const deal = Number(tb.total.deal_order_count ?? 0)
    return {
        tooltip: { trigger: 'item', formatter: '{b}: {c}' },
        color: ['#2563eb', '#38bdf8', '#0d9488'],
        series: [
            {
                type: 'funnel',
                left: '8%',
                width: '84%',
                minSize: '12%',
                sort: 'none',
                gap: 6,
                label: {
                    show: true,
                    position: 'inside',
                    formatter: '{b}\n{c}',
                    fontSize: 11,
                },
                data: [
                    { value: lead, name: '进线' },
                    { value: consult, name: '接诊' },
                    { value: deal, name: '成交单' },
                ],
            },
        ],
    }
}

function deptPerformancePieHasData(tb: YejiTable): boolean {
    return tb.rows.some(r => r.dept_id > 0 && Number(r.performance_amount) > 0)
}

function getDeptPerformancePieOption(tb: YejiTable) {
    const rows = tb.rows
        .filter(r => r.dept_id > 0 && Number(r.performance_amount) > 0)
        .sort((a, b) => Number(b.performance_amount) - Number(a.performance_amount))
    if (!rows.length) {
        return { series: [{ type: 'pie', data: [] }] }
    }
    const top = rows.slice(0, 10)
    const pieRows: { name: string; value: number }[] = top.map(r => ({
        name: r.dept_name.length > 8 ? `${r.dept_name.slice(0, 8)}…` : r.dept_name,
        value: Number(r.performance_amount),
    }))
    if (rows.length > 10) {
        const rest = rows.slice(10).reduce((s, r) => s + Number(r.performance_amount), 0)
        if (rest > 0) pieRows.push({ name: '其它', value: rest })
    }
    return {
        color: [
            '#2563eb',
            '#0d9488',
            '#d97706',
            '#7c3aed',
            '#64748b',
            '#0891b2',
            '#65a30d',
            '#c026d3',
            '#ea580c',
            '#4f46e5',
            '#94a3b8',
        ],
        tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 元（{d}%）' },
        legend: { type: 'scroll', bottom: 0, textStyle: { fontSize: 10 } },
        series: [
            {
                type: 'pie',
                radius: ['36%', '62%'],
                center: ['50%', '44%'],
                data: pieRows,
                itemStyle: { borderRadius: 5, borderColor: '#fff', borderWidth: 2 },
                label: { formatter: '{b}\n{d}%', fontSize: 10 },
            },
        ],
    }
}

// 选渠道时表头缩略（卡片标题已含渠道名，避免「自媒体1成交单」过长挤压）
function yejiChannelMetricShort(kind: 'perf' | 'deal' | 'aov'): string {
    const m: Record<string, string> = { perf: '渠道业绩', deal: '渠道成交', aov: '渠道客单' }
    return m[kind] ?? kind
}

// ── 格式化 ───────────────────────────────
function formatInt(v: any) {
    if (v === null || v === undefined || v === '') return '0'
    return Number(v).toLocaleString('en-US')
}
function formatMoney(v: any) {
    if (v === null || v === undefined || v === '') return '0.0'
    return Number(v).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 })
}
/** 排行榜整数列（接诊单数等） */
function formatLeaderboardInt(v: any) {
    if (v === null || v === undefined || v === '') return '0'
    const n = Number(v)
    if (!Number.isFinite(n)) return '0'
    return Math.round(n).toLocaleString('zh-CN')
}
function formatNumber(v: any) {
    if (v === null || v === undefined || v === '') return '—'
    return Number(v).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 })
}
function formatRoi(v: any) {
    if (v === null || v === undefined || v === '') return '—'
    return Number(v).toFixed(1)
}
function roiClass(v: any) {
    if (v === null || v === undefined || v === '') return ''
    const n = Number(v)
    if (n >= 1) return 'roi-good'
    if (n >= 0.8) return 'roi-mid'
    return 'roi-bad'
}
function formatAchievement(v: number | null): string {
    if (v === null) return '—'
    return (v * 100).toFixed(2) + '%'
}
function achievementClass(v: number | null): string {
    if (v === null) return ''
    if (v >= 1) return 'tp-good'
    if (v >= 0.8) return 'tp-mid'
    return 'tp-bad'
}
function formatMoneyW(v: number): string {
    if (v >= 10000) return (v / 10000).toFixed(1) + '万'
    return String(v)
}

/** 本地日历日 yyyy-mm-dd（排行榜「今日」、日期快捷与日期控件一致） */
function formatLocalYmd(d: Date): string {
    const y = d.getFullYear()
    const m = String(d.getMonth() + 1).padStart(2, '0')
    const day = String(d.getDate()).padStart(2, '0')
    return `${y}-${m}-${day}`
}

onMounted(async () => {
    await Promise.all([loadDeptOptions(), loadChannelOptions()])
    loadData()
    loadTargetProgress()
})
</script>

<style lang="scss" scoped>
/* ═══════════════════════════════════════════════
   业绩看板 — Clean Modern Dashboard
   ═══════════════════════════════════════════════ */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');

.yeji-page {
    min-width: 0;
    --yj-brand: #6366f1;
    --yj-brand-soft: #eef2ff;
    --yj-teal: #0ea5e9;
    --yj-teal-soft: #f0f9ff;
    --yj-accent: #f43f5e;
    --yj-accent-soft: #fff1f2;
    --yj-ink: #1e293b;
    --yj-ink-secondary: #475569;
    --yj-muted: #64748b;
    --yj-line: #e2e8f0;
    --yj-line-strong: #cbd5e1;
    --yj-surface: #ffffff;
    --yj-surface-muted: #f8fafc;
    --yj-page: #f1f5f9;
    --yj-shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
    --yj-shadow-md: 0 4px 16px rgba(0,0,0,0.06);
    --yj-radius: 16px;
    --yj-radius-sm: 10px;
    --yj-font: 'Inter', var(--el-font-family), system-ui, sans-serif;
    /* 部门列竖条（表头略深、与指标列区分） */
    --yj-dept-col-bg: #eef2ff;
    --yj-dept-col-bg-hover: #e0e7ff;
    --yj-dept-col-head-start: #e0e7ff;
    --yj-dept-col-head-end: #e8ecff;
    --yj-dept-col-border: #c7d2fe;
    --yj-dept-col-head-ink: #3730a3;

    min-height: 100%;
    padding: 24px;
    font-family: var(--yj-font);
    color: var(--yj-ink);
    background: var(--yj-page);
}

.yeji-masthead {
    margin-bottom: 24px;

    &__inner {
        display: flex;
        align-items: center;
        gap: 16px;
        padding: 28px 32px;
        background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
        border-radius: var(--yj-radius);
        box-shadow: var(--yj-shadow-md);
    }

    &__title {
        margin: 0;
        font-size: 1.5rem;
        font-weight: 800;
        letter-spacing: -0.03em;
        color: #fff;
        line-height: 1.2;
    }

    &__desc {
        margin: 6px 0 0;
        font-size: 0.875rem;
        line-height: 1.5;
        color: #94a3b8;
    }
}

.yeji-panel {
    margin-bottom: 20px;
    border-radius: var(--yj-radius);
    border: 1px solid var(--yj-line);
    background: var(--yj-surface);
    box-shadow: var(--yj-shadow-sm);

    :deep(.el-card__body) {
        padding: 0;
    }
}

.filter-card {
    overflow: hidden;
    border-radius: var(--yj-radius);
}

.yeji-main-tabs-card {
    margin-bottom: 20px;
    box-shadow: var(--yj-shadow-md);

    :deep(.el-card__body) {
        padding: 0 0 18px;
    }
}

.yeji-charts-only-card {
    margin-bottom: 20px;
    box-shadow: var(--yj-shadow-md);

    :deep(.el-card__body) {
        padding: 0;
    }

    .yeji-charts-stack {
        padding: 16px 20px 4px;
    }
}

.yeji-charts-only-head {
    padding: 16px 20px 12px;
    font-size: 15px;
    font-weight: 700;
    color: var(--yj-ink);
    border-bottom: 1px solid var(--yj-line);
}

.yeji-table-tabs-head {
    padding: 14px 20px 10px;
    background: var(--yj-surface-muted);
    border-bottom: 1px solid var(--yj-line);

    &__title {
        display: block;
        font-size: 15px;
        font-weight: 700;
        color: var(--yj-ink);
        line-height: 1.3;
    }

    &__hint {
        display: block;
        margin-top: 6px;
        font-size: 12px;
        font-weight: 500;
        color: var(--yj-muted);
        line-height: 1.45;
    }
}

.yeji-tab-pane-stack {
    min-height: 100px;
}

.yeji-zyyt-intro {
    margin: 0 20px 14px;
    padding-top: 4px;
    font-size: 0.8125rem;
    line-height: 1.65;
    color: #64748b;
}

.yeji-view-tabs {
    :deep(.el-tabs__header) {
        margin: 0;
        padding: 0 20px;
        background: var(--yj-surface-muted);
        border-bottom: 1px solid var(--yj-line);
    }
    :deep(.el-tabs__nav-wrap::after) { display: none; }
    :deep(.el-tabs__nav) { border: none; }
    :deep(.el-tabs__item) {
        font-weight: 600;
        font-size: 13px;
        height: 44px;
        line-height: 44px;
        padding: 0 20px;
        color: var(--yj-muted);
        border-bottom: 2px solid transparent;
        transition: color 0.2s, border-color 0.2s;
        &:hover { color: var(--yj-ink); }
        &.is-active {
            color: var(--yj-brand);
            border-bottom-color: var(--yj-brand);
            font-weight: 700;
        }
    }
    :deep(.el-tabs__active-bar) { display: none; }
    :deep(.el-tabs__content) {
        padding: 20px;
        overflow: visible;
    }
    :deep(.el-tab-pane) { overflow: visible; }
}

.yeji-charts-intro {
    margin: 0 0 16px;
    padding: 12px 16px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--yj-muted);
    background: var(--yj-surface-muted);
    border-radius: var(--yj-radius-sm);
    border: 1px solid var(--yj-line);
}

.yeji-charts-stack {
    display: flex;
    flex-direction: column;
    gap: 20px;
    min-height: 120px;
}

.yeji-charts-empty { padding: 48px 16px; }

.yeji-chart-block {
    padding: 20px;
    background: var(--yj-surface);
    border-radius: var(--yj-radius-sm);
    border: 1px solid var(--yj-line);

    &__title {
        margin: 0 0 16px;
        font-size: 1rem;
        font-weight: 700;
        color: var(--yj-ink);
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: 8px 12px;
    }
    &__sub { font-size: 12px; font-weight: 600; color: var(--yj-teal); }
    &__sub--muted { font-size: 11px; font-weight: 500; color: var(--yj-muted); }
}

.yeji-chart-subrow { margin-top: 4px; }

.yeji-chart-panel {
    border: 1px solid var(--yj-line);
    border-radius: var(--yj-radius-sm);
    padding: 14px;
    background: var(--yj-surface);
    height: 100%;
    transition: box-shadow 0.2s;
    &:hover { box-shadow: var(--yj-shadow-sm); }
    &__label {
        font-size: 12px;
        font-weight: 700;
        letter-spacing: 0.03em;
        color: var(--yj-muted);
        margin-bottom: 10px;
    }
}

.yeji-echart {
    width: 100%;
    height: 320px;
    min-height: 240px;
}

.yeji-echart--short {
    height: 280px;
    min-height: 220px;
}

.yeji-echart--tall {
    height: 400px;
    min-height: 300px;
}

.yeji-echart--compact {
    height: 288px;
    min-height: 232px;
}

.doctor-daily-card--nested {
    margin-bottom: 20px;
}

.doctor-daily-card {
    box-shadow: var(--yj-shadow-sm);

    :deep(.el-card__header) {
        padding: 16px 22px;
        background: var(--yj-surface-muted);
        border-bottom: 1px solid var(--yj-line);
    }

    :deep(.el-card__body) {
        padding: 18px 22px 22px;
    }

    &__head {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-start;
        justify-content: space-between;
        gap: 12px;
    }

    &__title {
        font-weight: 700;
        font-size: 1.0625rem;
        color: var(--yj-ink);
    }

    &__title-block {
        display: flex;
        flex-direction: column;
        gap: 6px;
    }

    &__range {
        font-size: 12px;
        font-weight: 600;
        color: var(--yj-teal);
    }

    &__tools {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 10px;
    }

    &__lbl {
        font-size: 12px;
        color: var(--el-text-color-secondary);
    }

    &__picker {
        width: 150px;
    }

    &__hint {
        margin: 0 0 16px;
        padding: 14px 16px;
        font-size: 12px;
        line-height: 1.65;
        color: var(--yj-ink-secondary);
        background: var(--yj-brand-soft);
        border-radius: var(--yj-radius-sm);
        border: 1px solid rgba(37, 99, 235, 0.12);
    }
}

.doctor-daily-table {
    width: 100%;
    border-radius: var(--yj-radius-sm);
    overflow: hidden;

    :deep(.el-table__header th) {
        background: var(--yj-surface-muted) !important;
        color: var(--yj-ink-secondary) !important;
        font-weight: 600;
        font-size: 12px;
    }

    :deep(.el-table__footer-wrapper td) {
        font-weight: 700;
        background: var(--yj-accent-soft) !important;
        font-size: 13px;
    }
}

.filter-surface {
    padding: 20px;
    background: var(--yj-surface);
}

.filter-row {
    &--top {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-end;
        gap: 16px 20px;
    }
}

.filter-field {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    min-width: 0;

    .lbl {
        font-size: 12px;
        font-weight: 600;
        color: var(--yj-muted);
        letter-spacing: 0.02em;
    }

    &--wide {
        flex: 1 1 280px;
        min-width: 240px;
    }

    &--date-top {
        flex: 1 1 320px;
        min-width: 260px;
    }
}

.filter-select {
    width: 100%;

    &--channel {
        min-width: 220px;
        max-width: 320px;
    }

    &--dept {
        min-width: 280px;
    }
}

.filter-dept-tree-wrap {
    min-height: 32px;
}

.filter-dept-tree-select {
    width: 100%;
}

.filter-date-picker {
    width: 100%;
    max-width: 320px;
}

.filter-hint {
    display: none; /* 已在模板中移除，保留class以免报错 */
}

.filter-quick {
    margin-top: 16px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 14px;
}

.filter-quick__title {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--yj-muted);
}

.date-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.date-pill {
    appearance: none;
    margin: 0;
    border: 1px solid var(--yj-line);
    background: var(--yj-surface);
    color: var(--yj-ink-secondary);
    font-size: 12px;
    font-weight: 600;
    font-family: inherit;
    padding: 7px 14px;
    border-radius: 6px;
    cursor: pointer;
    line-height: 1.4;
    transition: all 0.15s ease;
    &:hover {
        border-color: var(--yj-brand);
        color: var(--yj-brand);
        background: var(--yj-brand-soft);
    }
    &:focus-visible {
        outline: 2px solid var(--yj-brand);
        outline-offset: 2px;
    }
}

.filter-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-left: auto;
    flex-shrink: 0;
}

.filter-btn {
    padding: 8px 20px;
    font-weight: 600;
    border-radius: var(--yj-radius-sm);
    &__icon { margin-right: 4px; font-size: 15px; vertical-align: -2px; }
    &--query {
        background: var(--yj-brand);
        color: #fff;
        border: none;
        &:hover { opacity: 0.9; color: #fff; }
    }
    &--reset {
        border: 1px solid var(--yj-line);
        background: var(--yj-surface);
        &:hover { border-color: var(--yj-brand); color: var(--yj-brand); }
        &:disabled { opacity: 0.5; }
    }
}

.channel-opt-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    width: 100%;
    padding-right: 4px;
}

.opt-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.opt-count {
    flex-shrink: 0;
    font-size: 12px;
    color: var(--yj-muted);
}

.filter-alert {
    margin-bottom: 12px;
}

.tables-wrap {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 20px;
}

.leaderboards-wrap {
    margin-top: 22px;
    min-height: 48px;
}

.leaderboard-hint {
    margin: 0 0 16px;
    padding: 14px 16px;
    font-size: 13px;
    color: var(--yj-ink-secondary);
    line-height: 1.6;
    background: var(--yj-surface);
    border: 1px solid var(--yj-line);
    border-radius: var(--yj-radius-sm);
    box-shadow: var(--yj-shadow-sm);

    .muted {
        display: block;
        margin-top: 8px;
        color: var(--yj-muted);
        font-size: 12px;
    }
}

/* 目标完成率色条：目标看板（卡片）Tab 复用 */
.tp-good {
    color: #15803d !important;
    font-weight: 700;
}
.tp-mid {
    color: #c2410c !important;
    font-weight: 700;
}
.tp-bad {
    color: #b91c1c !important;
    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 {
    font-size: 13px;
    font-weight: 500;
    opacity: 0.95;
}

.leaderboard-table {
    .th-sub {
        display: block;
        font-size: 11px;
        font-weight: 500;
        opacity: 0.85;
    }

    .col-rank {
        width: 52px;
        font-weight: 600;
    }

    .col-abbr {
        min-width: 56px;
    }

    .col-name {
        min-width: 72px;
        font-weight: 500;
    }

    .col-consult {
        width: 84px;
        text-align: right;
        font-variant-numeric: tabular-nums;
    }

    .col-deal {
        width: 84px;
        text-align: right;
        font-variant-numeric: tabular-nums;
    }

    .col-assign {
        width: 84px;
        text-align: right;
        font-variant-numeric: tabular-nums;
    }

    .col-appointment {
        width: 88px;
        text-align: right;
        font-variant-numeric: tabular-nums;
    }
}

.leaderboard-empty {
    grid-column: 1 / -1;
}

.yeji-table__foot--left {
    text-align: left;
}

.empty-tip {
    grid-column: 1 / -1;
    background: var(--yj-surface);
    border-radius: var(--yj-radius-sm);
    padding: 36px 16px;
    border: 1px dashed var(--yj-line-strong);
}

.yeji-th-label {
    display: inline-block;
    line-height: 1.25;
    vertical-align: middle;
    max-width: 6.5rem;
}

.yeji-table {
    background: var(--yj-surface);
    border-radius: var(--yj-radius);
    overflow: hidden;
    box-shadow: var(--yj-shadow-sm);
    border: 1px solid var(--yj-line);

    &__scroll {
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
        scrollbar-gutter: stable;
        outline: none;

        &::-webkit-scrollbar {
            height: 10px;
        }
        &::-webkit-scrollbar-thumb {
            background: color-mix(in srgb, var(--yj-muted) 50%, transparent);
            border-radius: 999px;
        }
        &::-webkit-scrollbar-track {
            background: var(--yj-surface-muted);
        }

        &:focus-visible {
            box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--yj-brand) 40%, transparent);
        }
    }

    &__header {
        background: var(--yj-surface-muted);
        color: var(--yj-ink);
        font-weight: 700;
        font-size: 14px;
        text-align: center;
        padding: 14px 16px;
        border-bottom: 1px solid var(--yj-line);
    }

    &__body {
        width: 100%;
        border-collapse: collapse;
        text-align: center;
        font-size: 13px;
        color: var(--yj-ink);

        thead th.col-dept,
        thead th.col-abbr {
            min-width: 88px;
            background: linear-gradient(
                180deg,
                var(--yj-dept-col-head-start) 0%,
                var(--yj-dept-col-head-end) 100%
            );
            color: var(--yj-dept-col-head-ink);
            font-weight: 700;
            border-right: 2px solid var(--yj-dept-col-border);
            box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
        }

        thead th.col-abbr {
            min-width: 56px;
        }

        tbody td.col-dept,
        tbody td.col-abbr {
            background: var(--yj-dept-col-bg);
            color: var(--yj-ink);
            font-weight: 600;
            border-right: 2px solid var(--yj-dept-col-border);
            text-align: center;
        }

        tbody tr.yeji-row--action:not(.yeji-row--idle):hover td.col-dept,
        tbody tr.yeji-row--action:not(.yeji-row--idle):hover td.col-abbr {
            background: var(--yj-dept-col-bg-hover);
        }

        .total-row td.col-dept {
            background: color-mix(in srgb, var(--yj-accent-soft) 55%, var(--yj-dept-col-bg) 45%);
            color: var(--yj-ink);
            font-weight: 700;
            border-right: 2px solid color-mix(in srgb, var(--yj-dept-col-border) 55%, var(--yj-line) 45%);
        }

        thead th {
            background: #f1f5f9;
            color: var(--yj-ink-secondary);
            font-weight: 600;
            padding: 10px 8px;
            border: 1px solid var(--yj-line);
            font-variant-numeric: tabular-nums;
            font-size: 12px;
            .col-info {
                margin-left: 4px;
                color: var(--yj-muted);
                cursor: help;
                vertical-align: -2px;
                font-size: 14px;
                transition: color 0.15s;
                &:hover { color: var(--yj-brand); }
            }
        }

        tbody td {
            padding: 10px 8px;
            border: 1px solid var(--yj-line);
            white-space: nowrap;
            font-variant-numeric: tabular-nums;
        }

        tbody tr.yeji-row--action {
            cursor: pointer;
            transition: background 0.18s ease;

            &:hover {
                background: var(--yj-brand-soft);
            }

            &.yeji-row--unassigned {
                cursor: pointer;
            }

            &.yeji-row--idle {
                cursor: default;

                &:hover {
                    background: transparent;
                }
            }
        }

        .col-completed {
            color: #0f766e;
            font-weight: 600;
            background: var(--yj-teal-soft);
        }

        .total-row {
            font-weight: 700;
            background: var(--yj-accent-soft);
            color: var(--yj-ink);

            td {
                border-color: rgba(217, 119, 6, 0.2);
            }
        }

        .roi-good {
            color: #15803d;
            font-weight: 600;
        }

        .roi-mid {
            color: #c2410c;
            font-weight: 600;
        }

        .roi-bad {
            color: #b91c1c;
            font-weight: 600;
        }

        &.yeji-table__body--data {
            min-width: max(100%, 1180px);
            width: max-content;

            thead th.col-dept,
            tbody td.col-dept {
                position: sticky;
                left: 0;
                z-index: 3;
            }
            thead th.col-dept {
                z-index: 6;
                text-align: left;
                padding-left: 12px;
                padding-right: 10px;
                box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.35);
            }
            tbody td.col-dept {
                box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.28);
                white-space: normal;
                max-width: 300px;
                line-height: 1.35;
                text-align: left;
                padding-left: 12px;
                padding-right: 10px;
                word-break: break-word;
            }
            .total-row td.col-dept {
                box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.28);
            }
            thead th:not(.col-dept) {
                min-width: 88px;
            }
            thead th.col-narrow-head {
                min-width: 76px;
                max-width: 112px;
                padding: 8px 6px;
                vertical-align: middle;
                line-height: 1.2;
            }
        }

        &.yeji-table__body--leaderboard {
            min-width: max(100%, 780px);
            width: max-content;

            thead th.col-rank,
            tbody td.col-rank {
                position: sticky;
                left: 0;
                z-index: 3;
            }
            thead th.col-rank {
                z-index: 6;
                background: linear-gradient(
                    180deg,
                    var(--yj-dept-col-head-start) 0%,
                    var(--yj-dept-col-head-end) 100%
                ) !important;
                box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.3);
            }
            tbody td.col-rank {
                background: var(--yj-surface);
                box-shadow: 8px 0 20px -12px rgba(15, 23, 42, 0.22);
            }
            tbody tr.yeji-row--action:hover td.col-rank {
                background: var(--yj-brand-soft);
            }
        }
    }

    &__foot {
        padding: 11px 14px;
        font-size: 12px;
        color: var(--yj-muted);
        background: var(--yj-surface-muted);
        border-top: 1px solid var(--yj-line);
        text-align: right;
        line-height: 1.5;
    }
}

.yeji-order-drawer__hint {
    margin: 0 0 12px;
    font-size: 13px;
    color: var(--yj-muted);
    line-height: 1.5;
}

.yeji-order-drawer__header {
    padding-right: 8px;
}

.yeji-order-drawer__heading {
    margin: 0;
    font-size: 18px;
    font-weight: 700;
    color: var(--yj-ink);
    line-height: 1.35;
    letter-spacing: -0.01em;
}

.yeji-order-drawer__sub {
    margin: 6px 0 0;
    font-size: 13px;
    color: var(--yj-muted);
    line-height: 1.55;
}

.yeji-order-drawer__shell {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-height: 200px;
}

.yeji-order-drawer__stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 12px;
}

.yeji-order-drawer__stat-card {
    border: 1px solid var(--yj-line);
    border-radius: var(--yj-radius-sm);
    padding: 14px 16px;
    background: var(--yj-surface);
    display: flex;
    flex-direction: column;
    gap: 8px;
    transition:
        border-color 0.2s ease,
        box-shadow 0.2s ease;

    &:hover {
        border-color: rgba(37, 99, 235, 0.22);
        box-shadow: var(--yj-shadow-sm);
    }

    &--accent {
        border-color: rgba(220, 38, 38, 0.18);
        background: linear-gradient(180deg, #fef2f2 0%, var(--el-fill-color-blank) 100%);
    }

    &--muted {
        background: #f8fafc;
    }

    &--total {
        border-color: rgba(217, 119, 6, 0.22);
        background: linear-gradient(180deg, #fffbeb 0%, var(--el-fill-color-blank) 100%);
    }
}

.yeji-order-drawer__list-tabs {
    :deep(.el-tabs__header) {
        margin: 0;
        padding: 0;
        border-bottom: 1px solid var(--yj-line);
    }

    :deep(.el-tabs__content) {
        display: none;
    }

    :deep(.el-tabs__nav-wrap::after) {
        display: none;
    }
}

.yeji-order-drawer__stat-label {
    font-size: 12px;
    color: var(--yj-muted);
    line-height: 1.45;
}

.yeji-order-drawer__stat-note {
    display: block;
    margin-top: 2px;
    font-size: 11px;
    font-weight: 400;
    color: var(--el-text-color-placeholder);
}

.yeji-order-drawer__stat-value {
    font-size: 18px;
    font-weight: 700;
    color: var(--yj-ink);
    font-variant-numeric: tabular-nums;

    &--money {
        font-size: 21px;
        color: #b91c1c;
    }
}

.yeji-order-drawer__body {
    flex: 1;
    min-height: 120px;
}

.yeji-order-drawer__pager {
    margin-top: 4px;
    padding-top: 10px;
    border-top: 1px solid var(--yj-line);
    display: flex;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 8px;
}

.yeji-order-appt-lines {
    line-height: 1.45;
    font-size: 12px;
    color: var(--yj-ink, #0f172a);
}

.yeji-order-appt-line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 6px;
    margin-bottom: 2px;

    &:last-child {
        margin-bottom: 0;
    }

    &.yeji-order-appt-line--channel {
        align-items: flex-start;

        .yeji-order-appt-channel-text {
            flex: 1;
            min-width: 0;
        }
    }
}

.yeji-order-appt-info {
    font-size: 14px;
    color: var(--yj-muted, #94a3b8);
    cursor: help;
    flex-shrink: 0;
    margin-top: 1px;
}

.yeji-order-appt-k {
    flex: 0 0 auto;
    font-size: 11px;
    font-weight: 600;
    color: var(--yj-muted, #64748b);
}

.yeji-order-appt-sub {
    font-size: 11px;
    color: var(--yj-muted, #64748b);
}

.yeji-order-appt-empty {
    font-size: 12px;
    color: var(--yj-muted, #94a3b8);
}

.yeji-order-amt {
    color: #b91c1c;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

:deep(.yeji-order-drawer.el-drawer) {
    --el-drawer-padding-primary: 18px;
}

:deep(.yeji-order-drawer .el-drawer__header) {
    margin-bottom: 10px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--yj-line, #e2e8f0);
}

:deep(.yeji-order-drawer .el-pagination) {
    flex-wrap: wrap;
    justify-content: flex-end;
}

.yeji-unassigned-dialog__head {
    line-height: 1.35;
}

.yeji-unassigned-dialog__title {
    display: block;
    font-size: 16px;
    font-weight: 700;
    color: var(--yj-ink, #0f172a);
}

.yeji-unassigned-dialog__sub {
    margin: 6px 0 0;
    font-size: 12px;
    color: var(--yj-ink-secondary, #64748b);
    font-weight: 400;
}

.yeji-unassigned-dialog__note {
    margin: 0 0 12px;
    padding: 10px 12px;
    font-size: 12px;
    line-height: 1.65;
    color: var(--yj-ink-secondary, #64748b);
    background: var(--yj-accent-soft, #fff7ed);
    border-radius: 8px;
    border: 1px solid color-mix(in srgb, var(--yj-line, #e2e8f0) 80%, transparent);
}

.yeji-unassigned-dialog__body {
    min-height: 120px;
}

.yeji-lead-cell--link {
    color: var(--yj-brand, #2563eb);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

tbody tr.yeji-row--action:hover .yeji-lead-cell--link {
    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 {
    margin-top: 14px;
    display: flex;
    justify-content: flex-end;
}

@media (prefers-reduced-motion: reduce) {
    .date-pill,
    .filter-btn,
    .yeji-table__body tbody tr.yeji-row--action,
    .yeji-order-drawer__stat-card {
        transition: none;
    }
}
</style>
