$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; } }