更新
This commit is contained in:
@@ -31,8 +31,10 @@ use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminDept;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\dict\DictData;
|
||||
use app\common\model\tcm\DiagnosisRevisitDue;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\lists\Traits\HasDataScopeFilter;
|
||||
use app\common\service\tcm\RevisitDueService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
@@ -143,6 +145,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
|
||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||
$this->applyLatestAssignFilters($query, $pendingWideSearch);
|
||||
$this->applyRevisitDueFilters($query, $pendingWideSearch);
|
||||
|
||||
$this->applyPendingAssignBusinessOrderMonthFilter($query);
|
||||
|
||||
@@ -460,6 +463,19 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
unset($item);
|
||||
|
||||
// 复诊到期(契约 C):按快照表批量读取,缺行/不一致时由服务内部自愈(仅小页前 50 行);读取异常由服务降级为 none 占位
|
||||
$revisitDueMap = [];
|
||||
if (!empty($diagIdsForUnserved)) {
|
||||
$revisitDueMap = RevisitDueService::buildListPayload($diagIdsForUnserved, [
|
||||
'self_heal' => true,
|
||||
'page_size' => (int) $this->limitLength,
|
||||
]);
|
||||
}
|
||||
foreach ($lists as &$item) {
|
||||
$item['revisit_due'] = $revisitDueMap[(int) $item['id']] ?? RevisitDueService::emptyPayload();
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
@@ -598,6 +614,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
|
||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||
$this->applyLatestAssignFilters($query, $pendingWideSearch);
|
||||
$this->applyRevisitDueFilters($query, $pendingWideSearch);
|
||||
|
||||
// 仅已开方(待分配+关键词检索时不限制)
|
||||
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
|
||||
@@ -714,11 +731,120 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表默认排序;支持 sort_unserved_days=asc|desc 按未服务天数排序。
|
||||
* 复诊到期筛选(方案 §3.2 ②):以快照表 tcm_diagnosis_revisit_due 的 EXISTS 子查询实现。
|
||||
*
|
||||
* revisit_state:due(超期 ∪ 过号,排除已承诺未到期)| warning(到期前 warn_days 内)| booked(已约且日期未过)
|
||||
* | visited(已面诊)| contacted(已承诺复诊日未过)| no_days(有有效订单但缺开药天数)| pending(到期日在预警窗之外)
|
||||
* revisit_days_bucket:14|21|28|30 精确命中档位;other = 未命中档位且有开药天数
|
||||
* 待分配宽搜不套此过滤;lists() 与 count() 各调用一次保证分页总数一致。
|
||||
*
|
||||
* @param mixed $query
|
||||
*/
|
||||
private function applyRevisitDueFilters($query, bool $pendingWideSearch): void
|
||||
{
|
||||
if ($pendingWideSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = strtolower(trim((string) ($this->params['revisit_state'] ?? '')));
|
||||
$bucket = strtolower(trim((string) ($this->params['revisit_days_bucket'] ?? '')));
|
||||
if ($state === '' && $bucket === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$rdTbl = (new DiagnosisRevisitDue())->getTable();
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$conditions = ["rd.diagnosis_id = {$diagTbl}.id"];
|
||||
|
||||
if ($state !== '') {
|
||||
$warnDays = (int) (RevisitDueService::config()['warn_days'] ?? 3);
|
||||
if ($warnDays < 0) {
|
||||
$warnDays = 0;
|
||||
}
|
||||
$warnEdge = "DATE_ADD(CURDATE(), INTERVAL {$warnDays} DAY)";
|
||||
switch ($state) {
|
||||
case 'due':
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = '(rd.appointment_state = 4 OR (rd.due_date IS NOT NULL AND rd.due_date <= CURDATE()))';
|
||||
$conditions[] = '(rd.promised_date IS NULL OR rd.promised_date < CURDATE())';
|
||||
break;
|
||||
case 'warning':
|
||||
// 与读取侧 deriveState 对齐:过号(4)/已约已过期/已联系(承诺日未到) 的行优先级更高,不落入预警
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = 'rd.due_date IS NOT NULL';
|
||||
$conditions[] = 'rd.due_date > CURDATE()';
|
||||
$conditions[] = "rd.due_date <= {$warnEdge}";
|
||||
$conditions[] = 'rd.appointment_state <> 4';
|
||||
$conditions[] = 'NOT (rd.appointment_state = 1 AND rd.appointment_date < CURDATE())';
|
||||
$conditions[] = '(rd.promised_date IS NULL OR rd.promised_date < CURDATE())';
|
||||
break;
|
||||
case 'booked':
|
||||
$conditions[] = 'rd.appointment_state = 1';
|
||||
$conditions[] = 'rd.appointment_date IS NOT NULL';
|
||||
$conditions[] = 'rd.appointment_date >= CURDATE()';
|
||||
break;
|
||||
case 'visited':
|
||||
$conditions[] = '(rd.closed_state = 3 OR rd.appointment_state = 3)';
|
||||
break;
|
||||
case 'contacted':
|
||||
$conditions[] = 'rd.promised_date IS NOT NULL';
|
||||
$conditions[] = 'rd.promised_date >= CURDATE()';
|
||||
break;
|
||||
case 'no_days':
|
||||
// 与 deriveState 的 no_days 分支对齐:进行中、有有效订单但算不出预计复诊日(失效行不计)
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = 'rd.valid_order_count > 0';
|
||||
$conditions[] = 'rd.due_date IS NULL';
|
||||
break;
|
||||
case 'pending':
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = 'rd.due_date IS NOT NULL';
|
||||
$conditions[] = "rd.due_date > {$warnEdge}";
|
||||
$conditions[] = 'rd.appointment_state <> 4';
|
||||
$conditions[] = 'NOT (rd.appointment_state = 1 AND rd.appointment_date < CURDATE())';
|
||||
$conditions[] = '(rd.promised_date IS NULL OR rd.promised_date < CURDATE())';
|
||||
break;
|
||||
default:
|
||||
// 未知状态值:不做任何收窄(与其它未知筛选参数一致,避免误清空列表)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bucket !== '') {
|
||||
if ($bucket === 'other') {
|
||||
$conditions[] = 'rd.rule_days = 0';
|
||||
$conditions[] = 'rd.medication_days > 0';
|
||||
} elseif (ctype_digit($bucket) && (int) $bucket > 0) {
|
||||
$conditions[] = 'rd.rule_days = ' . (int) $bucket;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($conditions) <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereExists("SELECT 1 FROM {$rdTbl} rd WHERE " . implode(' AND ', $conditions));
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表默认排序;支持 sort_revisit_due=asc|desc 按复诊到期日排序(优先),
|
||||
* 或 sort_unserved_days=asc|desc 按未服务天数排序;两者互斥,前者存在时忽略后者。
|
||||
*/
|
||||
private function resolveListOrderRaw(bool $pendingWideSearch): string
|
||||
{
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
|
||||
// 复诊到期日排序(方案 §3.2 ③):单行子查询取进行中快照的 due_date,无快照 / 无到期日的行置后
|
||||
$sortRevisitDue = strtolower(trim((string) ($this->params['sort_revisit_due'] ?? '')));
|
||||
if (in_array($sortRevisitDue, ['asc', 'desc'], true)) {
|
||||
$rdTbl = (new DiagnosisRevisitDue())->getTable();
|
||||
$dueExpr = "(SELECT rd.due_date FROM {$rdTbl} rd WHERE rd.diagnosis_id = {$diagTbl}.id AND rd.closed_state = 0 LIMIT 1)";
|
||||
$nullLast = "CASE WHEN {$dueExpr} IS NULL THEN 1 ELSE 0 END ASC";
|
||||
$dir = $sortRevisitDue === 'desc' ? 'DESC' : 'ASC';
|
||||
|
||||
return "{$nullLast}, {$dueExpr} {$dir}, {$diagTbl}.id DESC";
|
||||
}
|
||||
|
||||
$sortUnserved = strtolower(trim((string) ($this->params['sort_unserved_days'] ?? '')));
|
||||
if (in_array($sortUnserved, ['asc', 'desc'], true)) {
|
||||
$anchorExpr = $this->unservedAnchorExpr($diagTbl);
|
||||
|
||||
@@ -25,6 +25,7 @@ use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
use app\common\model\ExpressTracking;
|
||||
use app\common\service\gancao\GancaoScmRecipelService;
|
||||
use app\common\service\pharmacy\EjPharmacyClient;
|
||||
use app\common\service\tcm\RevisitVisitSlotService;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\db\Query;
|
||||
@@ -1464,7 +1465,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单编辑-业务订单 tab:标注全局诊次及是否计入复诊接诊率(与 RevisitRateLogic 同口径)
|
||||
* 诊单编辑-业务订单 tab:标注全局诊次及是否计入复诊接诊率(与 RevisitRateLogic 同口径)。
|
||||
* 诊次口径单点在 RevisitVisitSlotService:有效订单序列(fetchOrderSeqRows,一次 SQL 覆盖全部诊单,消除逐诊单 N+1)
|
||||
* + 诊单偏移(fetchRevisitSlotStartOffsetMap / resolveRevisitSlotStartOffset 钳 0~20);global_visit_seq / counts_for_revisit_rate 规则不变。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $lists
|
||||
*/
|
||||
@@ -1490,26 +1493,23 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
return;
|
||||
}
|
||||
|
||||
$offsetRows = Diagnosis::whereIn('id', $diagIdList)
|
||||
->whereNull('delete_time')
|
||||
->column('revisit_slot_start_offset', 'id');
|
||||
$offsetMap = [];
|
||||
foreach ($offsetRows as $id => $offset) {
|
||||
$offsetMap[(int) $id] = max(0, min(20, (int) $offset));
|
||||
}
|
||||
$offsetMap = RevisitVisitSlotService::fetchRevisitSlotStartOffsetMap($diagIdList);
|
||||
|
||||
/** @var array<int, int> $seqByOrderId order_id => global seq within diagnosis */
|
||||
/** @var array<int, int> $seqByOrderId order_id => 诊单内实单全局序号 */
|
||||
$seqByOrderId = [];
|
||||
foreach ($diagIdList as $did) {
|
||||
$q = PrescriptionOrder::where('diagnosis_id', $did)->whereNull('delete_time');
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
|
||||
$orderIds = $q
|
||||
->order(['create_time' => 'asc', 'id' => 'asc'])
|
||||
->column('id');
|
||||
foreach (array_chunk($diagIdList, 2000) as $chunk) {
|
||||
// 已按 (diagnosis_id, create_time, id) 升序,同一诊单连续
|
||||
$orderRows = RevisitVisitSlotService::fetchOrderSeqRows($chunk, ['o.id', 'o.diagnosis_id']);
|
||||
$curDid = 0;
|
||||
$seq = 0;
|
||||
foreach ($orderIds as $oid) {
|
||||
foreach ($orderRows as $r) {
|
||||
$did = (int) ($r['diagnosis_id'] ?? 0);
|
||||
if ($did !== $curDid) {
|
||||
$curDid = $did;
|
||||
$seq = 0;
|
||||
}
|
||||
$seq++;
|
||||
$seqByOrderId[(int) $oid] = $seq;
|
||||
$seqByOrderId[(int) ($r['id'] ?? 0)] = $seq;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1517,7 +1517,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
$oid = (int) ($row['id'] ?? 0);
|
||||
$did = (int) ($row['diagnosis_id'] ?? 0);
|
||||
$seq = (int) ($seqByOrderId[$oid] ?? 0);
|
||||
$offset = (int) ($offsetMap[$did] ?? 0);
|
||||
$offset = RevisitVisitSlotService::resolveRevisitSlotStartOffset($did, $offsetMap);
|
||||
$effectiveSlot = $seq > 0 ? $seq + $offset : 0;
|
||||
$row['global_visit_seq'] = $effectiveSlot > 0 ? $effectiveSlot : null;
|
||||
$row['raw_visit_seq'] = $seq > 0 ? $seq : null;
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\lists\tcm;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\tcm\RevisitDueLogic;
|
||||
use app\common\lists\ListsExcelInterface;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\lists\Traits\HasDataScopeFilter;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminDept;
|
||||
use app\common\model\dept\Dept;
|
||||
use app\common\model\tcm\DiagnosisRevisitDue;
|
||||
use app\common\service\tcm\RevisitDueCalculator;
|
||||
use app\common\service\tcm\RevisitDueService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 复诊到期跟进 - 主管团队视图列表(P2,方案 §3.3 tcm.revisitDue/lists)
|
||||
*
|
||||
* 数据来源:tcm_diagnosis_revisit_due 快照表(别名 rd),一诊单一行;时间相关状态由 RevisitDueCalculator::deriveState 按 today 推导。
|
||||
* 筛选:assistant_id / dept_id(经 admin_dept 子查询,含下级部门)/ state / days_bucket / min_snooze / keyword(患者姓名);默认排除失效行(closed_state<>5)。
|
||||
* state 的 SQL 条件与 DiagnosisLists::applyRevisitDueFilters 完全一致,另加 closed = closed_state=4(人工关闭,主管视图标记)。
|
||||
* 数据范围:HasDataScopeFilter::applyDataScopeByOwner('rd.assistant_id'),主管按角色 data_scope 看下属。
|
||||
* 排序:escalation_level desc(越点越上心优先),due_date asc(NULL 置后),id asc。
|
||||
* 导出:export=1/2,字段见 setExcelFields。
|
||||
*/
|
||||
class RevisitDueLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExcelInterface
|
||||
{
|
||||
use HasDataScopeFilter;
|
||||
|
||||
/** 支持的 state 筛选值 */
|
||||
public const STATES = ['due', 'warning', 'booked', 'visited', 'contacted', 'no_days', 'pending', 'closed'];
|
||||
|
||||
/**
|
||||
* @notes 设置搜索条件(精确匹配列走框架 createWhere,其余在 buildQuery 内处理)
|
||||
*/
|
||||
public function setSearch(): array
|
||||
{
|
||||
return [
|
||||
'=' => ['rd.assistant_id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取列表
|
||||
*/
|
||||
public function lists(): array
|
||||
{
|
||||
$rows = $this->buildQuery()
|
||||
->orderRaw('rd.escalation_level DESC, rd.due_date IS NULL ASC, rd.due_date ASC, rd.id ASC')
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cfg = RevisitDueService::config();
|
||||
$today = date('Y-m-d');
|
||||
$assistantIds = array_values(array_unique(array_map(static fn (array $r): int => (int) $r['assistant_id'], $rows)));
|
||||
$nameMap = $this->fetchAssistantNames($assistantIds);
|
||||
$deptMap = $this->fetchAssistantDeptNames($assistantIds);
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$out[] = $this->formatRow($row, $today, $cfg, $nameMap, $deptMap);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取数量
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->buildQuery()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 导出文件名
|
||||
*/
|
||||
public function setFileName(): string
|
||||
{
|
||||
return '复诊到期跟进';
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 导出字段
|
||||
*/
|
||||
public function setExcelFields(): array
|
||||
{
|
||||
return [
|
||||
'patient_name' => '患者',
|
||||
'assistant_name' => '医助',
|
||||
'dept_name' => '部门',
|
||||
'export_slot' => '第N诊',
|
||||
'medication_days' => '开药天数',
|
||||
'due_date' => '预计复诊',
|
||||
'export_overdue_days' => '超期天数',
|
||||
'state_text' => '状态',
|
||||
'snooze_count' => '延迟次数',
|
||||
'contact_count' => '已联系',
|
||||
'promised_date' => '承诺日',
|
||||
'missed_count' => '过号次数',
|
||||
'export_close_reason' => '关闭原因',
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 内部
|
||||
|
||||
/**
|
||||
* @return \think\db\Query
|
||||
*/
|
||||
private function buildQuery()
|
||||
{
|
||||
$query = Db::name(RevisitDueService::TABLE)->alias('rd')->where($this->searchWhere);
|
||||
|
||||
// 默认排除失效行(无医助 / 诊单删除或禁用 / 无有效订单)
|
||||
$query->where('rd.closed_state', '<>', RevisitDueCalculator::CLOSED_INVALID);
|
||||
|
||||
// 数据范围:主管按角色 data_scope 看下属医助
|
||||
$this->applyDataScopeByOwner($query, 'rd.assistant_id');
|
||||
|
||||
// 部门:医助 ∈ admin_dept 且部门在所选部门子树内
|
||||
$deptId = (int) ($this->params['dept_id'] ?? 0);
|
||||
if ($deptId > 0) {
|
||||
$deptIds = array_values(array_filter(array_map('intval', DeptLogic::getSelfAndDescendantIds($deptId)), static fn (int $v): bool => $v > 0));
|
||||
if ($deptIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} else {
|
||||
$adTbl = (new AdminDept())->getTable();
|
||||
$query->whereExists('SELECT 1 FROM `' . $adTbl . '` ad WHERE ad.admin_id = rd.assistant_id AND ad.dept_id IN (' . implode(',', $deptIds) . ')');
|
||||
}
|
||||
}
|
||||
|
||||
// 患者姓名
|
||||
$keyword = trim((string) ($this->params['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->whereLike('rd.patient_name', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 延迟次数下限
|
||||
$minSnooze = (int) ($this->params['min_snooze'] ?? 0);
|
||||
if ($minSnooze > 0) {
|
||||
$query->where('rd.snooze_count', '>=', $minSnooze);
|
||||
}
|
||||
|
||||
$this->applyStateFilter($query, strtolower(trim((string) ($this->params['state'] ?? ''))));
|
||||
$this->applyBucketFilter($query, strtolower(trim((string) ($this->params['days_bucket'] ?? ''))));
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* state 筛选:SQL 条件与 DiagnosisLists::applyRevisitDueFilters 完全一致(表别名 rd),另加 closed。
|
||||
*
|
||||
* @param mixed $query
|
||||
*/
|
||||
private function applyStateFilter($query, string $state): void
|
||||
{
|
||||
if ($state === '' || !in_array($state, self::STATES, true)) {
|
||||
return;
|
||||
}
|
||||
$warnDays = (int) (RevisitDueService::config()['warn_days'] ?? 3);
|
||||
if ($warnDays < 0) {
|
||||
$warnDays = 0;
|
||||
}
|
||||
$warnEdge = "DATE_ADD(CURDATE(), INTERVAL {$warnDays} DAY)";
|
||||
$conditions = [];
|
||||
switch ($state) {
|
||||
case 'due':
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = '(rd.appointment_state = 4 OR (rd.due_date IS NOT NULL AND rd.due_date <= CURDATE()))';
|
||||
$conditions[] = '(rd.promised_date IS NULL OR rd.promised_date < CURDATE())';
|
||||
break;
|
||||
case 'warning':
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = 'rd.due_date IS NOT NULL';
|
||||
$conditions[] = 'rd.due_date > CURDATE()';
|
||||
$conditions[] = "rd.due_date <= {$warnEdge}";
|
||||
$conditions[] = 'rd.appointment_state <> 4';
|
||||
$conditions[] = 'NOT (rd.appointment_state = 1 AND rd.appointment_date < CURDATE())';
|
||||
$conditions[] = '(rd.promised_date IS NULL OR rd.promised_date < CURDATE())';
|
||||
break;
|
||||
case 'booked':
|
||||
$conditions[] = 'rd.appointment_state = 1';
|
||||
$conditions[] = 'rd.appointment_date IS NOT NULL';
|
||||
$conditions[] = 'rd.appointment_date >= CURDATE()';
|
||||
break;
|
||||
case 'visited':
|
||||
$conditions[] = '(rd.closed_state = 3 OR rd.appointment_state = 3)';
|
||||
break;
|
||||
case 'contacted':
|
||||
$conditions[] = 'rd.promised_date IS NOT NULL';
|
||||
$conditions[] = 'rd.promised_date >= CURDATE()';
|
||||
break;
|
||||
case 'no_days':
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = 'rd.valid_order_count > 0';
|
||||
$conditions[] = 'rd.due_date IS NULL';
|
||||
break;
|
||||
case 'pending':
|
||||
$conditions[] = 'rd.closed_state = 0';
|
||||
$conditions[] = 'rd.due_date IS NOT NULL';
|
||||
$conditions[] = "rd.due_date > {$warnEdge}";
|
||||
$conditions[] = 'rd.appointment_state <> 4';
|
||||
$conditions[] = 'NOT (rd.appointment_state = 1 AND rd.appointment_date < CURDATE())';
|
||||
$conditions[] = '(rd.promised_date IS NULL OR rd.promised_date < CURDATE())';
|
||||
break;
|
||||
case 'closed':
|
||||
$conditions[] = 'rd.closed_state = ' . RevisitDueCalculator::CLOSED_MANUAL;
|
||||
break;
|
||||
}
|
||||
foreach ($conditions as $cond) {
|
||||
$query->whereRaw($cond);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* days_bucket:14|21|28|30 精确命中档位;other = 未命中档位且有开药天数。
|
||||
*
|
||||
* @param mixed $query
|
||||
*/
|
||||
private function applyBucketFilter($query, string $bucket): void
|
||||
{
|
||||
if ($bucket === '') {
|
||||
return;
|
||||
}
|
||||
if ($bucket === 'other') {
|
||||
$query->where('rd.rule_days', 0)->where('rd.medication_days', '>', 0);
|
||||
} elseif (ctype_digit($bucket) && (int) $bucket > 0) {
|
||||
$query->where('rd.rule_days', (int) $bucket);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行格式化:事实列 + deriveState 推导 + 医助/部门名 + 导出列。
|
||||
*
|
||||
* @param array<string, mixed> $row
|
||||
* @param array<string, mixed> $cfg
|
||||
* @param array<int, string> $nameMap
|
||||
* @param array<int, string> $deptMap
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatRow(array $row, string $today, array $cfg, array $nameMap, array $deptMap): array
|
||||
{
|
||||
$st = RevisitDueCalculator::deriveState($row, $today, $cfg);
|
||||
$aid = (int) ($row['assistant_id'] ?? 0);
|
||||
$anchorTs = (int) ($row['anchor_ts'] ?? 0);
|
||||
$closed = (int) ($row['closed_state'] ?? 0);
|
||||
$slot = (int) ($row['current_slot'] ?? 0);
|
||||
$reason = (string) ($row['handle_reason'] ?? '');
|
||||
$remark = (string) ($row['handle_remark'] ?? '');
|
||||
$reasonText = RevisitDueLogic::CLOSE_REASON_TEXT[$reason] ?? $reason;
|
||||
$name = trim((string) ($row['patient_name'] ?? ''));
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'diagnosis_id' => (int) ($row['diagnosis_id'] ?? 0),
|
||||
'patient_name' => $name !== '' ? $name : '患者',
|
||||
'assistant_id' => $aid,
|
||||
'assistant_name' => (string) ($nameMap[$aid] ?? ''),
|
||||
'dept_name' => (string) ($deptMap[$aid] ?? ''),
|
||||
'current_slot' => $slot,
|
||||
'next_slot' => (int) ($row['next_slot'] ?? 0),
|
||||
'medication_days' => (int) ($row['medication_days'] ?? 0),
|
||||
'rule_days' => (int) ($row['rule_days'] ?? 0),
|
||||
'anchor_date' => $anchorTs > 0 ? date('Y-m-d', $anchorTs) : '',
|
||||
'due_date' => self::dateStr($row['due_date'] ?? null),
|
||||
'overdue_days' => $st['overdue_days'],
|
||||
'days_left' => $st['days_left'],
|
||||
'state' => $st['state'],
|
||||
'state_text' => $st['state_text'],
|
||||
'severe' => (int) $st['severe'],
|
||||
'appointment_state' => (int) ($row['appointment_state'] ?? 0),
|
||||
'appointment_date' => self::dateStr($row['appointment_date'] ?? null),
|
||||
'snooze_count' => (int) ($row['snooze_count'] ?? 0),
|
||||
'escalation_level' => (int) ($row['escalation_level'] ?? 0),
|
||||
'contact_count' => (int) ($row['contact_count'] ?? 0),
|
||||
'promised_date' => self::dateStr($row['promised_date'] ?? null),
|
||||
'missed_count' => (int) ($row['missed_count'] ?? 0),
|
||||
'closed_state' => $closed,
|
||||
'closed_state_text' => DiagnosisRevisitDue::CLOSED_STATE_TEXT_MAP[$closed] ?? '未知',
|
||||
'handle_reason' => $reason,
|
||||
'handle_remark' => $remark,
|
||||
'first_shown_at_text' => self::tsText($row['first_shown_at'] ?? 0),
|
||||
'last_snoozed_at_text' => self::tsText($row['last_snoozed_at'] ?? 0),
|
||||
'computed_at_text' => self::tsText($row['computed_at'] ?? 0),
|
||||
// 导出列
|
||||
'export_slot' => $slot > 0 ? '第' . $slot . '诊' : '',
|
||||
'export_overdue_days' => $st['overdue_days'] === null ? '' : (string) $st['overdue_days'],
|
||||
'export_close_reason' => $closed === RevisitDueCalculator::CLOSED_MANUAL
|
||||
? trim($reasonText . ($remark !== '' ? ':' . $remark : ''))
|
||||
: '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function fetchAssistantNames(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_filter($ids, static fn (int $v): bool => $v > 0));
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
$map = [];
|
||||
foreach (Admin::whereIn('id', $ids)->column('name', 'id') as $id => $n) {
|
||||
$map[(int) $id] = (string) $n;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助部门名:admin_dept 取最小 dept_id,再查 dept.name。
|
||||
*
|
||||
* @param list<int> $ids
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function fetchAssistantDeptNames(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_filter($ids, static fn (int $v): bool => $v > 0));
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
$minDept = [];
|
||||
$adRows = AdminDept::whereIn('admin_id', $ids)->field(['admin_id', 'dept_id'])->select()->toArray();
|
||||
foreach ($adRows as $ad) {
|
||||
$aid = (int) ($ad['admin_id'] ?? 0);
|
||||
$did = (int) ($ad['dept_id'] ?? 0);
|
||||
if ($aid <= 0 || $did <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($minDept[$aid]) || $did < $minDept[$aid]) {
|
||||
$minDept[$aid] = $did;
|
||||
}
|
||||
}
|
||||
if ($minDept === []) {
|
||||
return [];
|
||||
}
|
||||
$deptNames = [];
|
||||
foreach (Dept::whereIn('id', array_values(array_unique($minDept)))->column('name', 'id') as $id => $n) {
|
||||
$deptNames[(int) $id] = (string) $n;
|
||||
}
|
||||
$out = [];
|
||||
foreach ($minDept as $aid => $did) {
|
||||
$out[$aid] = (string) ($deptNames[$did] ?? '');
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 时间戳 → 'Y-m-d H:i' 或 '' */
|
||||
private static function tsText(mixed $ts): string
|
||||
{
|
||||
$ts = (int) $ts;
|
||||
|
||||
return $ts > 0 ? date('Y-m-d H:i', $ts) : '';
|
||||
}
|
||||
|
||||
/** 日期列 → 'Y-m-d' 或 ''(null / 空 / 0000-00-00 均视为空) */
|
||||
private static function dateStr(mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
$s = substr(trim((string) $value), 0, 10);
|
||||
|
||||
return ($s === '' || $s === '0000-00-00') ? '' : $s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user