Refactor patient follow-up system to focus on monthly reminders. Updated logic to filter and display only patients needing follow-up this month. Enhanced patient model methods for calculating follow-up dates and counts. Improved UI in reminders view with new layout and call button functionality.

This commit is contained in:
Ethanfly 2026-01-12 13:55:41 +08:00
parent ca4d1fa1a5
commit 0dd6529dcf
6 changed files with 149 additions and 230 deletions

View File

@ -11,11 +11,10 @@
- 🗑️ 删除患者记录
### 随访提醒
- ⏰ 智能计算随访日期出院后1周、1月、3月、6月
- 🔴 过期提醒 - 显示已过期未随访的患者
- 🟡 今日到期 - 当天需要随访的患者
- 🔵 即将到期 - 7天内需要随访的患者
- ✅ 标记随访完成,自动计算下次随访时间
- ⏰ 智能计算随访日期出院后1、3、6、12个月
- 📅 本月提醒 - 只显示本月需要随访的患者列表
- 📊 进度自动更新 - 过期月份自动算作已完成
- 📞 一键拨打电话进行回访
### 用户系统
- 👤 用户注册/登录

View File

@ -46,54 +46,33 @@ class PatientController extends Controller
}
/**
* 显示需要随访提醒的患者
* 显示需要随访提醒的患者(只显示当月需要随访的)
*/
public function reminders(Request $request)
{
$patients = $this->userPatients()->get();
// 筛选需要随访的患者
// 筛选当月需要随访的患者
$reminders = $patients->filter(function ($patient) {
return !$patient->isCompleted();
return $patient->needsFollowUpThisMonth();
})->map(function ($patient) {
$nextDate = $patient->getNextFollowUpDate();
return [
'patient' => $patient,
'next_follow_up_date' => $nextDate,
'next_follow_up_number' => $patient->getNextFollowUpNumber(),
'status' => $patient->getFollowUpStatus(),
'needs_attention' => $patient->needsFollowUp(),
'days_until' => $nextDate ? Carbon::today()->diffInDays($nextDate, false) : null,
'follow_up_date' => $patient->getCurrentMonthFollowUpDate(),
'follow_up_number' => $patient->getCurrentMonthFollowUpNumber(),
];
})->sortBy('days_until');
// 筛选类型
$filter = $request->input('filter', 'all');
if ($filter === 'overdue') {
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] < 0);
} elseif ($filter === 'today') {
$reminders = $reminders->filter(fn($r) => $r['days_until'] === 0);
} elseif ($filter === 'upcoming') {
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] > 0 && $r['days_until'] <= 7);
}
})->sortBy(function ($r) {
return $r['follow_up_date'];
});
// 统计数据
$stats = [
'total' => $this->userPatients()->count(),
'overdue' => $patients->filter(fn($p) => !$p->isCompleted() && $p->getNextFollowUpDate()?->lt(Carbon::today()))->count(),
'today' => $patients->filter(fn($p) => !$p->isCompleted() && $p->getNextFollowUpDate()?->isToday())->count(),
'upcoming' => $patients->filter(function ($p) {
if ($p->isCompleted()) return false;
$next = $p->getNextFollowUpDate();
if (!$next) return false;
$diff = Carbon::today()->diffInDays($next, false);
return $diff > 0 && $diff <= 7;
})->count(),
'count' => $reminders->count(),
'completed' => $patients->filter(fn($p) => $p->isCompleted())->count(),
'total' => $patients->count(),
];
return view('patients.reminders', compact('reminders', 'stats', 'filter'));
return view('patients.reminders', compact('reminders', 'stats'));
}
/**
@ -359,46 +338,34 @@ class PatientController extends Controller
}
/**
* 导出提醒列表 (使用 xlswriter 生成 Excel)
* 导出本月随访列表 (使用 xlswriter 生成 Excel)
*/
public function export(Request $request)
{
$filter = $request->input('filter', 'all');
$patients = $this->userPatients()->get();
// 筛选需要随访的患者
// 筛选本月需要随访的患者
$reminders = $patients->filter(function ($patient) {
return !$patient->isCompleted();
return $patient->needsFollowUpThisMonth();
})->map(function ($patient) {
$nextDate = $patient->getNextFollowUpDate();
return [
'patient' => $patient,
'next_follow_up_date' => $nextDate,
'next_follow_up_number' => $patient->getNextFollowUpNumber(),
'status' => $patient->getFollowUpStatus(),
'days_until' => $nextDate ? Carbon::today()->diffInDays($nextDate, false) : null,
'follow_up_date' => $patient->getCurrentMonthFollowUpDate(),
'follow_up_number' => $patient->getCurrentMonthFollowUpNumber(),
];
})->sortBy('days_until');
// 根据筛选条件过滤
if ($filter === 'overdue') {
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] < 0);
} elseif ($filter === 'today') {
$reminders = $reminders->filter(fn($r) => $r['days_until'] === 0);
} elseif ($filter === 'upcoming') {
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] > 0 && $r['days_until'] <= 7);
}
})->sortBy(function ($r) {
return $r['follow_up_date'];
});
// 使用 xlswriter 生成 Excel 文件
$config = ['path' => sys_get_temp_dir()];
$filename = '随访提醒_' . date('Y-m-d_His') . '.xlsx';
$filename = '本月随访_' . date('Y-m') . '.xlsx';
$excel = new \Vtiful\Kernel\Excel($config);
// 创建文件并设置表头
$fileHandle = $excel->fileName($filename)
->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '下次随访日期', '第几次随访', '状态', '户籍地址', '联系方式', '备注']);
->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '随访日期', '第几次随访', '户籍地址', '联系方式', '备注']);
// 设置表头样式
$headerStyle = $fileHandle->getHandle();
@ -415,9 +382,8 @@ class PatientController extends Controller
$p->age,
$p->diagnosis,
$p->discharge_date->format('Y-m-d'),
$reminder['next_follow_up_date']?->format('Y-m-d') ?? '',
'第' . $reminder['next_follow_up_number'] . '次',
$reminder['status'],
$reminder['follow_up_date']?->format('Y-m-d') ?? '',
'第' . $reminder['follow_up_number'] . '次',
$p->address ?? '',
$p->phone ?? '',
$p->remark ?? '',

View File

@ -53,74 +53,82 @@ class Patient extends Model
}
/**
* 获取下次随访日期
* 自动计算已完成的随访次数(过期的月份自动算已完成)
*/
public function getNextFollowUpDate(): ?Carbon
public function getAutoFollowUpCount(): int
{
$schedule = $this->getFollowUpSchedule();
$nextIndex = $this->follow_up_count;
$currentMonth = Carbon::now()->startOfMonth();
$completed = 0;
if ($nextIndex >= count($schedule)) {
return null; // 已完成所有随访
}
$months = $schedule[$nextIndex];
return $this->discharge_date->copy()->addMonths($months);
}
/**
* 获取下次随访是第几次
*/
public function getNextFollowUpNumber(): int
{
return $this->follow_up_count + 1;
}
/**
* 检查是否需要随访(到期或已过期)
*/
public function needsFollowUp(): bool
{
$nextDate = $this->getNextFollowUpDate();
if (!$nextDate) {
return false;
}
return $nextDate->lte(Carbon::today());
}
/**
* 获取随访状态
*/
public function getFollowUpStatus(): string
{
$nextDate = $this->getNextFollowUpDate();
if (!$nextDate) {
return '已完成';
}
$today = Carbon::today();
$diff = $today->diffInDays($nextDate, false);
if ($diff < 0) {
return '已过期 ' . abs($diff) . ' 天';
} elseif ($diff == 0) {
return '今日到期';
} elseif ($diff <= 7) {
return '即将到期(' . $diff . '天后)';
foreach ($schedule as $months) {
$followUpDate = $this->discharge_date->copy()->addMonths($months);
// 如果随访日期的月份早于当前月份,则算作已完成
if ($followUpDate->startOfMonth()->lt($currentMonth)) {
$completed++;
} else {
return '未到期';
break;
}
}
return $completed;
}
/**
* 获取当月需要随访的日期(如果有的话)
*/
public function getCurrentMonthFollowUpDate(): ?Carbon
{
$schedule = $this->getFollowUpSchedule();
$currentMonth = Carbon::now();
$monthStart = $currentMonth->copy()->startOfMonth();
$monthEnd = $currentMonth->copy()->endOfMonth();
foreach ($schedule as $index => $months) {
$followUpDate = $this->discharge_date->copy()->addMonths($months);
// 如果随访日期在当月内
if ($followUpDate->gte($monthStart) && $followUpDate->lte($monthEnd)) {
return $followUpDate;
}
}
return null;
}
/**
* 获取当月随访是第几次
*/
public function getCurrentMonthFollowUpNumber(): ?int
{
$schedule = $this->getFollowUpSchedule();
$currentMonth = Carbon::now();
$monthStart = $currentMonth->copy()->startOfMonth();
$monthEnd = $currentMonth->copy()->endOfMonth();
foreach ($schedule as $index => $months) {
$followUpDate = $this->discharge_date->copy()->addMonths($months);
if ($followUpDate->gte($monthStart) && $followUpDate->lte($monthEnd)) {
return $index + 1;
}
}
return null;
}
/**
* 检查当月是否需要随访
*/
public function needsFollowUpThisMonth(): bool
{
return $this->getCurrentMonthFollowUpDate() !== null;
}
/**
* 检查是否已完成所有随访
*/
public function isCompleted(): bool
{
return $this->follow_up_count >= count($this->getFollowUpSchedule());
return $this->getAutoFollowUpCount() >= count($this->getFollowUpSchedule());
}
/**

View File

@ -1113,7 +1113,31 @@
.reminder-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-light);
}
.call-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px 16px;
background: linear-gradient(135deg, var(--color-success), #34d399);
color: white;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
}
.call-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.action-form {
@ -1778,12 +1802,12 @@
</svg>
<span>随访提醒</span>
@php
$overdueCount = \App\Models\Patient::where('user_id', Auth::id())->get()->filter(function($p) {
return !$p->isCompleted() && $p->getNextFollowUpDate()?->lt(\Carbon\Carbon::today());
$monthCount = \App\Models\Patient::where('user_id', Auth::id())->get()->filter(function($p) {
return $p->needsFollowUpThisMonth();
})->count();
@endphp
@if($overdueCount > 0)
<span class="nav-badge">{{ $overdueCount }}</span>
@if($monthCount > 0)
<span class="nav-badge">{{ $monthCount }}</span>
@endif
</a>

View File

@ -600,7 +600,6 @@
<th>诊断</th>
<th>转诊日期</th>
<th>随访进度</th>
<th>状态</th>
<th>联系方式</th>
<th>备注</th>
<th style="text-align: right;">操作</th>
@ -610,8 +609,7 @@
@foreach($patients as $patient)
@php
$total = count($patient->getFollowUpSchedule());
$completed = $patient->follow_up_count;
$status = $patient->getFollowUpStatus();
$completed = $patient->getAutoFollowUpCount();
@endphp
<tr>
<td>
@ -637,17 +635,6 @@
<span class="progress-text">{{ $completed }}/{{ $total }}</span>
</div>
</td>
<td>
@if($patient->isCompleted())
<span class="badge badge-success">已完成</span>
@elseif(str_contains($status, '过期'))
<span class="badge badge-danger">{{ $status }}</span>
@elseif(str_contains($status, '今日'))
<span class="badge badge-warning">{{ $status }}</span>
@else
<span class="badge badge-info">{{ $status }}</span>
@endif
</td>
<td>
@if($patient->phone)
<a href="tel:{{ $patient->phone }}" class="phone-link">{{ $patient->phone }}</a>
@ -671,16 +658,6 @@
</svg>
</a>
@endif
@if(!$patient->isCompleted())
<form action="{{ route('patients.follow-up', $patient) }}" method="POST" style="display: inline;">
@csrf
<button type="submit" class="btn-icon success" title="标记已随访" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $patient->getNextFollowUpNumber() }}次随访?')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
</form>
@endif
<form action="{{ route('patients.destroy', $patient) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
@ -705,8 +682,7 @@
@foreach($patients as $patient)
@php
$total = count($patient->getFollowUpSchedule());
$completed = $patient->follow_up_count;
$status = $patient->getFollowUpStatus();
$completed = $patient->getAutoFollowUpCount();
@endphp
<div class="mobile-patient-card">
<div class="mobile-patient-header">
@ -719,15 +695,7 @@
<div class="mobile-patient-meta">{{ $patient->gender }} · {{ $patient->age }} · {{ $patient->getDiagnosisType() }}</div>
</div>
</div>
@if($patient->isCompleted())
<span class="badge badge-success">已完成</span>
@elseif(str_contains($status, '过期'))
<span class="badge badge-danger">{{ $status }}</span>
@elseif(str_contains($status, '今日'))
<span class="badge badge-warning">{{ $status }}</span>
@else
<span class="badge badge-info">{{ $status }}</span>
@endif
<span class="badge {{ $completed >= $total ? 'badge-success' : 'badge-info' }}">{{ $completed }}/{{ $total }}</span>
</div>
<div class="mobile-patient-body">
@ -780,19 +748,6 @@
<div></div>
@endif
@if(!$patient->isCompleted())
<form action="{{ route('patients.follow-up', $patient) }}" method="POST">
@csrf
<button type="submit" class="btn btn-primary" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $patient->getNextFollowUpNumber() }}次随访?')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<polyline points="20 6 9 17 4 12"/>
</svg>
已随访
</button>
</form>
@else
<div></div>
@endif
<form action="{{ route('patients.destroy', $patient) }}" method="POST">
@csrf

View File

@ -4,35 +4,27 @@
@section('content')
<div class="page-header">
<h1 class="page-title">随访提醒</h1>
<p class="page-subtitle">查看需要随访的患者,及时完成回访工作</p>
<h1 class="page-title">本月随访</h1>
<p class="page-subtitle">{{ now()->format('Y年n月') }} 需要随访的患者</p>
</div>
<!-- 统计筛选-->
<!-- 统计-->
<div class="stats-filter-bar">
<div class="stats-filter-grid">
<a href="{{ route('patients.reminders', ['filter' => 'all']) }}" class="stats-filter-item {{ $filter === 'all' ? 'active' : '' }}">
<span class="sf-value">{{ $stats['total'] }}</span>
<span class="sf-label">全部</span>
</a>
<a href="{{ route('patients.reminders', ['filter' => 'overdue']) }}" class="stats-filter-item danger {{ $filter === 'overdue' ? 'active' : '' }}">
<span class="sf-value">{{ $stats['overdue'] }}</span>
<span class="sf-label">过期</span>
</a>
<a href="{{ route('patients.reminders', ['filter' => 'today']) }}" class="stats-filter-item warning {{ $filter === 'today' ? 'active' : '' }}">
<span class="sf-value">{{ $stats['today'] }}</span>
<span class="sf-label">今日</span>
</a>
<a href="{{ route('patients.reminders', ['filter' => 'upcoming']) }}" class="stats-filter-item info {{ $filter === 'upcoming' ? 'active' : '' }}">
<span class="sf-value">{{ $stats['upcoming'] }}</span>
<span class="sf-label">7天内</span>
</a>
<a href="{{ route('patients.reminders', ['filter' => 'completed']) }}" class="stats-filter-item success {{ $filter === 'completed' ? 'active' : '' }}">
<span class="sf-value">{{ $stats['completed'] }}</span>
<span class="sf-label">完成</span>
</a>
<div class="stats-filter-item active">
<span class="sf-value">{{ $stats['count'] }}</span>
<span class="sf-label">本月待随访</span>
</div>
<a href="{{ route('patients.export', ['filter' => $filter]) }}" class="export-icon-btn" title="导出">
<div class="stats-filter-item success">
<span class="sf-value">{{ $stats['completed'] }}</span>
<span class="sf-label">已完成</span>
</div>
<div class="stats-filter-item">
<span class="sf-value">{{ $stats['total'] }}</span>
<span class="sf-label">总患者</span>
</div>
</div>
<a href="{{ route('patients.export') }}" class="export-icon-btn" title="导出">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
@ -50,60 +42,59 @@
<line x1="9" y1="9" x2="9.01" y2="9"/>
<line x1="15" y1="9" x2="15.01" y2="9"/>
</svg>
<div class="empty-state-title">暂无需要随访的患者</div>
<p>当前筛选条件下没有需要随访的患者,您可以切换筛选条件或导入新的患者数据。</p>
<div style="margin-top: 20px;">
<a href="{{ route('patients.import') }}" class="btn btn-primary">导入患者数据</a>
</div>
<div class="empty-state-title">本月暂无需要随访的患者</div>
<p>太棒了!本月没有需要随访的患者</p>
</div>
</div>
@else
<!-- 提醒卡片列表 - 两列布局 -->
<!-- 提醒卡片列表 -->
<div class="reminder-grid">
@foreach($reminders as $reminder)
@php
$patient = $reminder['patient'];
$nextDate = $reminder['next_follow_up_date'];
$daysUntil = $reminder['days_until'];
$followUpDate = $reminder['follow_up_date'];
$followUpNumber = $reminder['follow_up_number'];
@endphp
<div class="reminder-card fade-in">
{{-- 卡片头部 --}}
<div class="reminder-card-header">
<div class="reminder-patient-info">
<div class="reminder-avatar">{{ mb_substr($patient->name, 0, 1) }}</div>
<div class="reminder-avatar {{ $patient->gender == '女' ? 'female' : 'male' }}">{{ mb_substr($patient->name, 0, 1) }}</div>
<div>
<div class="reminder-name">{{ $patient->name }}</div>
<div class="reminder-meta">{{ $patient->gender }} · {{ $patient->age }}</div>
</div>
</div>
@if($daysUntil !== null && $daysUntil < 0)
<span class="badge badge-danger">过期 {{ abs($daysUntil) }} </span>
@elseif($daysUntil === 0)
<span class="badge badge-warning">今日到期</span>
@elseif($daysUntil !== null && $daysUntil <= 7)
<span class="badge badge-info">{{ $daysUntil }} 天后</span>
@else
<span class="badge badge-success">未到期</span>
@endif
<span class="badge badge-info">{{ $followUpNumber }}</span>
</div>
{{-- 诊断标签 --}}
<div class="reminder-diagnosis">
<span class="diagnosis-tag">{{ $patient->getDiagnosisType() }}</span>
<span class="follow-up-tag">{{ $reminder['next_follow_up_number'] }}次随访</span>
</div>
{{-- 关键信息 --}}
<div class="reminder-details">
<div class="detail-row">
<span class="detail-icon">🏥</span>
<span class="detail-label">转诊日期</span>
<span class="detail-value">{{ $patient->discharge_date->format('Y-m-d') }}</span>
</div>
<div class="detail-row">
<span class="detail-icon">📅</span>
<span class="detail-label">随访日期</span>
<span class="detail-value">{{ $nextDate ? $nextDate->format('Y-m-d') : '-' }}</span>
<span class="detail-value">{{ $followUpDate->format('Y-m-d') }}</span>
</div>
<div class="detail-row">
<span class="detail-icon">📞</span>
<span class="detail-label">联系方式</span>
<span class="detail-value">{{ $patient->phone ?: '-' }}</span>
<span class="detail-value">
@if($patient->phone)
<a href="tel:{{ $patient->phone }}" class="phone-link">{{ $patient->phone }}</a>
@else
-
@endif
</span>
</div>
<div class="detail-row">
<span class="detail-icon">📍</span>
@ -111,7 +102,7 @@
<span class="detail-value text-truncate">{{ $patient->address ?: '-' }}</span>
</div>
@if($patient->remark)
<div class="detail-row">
<div class="detail-row remark-row">
<span class="detail-icon">📝</span>
<span class="detail-label">备注</span>
<span class="detail-value remark-text">{{ $patient->remark }}</span>
@ -119,41 +110,17 @@
@endif
</div>
{{-- 进度条 --}}
<div class="reminder-progress">
@php
$total = count($patient->getFollowUpSchedule());
$completed = $patient->follow_up_count;
$percent = ($completed / $total) * 100;
@endphp
<div class="progress-header">
<span>随访进度</span>
<span>{{ $completed }}/{{ $total }}</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: {{ $percent }}%"></div>
</div>
</div>
{{-- 操作按钮 --}}
<div class="reminder-actions">
{{-- 拨打电话按钮 --}}
@if($patient->phone)
<a href="tel:{{ $patient->phone }}" class="action-btn action-call" title="拨打电话">
<div class="reminder-actions">
<a href="tel:{{ $patient->phone }}" class="call-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
拨打电话
</a>
@endif
<form action="{{ route('patients.follow-up', $patient) }}" method="POST" class="action-form">
@csrf
<button type="submit" class="action-btn action-complete" title="标记已随访" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $reminder['next_follow_up_number'] }}次随访?')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span>已随访</span>
</button>
</form>
</div>
@endif
</div>
@endforeach
</div>