629 lines
26 KiB
PHP
629 lines
26 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\logic\firstvisit;
|
|
|
|
use app\adminapi\logic\dept\DeptLogic;
|
|
use app\adminapi\logic\stats\YejiStatsLogic;
|
|
use app\common\model\auth\AdminDept;
|
|
use app\common\service\DataScope\DataScopeService;
|
|
use think\facade\Db;
|
|
|
|
/**
|
|
* 一诊「挂号统计」。
|
|
*
|
|
* 统计口径:
|
|
* - 挂号:doctor_appointment.appointment_date,状态 1/3/4,排除已取消 2;
|
|
* 归属优先挂号医助 assistant_id,再回退诊单医助 assistant_id。
|
|
* - 诊单:tcm_prescription_order.create_time,归属订单 creator_id,排除履约 4/9/10。
|
|
* - 所有部门和员工筛选都只能收窄 DataScope,不允许 HTTP 参数扩大当前账号范围。
|
|
*/
|
|
class FirstVisitRegistrationStatsLogic
|
|
{
|
|
private const ASSISTANT_ROLE_ID = 2;
|
|
|
|
/** @return array<string,mixed> */
|
|
public static function overview(array $params, int $adminId, array $adminInfo): array
|
|
{
|
|
$range = self::resolveRange((string) ($params['time_type'] ?? 'today'));
|
|
$baseVisibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
|
$allowedDeptSet = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo);
|
|
$scopeValue = DataScopeService::getEffectiveScope($adminInfo);
|
|
$selectedDeptId = max(0, (int) ($params['dept_id'] ?? 0));
|
|
$selectedAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
|
|
|
|
[$selectedDeptIds, $deptSelectionValid] = self::resolveSelectedDeptIds(
|
|
$selectedDeptId,
|
|
$allowedDeptSet
|
|
);
|
|
$assistants = $deptSelectionValid
|
|
? self::assistantOptions($baseVisibleIds, $selectedDeptIds, $selectedDeptId)
|
|
: [];
|
|
$assistantIds = self::normalizeIds(array_column($assistants, 'id'));
|
|
if ($selectedAssistantId > 0) {
|
|
$assistantIds = in_array($selectedAssistantId, $assistantIds, true)
|
|
? [$selectedAssistantId]
|
|
: [];
|
|
}
|
|
|
|
$departmentTree = DeptLogic::getAllDataScoped($adminId, $adminInfo);
|
|
$departmentIndex = [];
|
|
self::flattenDepartmentTree($departmentTree, $departmentIndex, 0);
|
|
$assignment = self::buildAssistantDepartmentMap(
|
|
$assistantIds,
|
|
$departmentIndex,
|
|
$selectedDeptIds,
|
|
$selectedDeptId
|
|
);
|
|
|
|
$appointmentDaily = self::loadAppointmentDaily(
|
|
min($range['compare_start'], $range['start']),
|
|
$range['day_after_tomorrow'],
|
|
$assistantIds
|
|
);
|
|
$orderDaily = self::loadOrderDaily(
|
|
min($range['compare_start'], $range['start']),
|
|
$range['end'],
|
|
$assistantIds
|
|
);
|
|
|
|
$members = self::buildMemberRows(
|
|
$assistants,
|
|
$assistantIds,
|
|
$assignment,
|
|
$appointmentDaily,
|
|
$orderDaily,
|
|
$range
|
|
);
|
|
$groups = self::buildDepartmentGroups($members, $departmentIndex);
|
|
$summary = self::buildSummary($members, $range);
|
|
$targetDeptIds = self::resolveTargetDeptIds(
|
|
$adminId,
|
|
$scopeValue,
|
|
$selectedAssistantId,
|
|
$selectedDeptIds,
|
|
$selectedDeptId
|
|
);
|
|
$target = self::buildTarget((int) date('Y'), $assistantIds, $targetDeptIds);
|
|
|
|
$selectedDeptName = $selectedDeptId > 0
|
|
? (string) ($departmentIndex[$selectedDeptId]['name'] ?? '')
|
|
: '';
|
|
$selectedAssistantName = '';
|
|
if ($selectedAssistantId > 0) {
|
|
foreach ($assistants as $assistant) {
|
|
if ((int) ($assistant['id'] ?? 0) === $selectedAssistantId) {
|
|
$selectedAssistantName = (string) ($assistant['name'] ?? '');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'meta' => [
|
|
'time_type' => $range['type'],
|
|
'time_label' => $range['label'],
|
|
'start_date' => $range['start'],
|
|
'end_date' => $range['end'],
|
|
'generated_at' => date('Y-m-d H:i:s'),
|
|
'scope_value' => $scopeValue,
|
|
'scope_label' => DataScopeService::scopeLabel($scopeValue),
|
|
'selected_dept_name' => $selectedDeptName,
|
|
'selected_assistant_name' => $selectedAssistantName,
|
|
'member_count' => count($assistantIds),
|
|
'appointment_rule' => '预约日期在统计区间,状态为已预约、已完成或已过号,排除已取消',
|
|
'performance_rule' => '按订单创建时间和创建人统计,排除已取消、拒收和退款',
|
|
],
|
|
'filters' => [
|
|
'departments' => $departmentTree,
|
|
'assistants' => $assistants,
|
|
],
|
|
'summary' => $summary,
|
|
'employee_rows' => $groups,
|
|
'rankings' => [
|
|
'performance' => self::rankMembers($members, 'order_amount', 10),
|
|
'appointments' => self::rankMembers($members, 'appointment_count', 10),
|
|
],
|
|
'departments' => self::departmentSummaryRows($groups),
|
|
'target' => $target,
|
|
];
|
|
}
|
|
|
|
/** @return array<string,string> */
|
|
private static function resolveRange(string $type): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$tomorrow = date('Y-m-d', strtotime('+1 day'));
|
|
$dayAfterTomorrow = date('Y-m-d', strtotime('+2 days'));
|
|
if ($type === 'week') {
|
|
$start = date('Y-m-d', strtotime('monday this week'));
|
|
|
|
return [
|
|
'type' => 'week', 'label' => '本周', 'start' => $start, 'end' => $today,
|
|
'compare_start' => date('Y-m-d', strtotime($start . ' -7 days')),
|
|
'compare_end' => date('Y-m-d', strtotime($today . ' -7 days')),
|
|
'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow,
|
|
];
|
|
}
|
|
if ($type === 'month') {
|
|
$start = date('Y-m-01');
|
|
$previousStart = date('Y-m-01', strtotime('first day of previous month'));
|
|
$previousLastDay = (int) date('t', strtotime($previousStart));
|
|
$day = min((int) date('j'), $previousLastDay);
|
|
|
|
return [
|
|
'type' => 'month', 'label' => '本月', 'start' => $start, 'end' => $today,
|
|
'compare_start' => $previousStart,
|
|
'compare_end' => date('Y-m-d', strtotime($previousStart . ' +' . max(0, $day - 1) . ' days')),
|
|
'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'type' => 'today', 'label' => '今日', 'start' => $today, 'end' => $today,
|
|
'compare_start' => date('Y-m-d', strtotime('-1 day')),
|
|
'compare_end' => date('Y-m-d', strtotime('-1 day')),
|
|
'tomorrow' => $tomorrow, 'day_after_tomorrow' => $dayAfterTomorrow,
|
|
];
|
|
}
|
|
|
|
/** @param array<int,true>|null $allowedSet @return array{0:array<int>,1:bool} */
|
|
private static function resolveSelectedDeptIds(int $selectedDeptId, ?array $allowedSet): array
|
|
{
|
|
if ($selectedDeptId <= 0) {
|
|
return [[], true];
|
|
}
|
|
$ids = self::normalizeIds(DeptLogic::getSelfAndDescendantIds($selectedDeptId));
|
|
if ($allowedSet !== null) {
|
|
$ids = array_values(array_filter($ids, static fn (int $id): bool => isset($allowedSet[$id])));
|
|
}
|
|
|
|
return [$ids, $ids !== []];
|
|
}
|
|
|
|
/** @param int[]|null $visibleIds @param int[] $selectedDeptIds @return array<int,array{id:int,name:string}> */
|
|
private static function assistantOptions(?array $visibleIds, array $selectedDeptIds, int $selectedDeptId): array
|
|
{
|
|
if ($visibleIds === []) {
|
|
return [];
|
|
}
|
|
$query = Db::name('admin')->alias('a')
|
|
->join('admin_role ar', 'ar.admin_id = a.id')
|
|
->where('ar.role_id', self::ASSISTANT_ROLE_ID)
|
|
->where('a.disable', 0)
|
|
->whereNull('a.delete_time');
|
|
if ($visibleIds !== null) {
|
|
$query->whereIn('a.id', $visibleIds);
|
|
}
|
|
if ($selectedDeptId > 0) {
|
|
if ($selectedDeptIds === []) {
|
|
return [];
|
|
}
|
|
$query->join('admin_dept ad', 'ad.admin_id = a.id')
|
|
->whereIn('ad.dept_id', $selectedDeptIds);
|
|
}
|
|
|
|
return $query->field('a.id, a.name')->distinct(true)->order('a.name', 'asc')->select()->toArray();
|
|
}
|
|
|
|
/** @param array<int,array<string,mixed>> $nodes @param array<int,array<string,mixed>> $index */
|
|
private static function flattenDepartmentTree(array $nodes, array &$index, int $depth): void
|
|
{
|
|
foreach ($nodes as $node) {
|
|
$id = (int) ($node['id'] ?? 0);
|
|
if ($id <= 0) {
|
|
continue;
|
|
}
|
|
$index[$id] = [
|
|
'id' => $id,
|
|
'pid' => (int) ($node['pid'] ?? 0),
|
|
'name' => (string) ($node['name'] ?? '未命名部门'),
|
|
'sort' => (int) ($node['sort'] ?? 0),
|
|
'depth' => $depth,
|
|
];
|
|
self::flattenDepartmentTree(
|
|
is_array($node['children'] ?? null) ? $node['children'] : [],
|
|
$index,
|
|
$depth + 1
|
|
);
|
|
}
|
|
}
|
|
|
|
/** @param int[] $assistantIds @param array<int,array<string,mixed>> $deptIndex @param int[] $selectedDeptIds @return array<int,int> */
|
|
private static function buildAssistantDepartmentMap(
|
|
array $assistantIds,
|
|
array $deptIndex,
|
|
array $selectedDeptIds,
|
|
int $selectedDeptId
|
|
): array {
|
|
if ($assistantIds === []) {
|
|
return [];
|
|
}
|
|
$allowed = $selectedDeptId > 0 ? array_fill_keys($selectedDeptIds, true) : null;
|
|
$rows = AdminDept::whereIn('admin_id', $assistantIds)
|
|
->field('admin_id, dept_id')
|
|
->select()
|
|
->toArray();
|
|
$candidates = [];
|
|
foreach ($rows as $row) {
|
|
$aid = (int) ($row['admin_id'] ?? 0);
|
|
$deptId = (int) ($row['dept_id'] ?? 0);
|
|
if (!isset($deptIndex[$deptId]) || ($allowed !== null && !isset($allowed[$deptId]))) {
|
|
continue;
|
|
}
|
|
$candidates[$aid][] = $deptId;
|
|
}
|
|
$out = [];
|
|
foreach ($assistantIds as $aid) {
|
|
$ids = $candidates[$aid] ?? [];
|
|
usort($ids, static function (int $left, int $right) use ($deptIndex): int {
|
|
$depthCompare = (int) ($deptIndex[$right]['depth'] ?? 0) <=> (int) ($deptIndex[$left]['depth'] ?? 0);
|
|
if ($depthCompare !== 0) {
|
|
return $depthCompare;
|
|
}
|
|
|
|
return (int) ($deptIndex[$right]['sort'] ?? 0) <=> (int) ($deptIndex[$left]['sort'] ?? 0);
|
|
});
|
|
$out[$aid] = (int) ($ids[0] ?? 0);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** @param int[] $assistantIds @return array<int,array<string,array{count:int}>> */
|
|
private static function loadAppointmentDaily(string $startDate, string $endDate, array $assistantIds): array
|
|
{
|
|
if ($assistantIds === []) {
|
|
return [];
|
|
}
|
|
$effective = 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))';
|
|
$query = Db::name('doctor_appointment')->alias('a')
|
|
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
|
|
->where('a.appointment_date', 'between', [$startDate, $endDate])
|
|
->whereIn('a.status', [1, 3, 4])
|
|
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)')
|
|
->whereRaw("({$effective}) IN (" . implode(',', $assistantIds) . ')');
|
|
$rows = $query
|
|
->field([
|
|
'a.appointment_date AS date_label',
|
|
Db::raw("({$effective}) AS assistant_id"),
|
|
Db::raw('COUNT(*) AS item_count'),
|
|
])
|
|
->group(['a.appointment_date', $effective])
|
|
->select()
|
|
->toArray();
|
|
$out = [];
|
|
foreach ($rows as $row) {
|
|
$aid = (int) ($row['assistant_id'] ?? 0);
|
|
$date = (string) ($row['date_label'] ?? '');
|
|
if ($aid > 0 && $date !== '') {
|
|
$out[$aid][$date] = ['count' => (int) ($row['item_count'] ?? 0)];
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** @param int[] $assistantIds @return array<int,array<string,array{count:int,amount:float}>> */
|
|
private static function loadOrderDaily(string $startDate, string $endDate, array $assistantIds): array
|
|
{
|
|
if ($assistantIds === []) {
|
|
return [];
|
|
}
|
|
$query = Db::name('tcm_prescription_order')->alias('po')
|
|
->whereNull('po.delete_time')
|
|
->where('po.create_time', 'between', [
|
|
strtotime($startDate . ' 00:00:00'),
|
|
strtotime($endDate . ' 23:59:59'),
|
|
])
|
|
->whereIn('po.creator_id', $assistantIds);
|
|
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, 'po');
|
|
$rows = $query
|
|
->fieldRaw("po.creator_id AS assistant_id, FROM_UNIXTIME(po.create_time, '%Y-%m-%d') AS date_label, COUNT(*) AS item_count, SUM(po.amount) AS amount_sum")
|
|
->group(['po.creator_id', 'date_label'])
|
|
->select()
|
|
->toArray();
|
|
$out = [];
|
|
foreach ($rows as $row) {
|
|
$aid = (int) ($row['assistant_id'] ?? 0);
|
|
$date = (string) ($row['date_label'] ?? '');
|
|
if ($aid > 0 && $date !== '') {
|
|
$out[$aid][$date] = [
|
|
'count' => (int) ($row['item_count'] ?? 0),
|
|
'amount' => round((float) ($row['amount_sum'] ?? 0), 2),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** @return array<int,array<string,mixed>> */
|
|
private static function buildMemberRows(
|
|
array $assistants,
|
|
array $assistantIds,
|
|
array $assignment,
|
|
array $appointmentDaily,
|
|
array $orderDaily,
|
|
array $range
|
|
): array {
|
|
$assistantIndex = [];
|
|
foreach ($assistants as $assistant) {
|
|
$assistantIndex[(int) ($assistant['id'] ?? 0)] = (string) ($assistant['name'] ?? '未命名员工');
|
|
}
|
|
$rows = [];
|
|
foreach ($assistantIds as $aid) {
|
|
$appointmentCount = self::sumDaily($appointmentDaily[$aid] ?? [], $range['start'], $range['end'], 'count');
|
|
$compareAppointmentCount = self::sumDaily($appointmentDaily[$aid] ?? [], $range['compare_start'], $range['compare_end'], 'count');
|
|
$orderCount = self::sumDaily($orderDaily[$aid] ?? [], $range['start'], $range['end'], 'count');
|
|
$orderAmount = self::sumDaily($orderDaily[$aid] ?? [], $range['start'], $range['end'], 'amount');
|
|
$rows[] = [
|
|
'id' => 'admin-' . $aid,
|
|
'admin_id' => $aid,
|
|
'dept_id' => (int) ($assignment[$aid] ?? 0),
|
|
'name' => (string) ($assistantIndex[$aid] ?? '未命名员工'),
|
|
'row_type' => 'employee',
|
|
'appointment_count' => (int) $appointmentCount,
|
|
'compare_appointment_count' => (int) $compareAppointmentCount,
|
|
'appointment_compare_rate' => self::relativeChange($appointmentCount, $compareAppointmentCount),
|
|
'tomorrow_count' => (int) ($appointmentDaily[$aid][$range['tomorrow']]['count'] ?? 0),
|
|
'day_after_count' => (int) ($appointmentDaily[$aid][$range['day_after_tomorrow']]['count'] ?? 0),
|
|
'order_count' => (int) $orderCount,
|
|
'order_amount' => round((float) $orderAmount, 2),
|
|
'status' => 'normal',
|
|
];
|
|
}
|
|
usort($rows, static fn (array $a, array $b): int => ($b['appointment_count'] <=> $a['appointment_count']) ?: ($b['order_amount'] <=> $a['order_amount']));
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/** @param array<int,array<string,mixed>> $members @param array<int,array<string,mixed>> $deptIndex @return array<int,array<string,mixed>> */
|
|
private static function buildDepartmentGroups(array $members, array $deptIndex): array
|
|
{
|
|
$groups = [];
|
|
foreach ($members as $member) {
|
|
$deptId = (int) ($member['dept_id'] ?? 0);
|
|
$key = $deptId > 0 ? $deptId : -2;
|
|
if (!isset($groups[$key])) {
|
|
$groups[$key] = [
|
|
'id' => 'dept-' . $key,
|
|
'dept_id' => $key,
|
|
'name' => $key > 0 ? (string) ($deptIndex[$key]['name'] ?? '未命名部门') : '未分配部门',
|
|
'row_type' => 'department',
|
|
'member_count' => 0,
|
|
'appointment_count' => 0,
|
|
'compare_appointment_count' => 0,
|
|
'tomorrow_count' => 0,
|
|
'day_after_count' => 0,
|
|
'order_count' => 0,
|
|
'order_amount' => 0.0,
|
|
'children' => [],
|
|
'_sort' => $key > 0 ? (int) ($deptIndex[$key]['sort'] ?? 0) : -1,
|
|
];
|
|
}
|
|
$groups[$key]['children'][] = $member;
|
|
$groups[$key]['member_count']++;
|
|
foreach (['appointment_count', 'compare_appointment_count', 'tomorrow_count', 'day_after_count', 'order_count'] as $field) {
|
|
$groups[$key][$field] += (int) ($member[$field] ?? 0);
|
|
}
|
|
$groups[$key]['order_amount'] += (float) ($member['order_amount'] ?? 0);
|
|
}
|
|
foreach ($groups as &$group) {
|
|
$group['order_amount'] = round((float) $group['order_amount'], 2);
|
|
$group['appointment_compare_rate'] = self::relativeChange(
|
|
(float) $group['appointment_count'],
|
|
(float) $group['compare_appointment_count']
|
|
);
|
|
$group['status'] = 'normal';
|
|
}
|
|
unset($group);
|
|
$out = array_values($groups);
|
|
usort($out, static fn (array $a, array $b): int => ($b['_sort'] <=> $a['_sort']) ?: strcmp((string) $a['name'], (string) $b['name']));
|
|
foreach ($out as &$row) {
|
|
unset($row['_sort']);
|
|
}
|
|
unset($row);
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** @param array<int,array<string,mixed>> $members @return array<string,mixed> */
|
|
private static function buildSummary(array $members, array $range): array
|
|
{
|
|
$appointmentCount = 0;
|
|
$compareAppointmentCount = 0;
|
|
$orderCount = 0;
|
|
$orderAmount = 0.0;
|
|
foreach ($members as $member) {
|
|
$appointmentCount += (int) ($member['appointment_count'] ?? 0);
|
|
$compareAppointmentCount += (int) ($member['compare_appointment_count'] ?? 0);
|
|
$orderCount += (int) ($member['order_count'] ?? 0);
|
|
$orderAmount += (float) ($member['order_amount'] ?? 0);
|
|
}
|
|
|
|
return [
|
|
'appointment_count' => $appointmentCount,
|
|
'appointment_compare_count' => $compareAppointmentCount,
|
|
'appointment_compare_rate' => self::relativeChange($appointmentCount, $compareAppointmentCount),
|
|
'order_count' => $orderCount,
|
|
'order_amount' => round($orderAmount, 2),
|
|
'range_label' => $range['label'],
|
|
];
|
|
}
|
|
|
|
/** @param array<int,array<string,mixed>> $members @return array<int,array<string,mixed>> */
|
|
private static function rankMembers(array $members, string $field, int $limit): array
|
|
{
|
|
$rows = $members;
|
|
usort($rows, static fn (array $a, array $b): int => (($b[$field] ?? 0) <=> ($a[$field] ?? 0)) ?: strcmp((string) $a['name'], (string) $b['name']));
|
|
$out = [];
|
|
foreach (array_slice($rows, 0, $limit) as $row) {
|
|
$out[] = [
|
|
'admin_id' => (int) ($row['admin_id'] ?? 0),
|
|
'name' => (string) ($row['name'] ?? ''),
|
|
'value' => $field === 'order_amount'
|
|
? round((float) ($row[$field] ?? 0), 2)
|
|
: (int) ($row[$field] ?? 0),
|
|
'count' => $field === 'order_amount' ? (int) ($row['order_count'] ?? 0) : (int) ($row['appointment_count'] ?? 0),
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** @param array<int,array<string,mixed>> $groups @return array<int,array<string,mixed>> */
|
|
private static function departmentSummaryRows(array $groups): array
|
|
{
|
|
$rows = [];
|
|
foreach ($groups as $group) {
|
|
$copy = $group;
|
|
unset($copy['children']);
|
|
$rows[] = $copy;
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/** @param int[] $selectedDeptIds @return int[]|null */
|
|
private static function resolveTargetDeptIds(
|
|
int $adminId,
|
|
int $scopeValue,
|
|
int $selectedAssistantId,
|
|
array $selectedDeptIds,
|
|
int $selectedDeptId
|
|
): ?array {
|
|
if ($scopeValue === DataScopeService::SCOPE_SELF || $selectedAssistantId > 0) {
|
|
return [];
|
|
}
|
|
|
|
$scopeDeptIds = null;
|
|
if ($scopeValue !== DataScopeService::SCOPE_ALL) {
|
|
$ownDeptIds = self::normalizeIds(AdminDept::where('admin_id', $adminId)->column('dept_id'));
|
|
if ($scopeValue === DataScopeService::SCOPE_DEPT) {
|
|
$scopeDeptIds = $ownDeptIds;
|
|
} else {
|
|
$set = [];
|
|
foreach ($ownDeptIds as $deptId) {
|
|
foreach (DeptLogic::getSelfAndDescendantIds($deptId) as $id) {
|
|
$id = (int) $id;
|
|
if ($id > 0) {
|
|
$set[$id] = true;
|
|
}
|
|
}
|
|
}
|
|
$scopeDeptIds = array_map('intval', array_keys($set));
|
|
}
|
|
}
|
|
|
|
if ($selectedDeptId <= 0) {
|
|
return $scopeDeptIds;
|
|
}
|
|
if ($scopeDeptIds === null) {
|
|
return $selectedDeptIds;
|
|
}
|
|
|
|
return array_values(array_intersect($scopeDeptIds, $selectedDeptIds));
|
|
}
|
|
|
|
/** @param int[] $assistantIds @param int[]|null $targetDeptIds @return array<string,mixed> */
|
|
private static function buildTarget(int $year, array $assistantIds, ?array $targetDeptIds): array
|
|
{
|
|
$targetRows = [];
|
|
if ($targetDeptIds !== []) {
|
|
$query = Db::name('dept_performance_target')->whereLike('year_month', $year . '-%');
|
|
if ($targetDeptIds !== null) {
|
|
$query->whereIn('dept_id', $targetDeptIds);
|
|
}
|
|
$targetRows = $query
|
|
->fieldRaw('`year_month`, SUM(`target_amount`) AS target_amount, COUNT(DISTINCT `dept_id`) AS dept_count')
|
|
->group('`year_month`')
|
|
->select()
|
|
->toArray();
|
|
}
|
|
$actualRows = [];
|
|
if ($assistantIds !== []) {
|
|
$query = Db::name('tcm_prescription_order')->alias('po')
|
|
->whereNull('po.delete_time')
|
|
->whereIn('po.creator_id', $assistantIds)
|
|
->where('po.create_time', 'between', [
|
|
strtotime($year . '-01-01 00:00:00'),
|
|
strtotime($year . '-12-31 23:59:59'),
|
|
]);
|
|
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, 'po');
|
|
$actualRows = $query
|
|
->fieldRaw("DATE_FORMAT(FROM_UNIXTIME(po.create_time), '%m') AS month_no, SUM(po.amount) AS actual_amount")
|
|
->group('month_no')
|
|
->select()
|
|
->toArray();
|
|
}
|
|
$monthlyTarget = array_fill(1, 12, 0.0);
|
|
$monthlyActual = array_fill(1, 12, 0.0);
|
|
$deptCount = 0;
|
|
foreach ($targetRows as $row) {
|
|
$month = (int) substr((string) ($row['year_month'] ?? ''), 5, 2);
|
|
if ($month >= 1 && $month <= 12) {
|
|
$monthlyTarget[$month] = round((float) ($row['target_amount'] ?? 0), 2);
|
|
$deptCount = max($deptCount, (int) ($row['dept_count'] ?? 0));
|
|
}
|
|
}
|
|
foreach ($actualRows as $row) {
|
|
$month = (int) ($row['month_no'] ?? 0);
|
|
if ($month >= 1 && $month <= 12) {
|
|
$monthlyActual[$month] = round((float) ($row['actual_amount'] ?? 0), 2);
|
|
}
|
|
}
|
|
$targetCumulative = [];
|
|
$actualCumulative = [];
|
|
$targetTotal = 0.0;
|
|
$actualTotal = 0.0;
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
$targetTotal = round($targetTotal + $monthlyTarget[$month], 2);
|
|
$actualTotal = round($actualTotal + $monthlyActual[$month], 2);
|
|
$targetCumulative[] = $targetTotal;
|
|
$actualCumulative[] = $actualTotal;
|
|
}
|
|
|
|
return [
|
|
'year' => $year,
|
|
'target_amount' => $targetTotal,
|
|
'actual_amount' => $actualTotal,
|
|
'completion_rate' => $targetTotal > 0 ? round($actualTotal / $targetTotal * 100, 2) : null,
|
|
'department_count' => $deptCount,
|
|
'scope_note' => $targetDeptIds === [] ? '当前为本人或单个员工范围,未设置个人目标' : '按当前可见部门汇总',
|
|
'months' => array_map(static fn (int $month): string => str_pad((string) $month, 2, '0', STR_PAD_LEFT) . '月', range(1, 12)),
|
|
'target_cumulative' => $targetCumulative,
|
|
'actual_cumulative' => $actualCumulative,
|
|
];
|
|
}
|
|
|
|
/** @param array<string,array<string,int|float>> $daily */
|
|
private static function sumDaily(array $daily, string $start, string $end, string $field): float
|
|
{
|
|
$sum = 0.0;
|
|
foreach ($daily as $date => $values) {
|
|
if ($date >= $start && $date <= $end) {
|
|
$sum += (float) ($values[$field] ?? 0);
|
|
}
|
|
}
|
|
|
|
return $sum;
|
|
}
|
|
|
|
private static function relativeChange(float $current, float $previous): ?float
|
|
{
|
|
if (abs($previous) < 0.00001) {
|
|
return null;
|
|
}
|
|
|
|
return round(($current - $previous) / $previous * 100, 2);
|
|
}
|
|
|
|
/** @param array<int|string,mixed> $ids @return int[] */
|
|
private static function normalizeIds(array $ids): array
|
|
{
|
|
return array_values(array_unique(array_filter(array_map('intval', $ids), static fn (int $id): bool => $id > 0)));
|
|
}
|
|
}
|