You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

442 lines
17 KiB
PHP

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
/**
* 报表服务类
* 处理各类统计报表
*/
class ReportService
{
/**
* 获取预约总体统计报表
*
* @param string $dateType 日期类型: daily-日报, weekly-周报, monthly-月报
* @param string $startDate 开始日期 YYYY-MM-DD
* @param string $endDate 结束日期 YYYY-MM-DD
* @param int|null $departmentId 科室ID
* @param int|null $channelId 渠道ID
* @param int|null $patientType 患者类型
* @return array
*/
public function getAppointmentOverallReport($dateType, $startDate, $endDate, $departmentId = null, $channelId = null, $patientType = null)
{
// 验证日期范围
if ($startDate > $endDate) {
return ['success' => false, 'message' => '开始日期不能大于结束日期'];
}
// 根据日期类型确定分组粒度
$dateFormat = $this->getDateFormatByType($dateType);
$groupByRaw = $this->getGroupByRawByType($dateType);
// 构建查询
$query = DB::table('exam_appointment as ea')
->leftJoin('exam_application as eapp', 'ea.exam_application_id', '=', 'eapp.id')
->leftJoin('department as d', 'eapp.examination_department_code', '=', 'd.code')
->leftJoin('channel as c', 'ea.channel_id', '=', 'c.id')
->leftJoin('department_resource as dr', 'ea.department_resource_id', '=', 'dr.id')
->whereDate('ea.appointment_time', '>=', $startDate)
->whereDate('ea.appointment_time', '<=', $endDate)
->select([
DB::raw("$dateFormat as date_label"),
DB::raw('COUNT(*) as total_appointments'),
DB::raw('SUM(CASE WHEN ea.status = 1 THEN 1 ELSE 0 END) as booked_count'),
DB::raw('SUM(CASE WHEN ea.status = 2 THEN 1 ELSE 0 END) as checked_in_count'),
DB::raw('SUM(CASE WHEN ea.status = 3 THEN 1 ELSE 0 END) as completed_count'),
DB::raw('SUM(CASE WHEN ea.status = 4 THEN 1 ELSE 0 END) as cancelled_count'),
DB::raw('SUM(CASE WHEN ea.status = 5 THEN 1 ELSE 0 END) as no_show_count'),
]);
// 科室筛选
if ($departmentId != null) {
$query->where('d.id', $departmentId);
}
// 渠道筛选
if ($channelId != null) {
$query->where('ea.channel_id', $channelId);
}
// 患者类型筛选
if ($patientType != null) {
$query->where('eapp.patient_type', $patientType);
}
// 分组查询
$data = $query->groupByRaw($groupByRaw)
->orderBy('date_label')
->get()
->map(function ($item) {
// 计算百分比
$total = $item->total_appointments ?: 1; // 避免除零
$bookedTotal = $item->booked_count ?: 1; // 避免除零
$item->success_rate = round(($item->booked_count / $total) * 100, 2);
$item->check_in_rate = round(($item->checked_in_count / $bookedTotal) * 100, 2);
$item->completion_rate = round(($item->completed_count / $bookedTotal) * 100, 2);
// 取消率和爽约率基于预约总数计算
$item->cancel_rate = round(($item->cancelled_count / $total) * 100, 2);
$item->no_show_rate = round(($item->no_show_count / $total) * 100, 2);
return $item;
});
// 计算汇总数据
$summary = $this->calculateSummary($data);
return [
'success' => true,
'data' => [
'list' => $data,
'summary' => $summary,
'report_type' => $this->getReportTypeName($dateType),
'date_range' => [
'start' => $startDate,
'end' => $endDate
]
]
];
}
/**
* 根据日期类型获取日期格式
*/
private function getDateFormatByType($dateType)
{
switch ($dateType) {
case 'weekly':
return "CONCAT(DATE_FORMAT(DATE_SUB(ea.appointment_time, INTERVAL WEEKDAY(ea.appointment_time) DAY), '%m/%d'), ' - ', DATE_FORMAT(DATE_ADD(DATE_SUB(ea.appointment_time, INTERVAL WEEKDAY(ea.appointment_time) DAY), INTERVAL 6 DAY), '%m/%d'))";
case 'monthly':
return "DATE_FORMAT(ea.appointment_time, '%Y-%m')";
case 'daily':
default:
return "DATE(ea.appointment_time)";
}
}
/**
* 根据日期类型获取分组字段
*/
private function getGroupByRawByType($dateType)
{
switch ($dateType) {
case 'weekly':
return "CONCAT(DATE_FORMAT(DATE_SUB(ea.appointment_time, INTERVAL WEEKDAY(ea.appointment_time) DAY), '%m/%d'), ' - ', DATE_FORMAT(DATE_ADD(DATE_SUB(ea.appointment_time, INTERVAL WEEKDAY(ea.appointment_time) DAY), INTERVAL 6 DAY), '%m/%d'))";
case 'monthly':
return "DATE_FORMAT(ea.appointment_time, '%Y-%m')";
case 'daily':
default:
return "DATE(ea.appointment_time)";
}
}
/**
* 获取报表类型名称
*/
private function getReportTypeName($dateType)
{
$names = [
'daily' => '日报',
'weekly' => '周报',
'monthly' => '月报'
];
return $names[$dateType] ?? '日报';
}
/**
* 计算汇总数据
*/
private function calculateSummary($data)
{
if ($data->isEmpty()) {
return [
'total_appointments' => 0,
'total_booked' => 0,
'total_checked_in' => 0,
'total_completed' => 0,
'total_cancelled' => 0,
'total_no_show' => 0,
'avg_check_in_rate' => 0,
'avg_completion_rate' => 0,
'avg_cancel_rate' => 0,
'avg_no_show_rate' => 0
];
}
$summary = [
'total_appointments' => 0,
'total_booked' => 0,
'total_checked_in' => 0,
'total_completed' => 0,
'total_cancelled' => 0,
'total_no_show' => 0
];
foreach ($data as $item) {
$summary['total_appointments'] += $item->total_appointments;
$summary['total_booked'] += $item->booked_count;
$summary['total_checked_in'] += $item->checked_in_count;
$summary['total_completed'] += $item->completed_count;
$summary['total_cancelled'] += $item->cancelled_count;
$summary['total_no_show'] += $item->no_show_count;
}
// 计算平均比率
// 取消率和爽约率基于预约总数计算(因为预约可能直接被取消)
// 到检率和完成率基于预约成功数计算
$totalBooked = $summary['total_booked'] ?: 1;
$totalAppointments = $summary['total_appointments'] ?: 1;
$summary['avg_check_in_rate'] = round(($summary['total_checked_in'] / $totalBooked) * 100, 2);
$summary['avg_completion_rate'] = round(($summary['total_completed'] / $totalBooked) * 100, 2);
$summary['avg_cancel_rate'] = round(($summary['total_cancelled'] / $totalAppointments) * 100, 2);
$summary['avg_no_show_rate'] = round(($summary['total_no_show'] / $totalAppointments) * 100, 2);
return $summary;
}
/**
* 获取时间段分布报表(检查室 × 时间段矩阵)
*
* @param string $startDate 开始日期 YYYY-MM-DD
* @param string $endDate 结束日期 YYYY-MM-DD
* @param int|null $departmentId 科室 ID
* @param int|null $resourceId 资源 ID多资源逗号分隔
* @param int|null $channelId 渠道 ID
* @param int|null $patientType 患者类型
* @return array
*/
public function getTimeDistributionReport($startDate, $endDate, $departmentId = null, $resourceId = null, $channelId = null, $patientType = null)
{
if ($startDate > $endDate) {
return ['success' => false, 'message' => '开始日期不能大于结束日期'];
}
$timeSlots = [
'08:00-09:00', '09:00-10:00', '10:00-11:00', '11:00-12:00',
'12:00-13:00', '13:00-14:00', '14:00-15:00', '15:00-16:00',
'16:00-17:00', '17:00-18:00', '18:00-19:00', '19:00-20:00'
];
$resourcesQuery = DB::table('department_resource as dr')
->leftJoin('department as d', 'dr.department_id', '=', 'd.id')
->where('dr.deleted', 0)
->where('dr.status', 1)
->select('dr.id', 'dr.name as resource_name', 'dr.department_id', 'd.name as department_name');
if ($departmentId != null && $departmentId !== '') {
$resourcesQuery->where('dr.department_id', $departmentId);
}
if ($resourceId != null && $resourceId !== '') {
$resourceIds = is_array($resourceId) ? $resourceId : explode(',', $resourceId);
$resourcesQuery->whereIn('dr.id', $resourceIds);
}
$resources = $resourcesQuery->orderBy('dr.department_id', 'asc')
->orderBy('dr.id', 'asc')
->get()
->keyBy('id');
if ($resources->isEmpty()) {
return [
'success' => true,
'data' => [
'resources' => [],
'time_slots' => $timeSlots,
'matrix' => [],
'date_range' => ['start' => $startDate, 'end' => $endDate]
]
];
}
$appointmentsQuery = DB::table('exam_appointment as ea')
->leftJoin('plan as p', 'ea.time_slot_id', '=', 'p.id')
->leftJoin('plan_batch as pb', 'p.batch_id', '=', 'pb.id')
->leftJoin('exam_application as eapp', 'ea.exam_application_id', '=', 'eapp.id')
->leftJoin('channel as c', 'ea.channel_id', '=', 'c.id')
->whereDate('pb.plan_date', '>=', $startDate)
->whereDate('pb.plan_date', '<=', $endDate)
->where('pb.deleted', 0)
->where('p.deleted', 0);
if ($departmentId != null && $departmentId !== '') {
$appointmentsQuery->where('pb.department_id', $departmentId);
}
if ($resourceId != null && $resourceId !== '') {
$resourceIds = is_array($resourceId) ? $resourceId : explode(',', $resourceId);
$appointmentsQuery->whereIn('ea.department_resource_id', $resourceIds);
}
if ($channelId != null && $channelId !== '') {
$appointmentsQuery->where('ea.channel_id', $channelId);
}
if ($patientType != null && $patientType !== '') {
$appointmentsQuery->where('eapp.patient_type', $patientType);
}
$appointmentStats = $appointmentsQuery
->select([
'ea.department_resource_id',
DB::raw("TIME_FORMAT(p.start_time, '%H:00') as hour_time"),
DB::raw('COUNT(*) as appointment_count'),
DB::raw('SUM(CASE WHEN ea.status = 2 THEN 1 ELSE 0 END) as checked_in_count'),
DB::raw('SUM(CASE WHEN ea.status = 3 THEN 1 ELSE 0 END) as completed_count'),
DB::raw('SUM(CASE WHEN ea.status = 4 THEN 1 ELSE 0 END) as cancelled_count'),
DB::raw('SUM(CASE WHEN ea.status = 5 THEN 1 ELSE 0 END) as no_show_count'),
])
->groupBy('ea.department_resource_id', DB::raw("TIME_FORMAT(p.start_time, '%H:00')"))
->get()
->groupBy('department_resource_id');
$planQuery = DB::table('plan as p')
->leftJoin('plan_batch as pb', 'p.batch_id', '=', 'pb.id')
->whereDate('pb.plan_date', '>=', $startDate)
->whereDate('pb.plan_date', '<=', $endDate)
->where('pb.deleted', 0)
->where('p.deleted', 0);
if ($departmentId != null && $departmentId !== '') {
$planQuery->where('pb.department_id', $departmentId);
}
if ($resourceId != null && $resourceId !== '') {
$resourceIds = is_array($resourceId) ? $resourceId : explode(',', $resourceId);
$planQuery->whereIn('pb.resource_id', $resourceIds);
}
$planStats = $planQuery
->select([
'pb.resource_id',
DB::raw("TIME_FORMAT(p.start_time, '%H:00') as hour_time"),
DB::raw('SUM(p.total_quota) as planned_quota'),
])
->groupBy('pb.resource_id', DB::raw("TIME_FORMAT(p.start_time, '%H:00')"))
->get()
->groupBy('resource_id');
$matrix = [];
foreach ($resources as $resourceIdKey => $resource) {
$resourceAppointments = $appointmentStats->get($resourceIdKey, collect());
$resourcePlans = $planStats->get($resourceIdKey, collect());
$row = [
'resource_id' => $resourceIdKey,
'resource_name' => $resource->resource_name,
'department_id' => $resource->department_id,
'department_name' => $resource->department_name ?? '',
'slots' => []
];
foreach ($timeSlots as $slot) {
$hourKey = substr($slot, 0, 5);
$stat = null;
foreach ($resourceAppointments as $item) {
if ($item->hour_time === $hourKey) {
$stat = $item;
break;
}
}
$planned = null;
foreach ($resourcePlans as $item) {
if ($item->hour_time === $hourKey) {
$planned = $item;
break;
}
}
$appointmentCount = $stat ? (int)$stat->appointment_count : 0;
$checkedInCount = $stat ? (int)$stat->checked_in_count : 0;
$completedCount = $stat ? (int)$stat->completed_count : 0;
$cancelledCount = $stat ? (int)$stat->cancelled_count : 0;
$noShowCount = $stat ? (int)$stat->no_show_count : 0;
$plannedQuota = $planned ? (int)$planned->planned_quota : 0;
$checkInRate = $appointmentCount > 0 ? round(($checkedInCount / $appointmentCount) * 100, 2) : 0;
$completionRate = $appointmentCount > 0 ? round(($completedCount / $appointmentCount) * 100, 2) : 0;
$utilizationRate = $plannedQuota > 0 ? round(($appointmentCount / $plannedQuota) * 100, 2) : 0;
$row['slots'][] = [
'time_period' => $slot,
'appointment_count' => $appointmentCount,
'checked_in_count' => $checkedInCount,
'completed_count' => $completedCount,
'cancelled_count' => $cancelledCount,
'no_show_count' => $noShowCount,
'planned_quota' => $plannedQuota,
'check_in_rate' => $checkInRate,
'completion_rate' => $completionRate,
'utilization_rate' => $utilizationRate,
];
}
$matrix[] = $row;
}
return [
'success' => true,
'data' => [
'resources' => array_values($resources->map(fn($r) => [
'id' => $r->id,
'name' => $r->resource_name,
'department_id' => $r->department_id,
'department_name' => $r->department_name ?? ''
])->values()->all()),
'time_slots' => $timeSlots,
'matrix' => $matrix,
'date_range' => ['start' => $startDate, 'end' => $endDate]
]
];
}
/**
* 计算汇总数据
*/
private function calculateSummaryOld($data)
{
if ($data->isEmpty()) {
return [
'total_appointments' => 0,
'total_booked' => 0,
'total_checked_in' => 0,
'total_completed' => 0,
'total_cancelled' => 0,
'total_no_show' => 0
];
}
$summary = [
'total_appointments' => 0,
'total_booked' => 0,
'total_checked_in' => 0,
'total_completed' => 0,
'total_cancelled' => 0,
'total_no_show' => 0
];
foreach ($data as $item) {
$summary['total_appointments'] += $item->total_appointments;
$summary['total_booked'] += $item->booked_count;
$summary['total_checked_in'] += $item->checked_in_count;
$summary['total_completed'] += $item->completed_count;
$summary['total_cancelled'] += $item->cancelled_count;
$summary['total_no_show'] += $item->no_show_count;
}
$totalBooked = $summary['total_booked'] ?: 1;
$totalAppointments = $summary['total_appointments'] ?: 1;
$summary['avg_check_in_rate'] = round(($summary['total_checked_in'] / $totalBooked) * 100, 2);
$summary['avg_completion_rate'] = round(($summary['total_completed'] / $totalBooked) * 100, 2);
$summary['avg_cancel_rate'] = round(($summary['total_cancelled'] / $totalAppointments) * 100, 2);
$summary['avg_no_show_rate'] = round(($summary['total_no_show'] / $totalAppointments) * 100, 2);
return $summary;
}
}