更新
This commit is contained in:
@@ -25,7 +25,7 @@ export function deptDetail(params: any) {
|
|||||||
return request.get({ url: '/dept.dept/detail', params })
|
return request.get({ url: '/dept.dept/detail', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 部门列表全部
|
// 部门列表全部;apply_data_scope=1 时按当前账号角色数据权限收窄树(与 DataScopeService 一致)
|
||||||
export function deptAll() {
|
export function deptAll(params?: { apply_data_scope?: number }) {
|
||||||
return request.get({ url: '/dept.dept/all' })
|
return request.get({ url: '/dept.dept/all', params })
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -43,7 +43,17 @@ export function yejiStatsLeaderboard(params: {
|
|||||||
return request.get({ url: '/stats.yejiStats/leaderboard', params })
|
return request.get({ url: '/stats.yejiStats/leaderboard', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 医生统计(start/end、channel_code;含数据范围;不按展示部门 dept_ids 筛选) */
|
/** 「未归属中心」行:按订单创建人拆解合计业绩(与看板「合计业绩」未归属部分同口径) */
|
||||||
|
export function yejiStatsUnassignedBreakdown(params: {
|
||||||
|
start_date: string
|
||||||
|
end_date: string
|
||||||
|
dept_ids?: string
|
||||||
|
channel_code?: string
|
||||||
|
tag_id?: string
|
||||||
|
}) {
|
||||||
|
return request.get({ url: '/stats.yejiStats/unassignedBreakdown', params })
|
||||||
|
}
|
||||||
|
|
||||||
export function doctorDailyStatsOverview(params: {
|
export function doctorDailyStatsOverview(params: {
|
||||||
start_date?: string
|
start_date?: string
|
||||||
end_date?: string
|
end_date?: string
|
||||||
|
|||||||
@@ -60,25 +60,26 @@
|
|||||||
|
|
||||||
<el-divider content-position="left">组织信息</el-divider>
|
<el-divider content-position="left">组织信息</el-divider>
|
||||||
|
|
||||||
<!-- 归属部门 -->
|
<!-- 归属部门:数据权限内仅末级可选,父级仅用于展开 -->
|
||||||
<el-form-item label="归属部门" prop="dept_id">
|
<el-form-item label="归属部门" prop="dept_id">
|
||||||
<el-tree-select
|
<div class="w-full">
|
||||||
class="flex-1"
|
<el-tree-select
|
||||||
v-model="formData.dept_id"
|
class="flex-1 w-full"
|
||||||
:data="optionsData.dept"
|
v-model="formData.dept_id"
|
||||||
clearable
|
:data="optionsData.dept"
|
||||||
multiple
|
clearable
|
||||||
node-key="id"
|
multiple
|
||||||
:props="{
|
node-key="id"
|
||||||
label: 'name',
|
:props="{
|
||||||
disabled(data: any) {
|
label: 'name',
|
||||||
return data.status !== 1
|
disabled: deptTreeNodeDisabled
|
||||||
}
|
}"
|
||||||
}"
|
check-strictly
|
||||||
check-strictly
|
:default-expand-all="true"
|
||||||
:default-expand-all="true"
|
placeholder="请选择末级归属部门"
|
||||||
placeholder="请选择归属部门"
|
/>
|
||||||
/>
|
<div class="form-tips">仅可选末级部门(含下级的父部门不可选),与数据权限范围一致。</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 岗位 -->
|
<!-- 岗位 -->
|
||||||
@@ -215,6 +216,13 @@ import { getDictData } from '@/api/app'
|
|||||||
import { useDictOptions } from '@/hooks/useDictOptions'
|
import { useDictOptions } from '@/hooks/useDictOptions'
|
||||||
import feedback from '@/utils/feedback'
|
import feedback from '@/utils/feedback'
|
||||||
|
|
||||||
|
/** 归属部门:禁止选父级,仅末级可选(status=1 且无子节点) */
|
||||||
|
function deptTreeNodeDisabled(data: { status?: number; children?: unknown[] }) {
|
||||||
|
if (data.status !== 1) return true
|
||||||
|
const ch = data.children
|
||||||
|
return Array.isArray(ch) && ch.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['success', 'close'])
|
const emit = defineEmits(['success', 'close'])
|
||||||
const formRef = shallowRef<FormInstance>()
|
const formRef = shallowRef<FormInstance>()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@@ -264,7 +272,8 @@ const { optionsData } = useDictOptions<{
|
|||||||
api: jobsAll
|
api: jobsAll
|
||||||
},
|
},
|
||||||
dept: {
|
dept: {
|
||||||
api: deptAll
|
api: deptAll,
|
||||||
|
params: { apply_data_scope: 1 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -54,25 +54,26 @@
|
|||||||
|
|
||||||
<el-divider content-position="left">组织信息</el-divider>
|
<el-divider content-position="left">组织信息</el-divider>
|
||||||
|
|
||||||
<!-- 归属部门 -->
|
<!-- 归属部门:数据权限内仅末级可选,父级仅用于展开 -->
|
||||||
<el-form-item label="归属部门" prop="dept_id">
|
<el-form-item label="归属部门" prop="dept_id">
|
||||||
<el-tree-select
|
<div class="w-full">
|
||||||
class="flex-1"
|
<el-tree-select
|
||||||
v-model="formData.dept_id"
|
class="flex-1 w-full"
|
||||||
:data="optionsData.dept"
|
v-model="formData.dept_id"
|
||||||
clearable
|
:data="optionsData.dept"
|
||||||
multiple
|
clearable
|
||||||
node-key="id"
|
multiple
|
||||||
:props="{
|
node-key="id"
|
||||||
label: 'name',
|
:props="{
|
||||||
disabled(data: any) {
|
label: 'name',
|
||||||
return data.status !== 1
|
disabled: deptTreeNodeDisabled
|
||||||
}
|
}"
|
||||||
}"
|
check-strictly
|
||||||
check-strictly
|
:default-expand-all="true"
|
||||||
:default-expand-all="true"
|
placeholder="请选择末级归属部门"
|
||||||
placeholder="请选择归属部门"
|
/>
|
||||||
/>
|
<div class="form-tips">仅可选末级部门(含下级的父部门不可选),与数据权限范围一致。</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 岗位 -->
|
<!-- 岗位 -->
|
||||||
@@ -254,6 +255,13 @@ import { getDictData } from '@/api/app'
|
|||||||
import { useDictOptions } from '@/hooks/useDictOptions'
|
import { useDictOptions } from '@/hooks/useDictOptions'
|
||||||
import feedback from '@/utils/feedback'
|
import feedback from '@/utils/feedback'
|
||||||
|
|
||||||
|
/** 归属部门:禁止选父级,仅末级可选(status=1 且无子节点) */
|
||||||
|
function deptTreeNodeDisabled(data: { status?: number; children?: unknown[] }) {
|
||||||
|
if (data.status !== 1) return true
|
||||||
|
const ch = data.children
|
||||||
|
return Array.isArray(ch) && ch.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['success', 'close'])
|
const emit = defineEmits(['success', 'close'])
|
||||||
const formRef = shallowRef<FormInstance>()
|
const formRef = shallowRef<FormInstance>()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@@ -307,7 +315,8 @@ const { optionsData } = useDictOptions<{
|
|||||||
api: jobsAll
|
api: jobsAll
|
||||||
},
|
},
|
||||||
dept: {
|
dept: {
|
||||||
api: deptAll
|
api: deptAll,
|
||||||
|
params: { apply_data_scope: 1 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -99,11 +99,12 @@ import EditPopup from './edit.vue'
|
|||||||
|
|
||||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||||
|
|
||||||
// 表单数据 - 固定role_id=1查询医生
|
// 表单数据 - 固定 role_id=1 查询医生;数据范围与医助列表一致,仅看自己权限内成员
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
account: '',
|
account: '',
|
||||||
name: '',
|
name: '',
|
||||||
role_id: 1 // 固定为医生角色
|
role_id: 1,
|
||||||
|
apply_data_scope: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const showEdit = ref(false)
|
const showEdit = ref(false)
|
||||||
|
|||||||
+197
-12
@@ -594,9 +594,12 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="r in tb.rows"
|
v-for="r in tb.rows"
|
||||||
:key="r.dept_id"
|
:key="`${idx}-${r.dept_id}-${r.dept_name}`"
|
||||||
class="yeji-row--action"
|
class="yeji-row--action"
|
||||||
:class="{ 'yeji-row--idle': r.dept_id <= 0 }"
|
:class="{
|
||||||
|
'yeji-row--unassigned': isUnassignedYejiRow(r),
|
||||||
|
'yeji-row--idle': r.dept_id <= 0 && !isUnassignedYejiRow(r),
|
||||||
|
}"
|
||||||
@click="onYejiDeptRowClick(tb, r)"
|
@click="onYejiDeptRowClick(tb, r)"
|
||||||
>
|
>
|
||||||
<td class="col-dept">{{ r.dept_name }}</td>
|
<td class="col-dept">{{ r.dept_name }}</td>
|
||||||
@@ -811,6 +814,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -820,12 +867,13 @@ import { Search, RefreshRight, InfoFilled } from '@element-plus/icons-vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import vCharts from 'vue-echarts'
|
import vCharts from 'vue-echarts'
|
||||||
import {
|
import {
|
||||||
|
doctorDailyStatsOverview,
|
||||||
yejiStatsChannelOptions,
|
yejiStatsChannelOptions,
|
||||||
yejiStatsDeptOptions,
|
yejiStatsDeptOptions,
|
||||||
yejiStatsLeaderboard,
|
yejiStatsLeaderboard,
|
||||||
yejiStatsMulti,
|
yejiStatsMulti,
|
||||||
yejiStatsOverview,
|
yejiStatsOverview,
|
||||||
doctorDailyStatsOverview,
|
yejiStatsUnassignedBreakdown,
|
||||||
} from '@/api/stats'
|
} from '@/api/stats'
|
||||||
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
|
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
|
||||||
import { prescriptionOrderLists } from '@/api/tcm'
|
import { prescriptionOrderLists } from '@/api/tcm'
|
||||||
@@ -907,6 +955,13 @@ interface ChannelGroup {
|
|||||||
channels: ChannelOption[]
|
channels: ChannelOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface YejiUnassignedBreakdownRow {
|
||||||
|
admin_id: number
|
||||||
|
name: string
|
||||||
|
order_count: number
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
interface YejiRow {
|
interface YejiRow {
|
||||||
dept_id: number
|
dept_id: number
|
||||||
dept_name: string
|
dept_name: string
|
||||||
@@ -984,6 +1039,12 @@ const tables = ref<YejiTable[]>([])
|
|||||||
const leaderboardBlock = ref<LeaderboardPack | null>(null)
|
const leaderboardBlock = ref<LeaderboardPack | null>(null)
|
||||||
const leaderboardsLoading = ref(false)
|
const leaderboardsLoading = ref(false)
|
||||||
|
|
||||||
|
const unassignedDialogVisible = ref(false)
|
||||||
|
const unassignedDialogLoading = ref(false)
|
||||||
|
const unassignedDialogSubtitle = ref('')
|
||||||
|
const unassignedDialogNote = ref('')
|
||||||
|
const unassignedDialogRows = ref<YejiUnassignedBreakdownRow[]>([])
|
||||||
|
|
||||||
/** 图表 / 数据表切换 */
|
/** 图表 / 数据表切换 */
|
||||||
const yejiDisplayTab = ref<'charts' | 'table'>('table')
|
const yejiDisplayTab = ref<'charts' | 'table'>('table')
|
||||||
|
|
||||||
@@ -1934,10 +1995,58 @@ function onOrderDrawerPageSizeChange(size: number) {
|
|||||||
void fetchOrderDrawerPage()
|
void fetchOrderDrawerPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUnassignedYejiRow(row: YejiRow): boolean {
|
||||||
|
return row.dept_id <= 0 && row.dept_name === '未归属中心'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 onYejiDeptRowClick(tb: YejiTable, row: YejiRow) {
|
function onYejiDeptRowClick(tb: YejiTable, row: YejiRow) {
|
||||||
|
if (isUnassignedYejiRow(row)) {
|
||||||
|
void openUnassignedBreakdown(tb)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (row.dept_id <= 0) return
|
if (row.dept_id <= 0) return
|
||||||
if (deptHasChildren(row.dept_id)) {
|
if (deptHasChildren(row.dept_id)) {
|
||||||
selectedDeptIds.value = [row.dept_id]
|
selectedDeptIds.value = [row.dept_id]
|
||||||
@@ -2312,6 +2421,13 @@ onMounted(async () => {
|
|||||||
--yj-radius: 16px;
|
--yj-radius: 16px;
|
||||||
--yj-radius-sm: 10px;
|
--yj-radius-sm: 10px;
|
||||||
--yj-font: 'Inter', var(--el-font-family), system-ui, sans-serif;
|
--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%;
|
min-height: 100%;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
@@ -2911,7 +3027,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col-abbr {
|
.col-abbr {
|
||||||
width: 48px;
|
min-width: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-name {
|
.col-name {
|
||||||
@@ -2972,6 +3088,45 @@ onMounted(async () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--yj-ink);
|
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 {
|
thead th {
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: var(--yj-ink-secondary);
|
color: var(--yj-ink-secondary);
|
||||||
@@ -2998,13 +3153,6 @@ onMounted(async () => {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-dept {
|
|
||||||
width: 80px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: var(--yj-surface-muted);
|
|
||||||
color: var(--yj-ink-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr.yeji-row--action {
|
tbody tr.yeji-row--action {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.18s ease;
|
transition: background 0.18s ease;
|
||||||
@@ -3013,6 +3161,10 @@ onMounted(async () => {
|
|||||||
background: var(--yj-brand-soft);
|
background: var(--yj-brand-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.yeji-row--unassigned {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&.yeji-row--idle {
|
&.yeji-row--idle {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
@@ -3194,6 +3346,39 @@ onMounted(async () => {
|
|||||||
justify-content: flex-end;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.date-pill,
|
.date-pill,
|
||||||
.filter-btn,
|
.filter-btn,
|
||||||
|
|||||||
@@ -126,7 +126,11 @@ class DeptController extends BaseAdminController
|
|||||||
*/
|
*/
|
||||||
public function all()
|
public function all()
|
||||||
{
|
{
|
||||||
$result = DeptLogic::getAllData();
|
$apply = (int) ($this->request->get('apply_data_scope', 0));
|
||||||
|
$result = $apply === 1
|
||||||
|
? DeptLogic::getAllDataScoped($this->adminId, $this->adminInfo)
|
||||||
|
: DeptLogic::getAllData();
|
||||||
|
|
||||||
return $this->data($result);
|
return $this->data($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use app\adminapi\logic\stats\YejiStatsLogic;
|
|||||||
* - GET stats.yeji-stats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
|
* - GET stats.yeji-stats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
|
||||||
* - GET stats.yeji-stats/dept-options 部门下拉
|
* - GET stats.yeji-stats/dept-options 部门下拉
|
||||||
* - GET stats.yeji-stats/leaderboard 医助排行榜(按展示部门分表)
|
* - GET stats.yeji-stats/leaderboard 医助排行榜(按展示部门分表)
|
||||||
|
* - GET stats.yeji-stats/unassignedBreakdown 「未归属中心」合计业绩按订单创建人拆解
|
||||||
*/
|
*/
|
||||||
class YejiStatsController extends BaseAdminController
|
class YejiStatsController extends BaseAdminController
|
||||||
{
|
{
|
||||||
@@ -102,4 +103,13 @@ class YejiStatsController extends BaseAdminController
|
|||||||
|
|
||||||
return $this->data(YejiStatsLogic::assistantLeaderboards($params, $this->adminId, $this->adminInfo));
|
return $this->data(YejiStatsLogic::assistantLeaderboards($params, $this->adminId, $this->adminInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 「未归属中心」行:按订单创建人拆解业绩(与「合计业绩」未归属部分同口径) */
|
||||||
|
public function unassignedBreakdown()
|
||||||
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
$params = $this->request->get();
|
||||||
|
|
||||||
|
return $this->data(YejiStatsLogic::unassignedCenterBreakdown($params, $this->adminId, $this->adminInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use app\common\logic\BaseLogic;
|
|||||||
use app\common\model\auth\Admin;
|
use app\common\model\auth\Admin;
|
||||||
use app\common\model\auth\AdminDept;
|
use app\common\model\auth\AdminDept;
|
||||||
use app\common\model\dept\Dept;
|
use app\common\model\dept\Dept;
|
||||||
|
use app\common\service\DataScope\DataScopeService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,6 +285,60 @@ class DeptLogic extends BaseLogic
|
|||||||
return self::getTree($data, $pid);
|
return self::getTree($data, $pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门树(与 getAllData 同结构),按当前管理员角色数据权限收窄可选节点;
|
||||||
|
* 保留必选祖先节点以便树形展示(与业绩看板 deptOptions 的 allowed 集合一致)。
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $adminInfo BaseAdminController::$adminInfo 形态(须含 root、role_id 等)
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function getAllDataScoped(int $adminId, array $adminInfo): array
|
||||||
|
{
|
||||||
|
$full = self::getAllData();
|
||||||
|
if ($full === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if ($adminId <= 0 || !DataScopeService::isEnabled()) {
|
||||||
|
return $full;
|
||||||
|
}
|
||||||
|
$allowed = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo);
|
||||||
|
if ($allowed === null) {
|
||||||
|
return $full;
|
||||||
|
}
|
||||||
|
if ($allowed === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::filterDeptTreeByAllowedIds($full, $allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $nodes
|
||||||
|
* @param array<int, true> $allowedIdMap
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private static function filterDeptTreeByAllowedIds(array $nodes, array $allowedIdMap): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$id = (int) ($node['id'] ?? 0);
|
||||||
|
$rawChildren = $node['children'] ?? [];
|
||||||
|
$children = \is_array($rawChildren) && $rawChildren !== []
|
||||||
|
? self::filterDeptTreeByAllowedIds($rawChildren, $allowedIdMap)
|
||||||
|
: [];
|
||||||
|
$inAllowed = isset($allowedIdMap[$id]);
|
||||||
|
if ($inAllowed || $children !== []) {
|
||||||
|
$row = $node;
|
||||||
|
$row['children'] = $children;
|
||||||
|
$out[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指定部门及其全部下级部门 id(含自身)。筛选时选父级可匹配子级下成员(admin_dept.dept_id)。
|
* 指定部门及其全部下级部门 id(含自身)。筛选时选父级可匹配子级下成员(admin_dept.dept_id)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2499,6 +2499,158 @@ class YejiStatsLogic
|
|||||||
return self::$tagScopeCache[$tagFilterId] = [array_keys($diagIds), $assistantIds];
|
return self::$tagScopeCache[$tagFilterId] = [array_keys($diagIds), $assistantIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「未归属中心」合计业绩按订单创建人拆解(与 aggregatePerformance /「合计业绩」列归属一致:仅 null,null 标签口径,不按渠道收窄)。
|
||||||
|
*
|
||||||
|
* @return array{
|
||||||
|
* start_date: string,
|
||||||
|
* end_date: string,
|
||||||
|
* channel_code: string,
|
||||||
|
* rows: list<array{admin_id:int, name:string, order_count:int, amount:float}>,
|
||||||
|
* note: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function unassignedCenterBreakdown(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||||
|
{
|
||||||
|
$c = self::resolveYejiContext($params);
|
||||||
|
if ($viewerAdminId > 0) {
|
||||||
|
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
|
||||||
|
}
|
||||||
|
$startTs = (int) $c['startTs'];
|
||||||
|
$endTs = (int) $c['endTs'];
|
||||||
|
$adminToPrimary = $c['adminToPrimary'];
|
||||||
|
$visibleCreatorIds = null;
|
||||||
|
if (!empty($c['dataScopeRestricted'])) {
|
||||||
|
$visibleCreatorIds = $c['dataScopeVisibleAdminIds'] ?? [];
|
||||||
|
}
|
||||||
|
$rows = self::aggregateUnassignedByCreator(
|
||||||
|
$startTs,
|
||||||
|
$endTs,
|
||||||
|
$adminToPrimary,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$visibleCreatorIds
|
||||||
|
);
|
||||||
|
$ids = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if ($row['admin_id'] > 0) {
|
||||||
|
$ids[] = $row['admin_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$names = self::batchAdminNames(array_values(array_unique($ids)));
|
||||||
|
foreach ($rows as &$row) {
|
||||||
|
$aid = (int) $row['admin_id'];
|
||||||
|
if ($aid <= 0) {
|
||||||
|
$row['name'] = '(无创建人/0)';
|
||||||
|
} else {
|
||||||
|
$row['name'] = (string) ($names[$aid] ?? ('#' . $aid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($row);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'start_date' => (string) $c['startDate'],
|
||||||
|
'end_date' => (string) $c['endDate'],
|
||||||
|
'channel_code' => (string) $c['channelCode'],
|
||||||
|
'rows' => $rows,
|
||||||
|
'note' => '与看板「合计业绩」列口径一致:按订单创建时间、剔除履约已取消(4);创建人无法映射到任一展示「中心」部门的业绩计入未归属。账号数据权限下仅列出可见创建人相关订单。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int[]|null $visibleCreatorIds 数据权限:仅统计这些创建人(null=不额外限制);须含 creator_id=0 时用 OR 条件已并入查询
|
||||||
|
*
|
||||||
|
* @return list<array{admin_id:int, name:string, order_count:int, amount:float}>
|
||||||
|
*/
|
||||||
|
private static function aggregateUnassignedByCreator(
|
||||||
|
int $startTs,
|
||||||
|
int $endTs,
|
||||||
|
array $adminToPrimary,
|
||||||
|
?array $tagDiagIds,
|
||||||
|
?array $tagAssistantIds,
|
||||||
|
?array $visibleCreatorIds
|
||||||
|
): array {
|
||||||
|
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$query = Db::name('tcm_prescription_order')
|
||||||
|
->alias('o')
|
||||||
|
->whereNull('o.delete_time')
|
||||||
|
->where('o.create_time', 'between', [$startTs, $endTs]);
|
||||||
|
self::applyPrescriptionOrderNotCancelledForPerformance($query);
|
||||||
|
if ($tagDiagIds !== null) {
|
||||||
|
$query->whereIn('o.diagnosis_id', $tagDiagIds);
|
||||||
|
}
|
||||||
|
if ($tagAssistantIds !== null) {
|
||||||
|
$query->whereIn('o.creator_id', array_keys($tagAssistantIds));
|
||||||
|
}
|
||||||
|
if ($visibleCreatorIds !== null) {
|
||||||
|
$visibleCreatorIds = array_values(array_unique(array_filter(array_map('intval', $visibleCreatorIds), static fn (int $v): bool => $v > 0)));
|
||||||
|
if ($visibleCreatorIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$ph = implode(',', array_fill(0, count($visibleCreatorIds), '?'));
|
||||||
|
$query->whereRaw("(o.creator_id = 0 OR o.creator_id IN ({$ph}))", $visibleCreatorIds);
|
||||||
|
}
|
||||||
|
$query->field(['o.creator_id', 'COUNT(*) AS order_count', 'SUM(o.amount) AS amount'])
|
||||||
|
->group('o.creator_id');
|
||||||
|
$dbRows = $query->select()->toArray();
|
||||||
|
if ($dbRows === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($dbRows as $r) {
|
||||||
|
$cid = (int) ($r['creator_id'] ?? 0);
|
||||||
|
$primary = (int) ($adminToPrimary[$cid] ?? 0);
|
||||||
|
if ($primary > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = [
|
||||||
|
'admin_id' => $cid,
|
||||||
|
'name' => '',
|
||||||
|
'order_count' => (int) ($r['order_count'] ?? 0),
|
||||||
|
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
usort($out, static function (array $a, array $b): int {
|
||||||
|
$c = $b['amount'] <=> $a['amount'];
|
||||||
|
if ($c !== 0) {
|
||||||
|
return $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $b['order_count'] <=> $a['order_count'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int[] $ids
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private static function batchAdminNames(array $ids): array
|
||||||
|
{
|
||||||
|
if ($ids === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$rows = Db::name('admin')
|
||||||
|
->whereIn('id', $ids)
|
||||||
|
->whereNull('delete_time')
|
||||||
|
->field(['id', 'name'])
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$map[(int) $r['id']] = (string) ($r['name'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表名 + 数据库前缀(用于 whereRaw 内嵌的 SQL,框架不会替换 raw 表名)
|
* 表名 + 数据库前缀(用于 whereRaw 内嵌的 SQL,框架不会替换 raw 表名)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user