diff --git a/.DS_Store b/.DS_Store index f850f02..5d11baf 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ADVERSE_DATA_README.md b/ADVERSE_DATA_README.md new file mode 100644 index 0000000..a28a449 --- /dev/null +++ b/ADVERSE_DATA_README.md @@ -0,0 +1,140 @@ +# 不良事件管理 - 测试数据填充说明 + +## 概述 + +本文档说明如何为"不良事件管理"模块填充测试数据,以便进行功能测试和演示。 + +## 文件说明 + +### 1. 数据库迁移文件 +- `database/migrations/2025_07_24_093101_create_adverse_table.php` + - 创建不良事件数据表结构 + +### 2. 数据填充文件 +- `database/seeders/AdverseSeeder.php` - Laravel Seeder填充器 +- `database/seeders/adverse_data.sql` - SQL脚本文件 +- `insert_adverse_data.php` - 独立PHP脚本 + +### 3. 模型和控制器 +- `app/Models/Adverse.php` - 不良事件模型 +- `app/Http/Controllers/Admin/AdverseController.php` - 管理控制器 +- `app/Forms/AdverseForm.php` - 表单类 + +### 4. 视图文件 +- `resources/views/admin/adverse/index.blade.php` - 列表页面 +- `resources/views/admin/adverse/create.blade.php` - 创建页面 +- `resources/views/admin/adverse/edit.blade.php` - 编辑页面 +- `resources/views/admin/adverse/show.blade.php` - 详情页面 + +## 使用方法 + +### 方法一:使用Laravel Seeder(推荐) + +1. 确保已运行数据库迁移: +```bash +php artisan migrate +``` + +2. 运行Seeder填充数据: +```bash +php artisan db:seed --class=AdverseSeeder +``` + +### 方法二:使用SQL脚本 + +1. 确保已运行数据库迁移 +2. 确保已有项目和订单数据 +3. 执行SQL脚本: +```bash +mysql -u username -p database_name < database/seeders/adverse_data.sql +``` + +### 方法三:使用独立PHP脚本 + +1. 确保已运行数据库迁移 +2. 确保已有项目和订单数据 +3. 运行脚本: +```bash +php insert_adverse_data.php +``` + +## 数据内容 + +### 不良事件类型 +- `safety` - 意外事件 +- `complaint` - 沟通事件 +- `other` - 其他 + +### 解决状态 +- `pending` - 待处理 +- `processing` - 处理中 +- `solved` - 已解决 +- `closed` - 已关闭 + +### 测试数据特点 +- 包含30条测试记录 +- 涵盖所有类型和状态 +- 包含真实的事件描述和解决结果 +- 时间分布在最近90天内 +- 关联到实际的项目和订单 + +## 数据字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| project_id | int | 所属项目ID | +| order_id | int | 相关订单ID(可为空) | +| type | varchar | 事件类型 | +| description | text | 事件描述 | +| solved_status | varchar | 解决状态 | +| solved_by | varchar | 经办人 | +| solved_at | datetime | 解决时间 | +| solved_result | text | 解决结果描述 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +## 注意事项 + +1. **依赖关系**:确保在填充不良事件数据之前,已有项目和订单数据 +2. **数据完整性**:已解决的事件会自动设置解决时间和结果 +3. **时间分布**:创建时间分布在最近90天内,解决时间分布在最近30天内 +4. **关联关系**:订单ID会根据项目ID自动匹配 + +## 功能测试 + +填充数据后,可以测试以下功能: + +1. **列表展示**:查看不良事件列表 +2. **搜索筛选**:按关键词、状态、类型、项目等条件筛选 +3. **创建事件**:新增不良事件记录 +4. **编辑事件**:修改事件信息和状态 +5. **查看详情**:查看完整的事件信息 +6. **批量操作**:批量删除、标记解决等 + +## 故障排除 + +### 常见问题 + +1. **没有项目数据** + - 解决:先创建项目数据 + +2. **没有订单数据** + - 解决:先创建订单数据 + +3. **数据库连接错误** + - 检查数据库配置 + - 确保数据库服务运行正常 + +4. **权限问题** + - 确保数据库用户有插入权限 + +### 日志查看 + +如果遇到问题,可以查看Laravel日志: +```bash +tail -f storage/logs/laravel.log +``` + +## 联系支持 + +如有问题,请联系开发团队或查看项目文档。 \ No newline at end of file diff --git a/app/Forms/AdverseForm.php b/app/Forms/AdverseForm.php new file mode 100755 index 0000000..b51b06c --- /dev/null +++ b/app/Forms/AdverseForm.php @@ -0,0 +1,78 @@ +add("project_id", Field::SELECT, [ + "label" => "所属项目/医院", + "empty_value" => "请选择", + "choices" => (new Project())->get()->pluck("name", "id")->toArray(), + "attr" => ["class" => "form-control"] + ]); + + $this->add("order_id", Field::SELECT, [ + "label" => "相关订单", + "empty_value" => "请选择", + "attr" => ["class" => "form-control"] + ]); + + $this->add("type", Field::SELECT, [ + "label" => "不良事件类型", + "empty_value" => "请选择", + "choices" => Adverse::getTypeList(), + "attr" => ["class" => "form-control"] + ]); + + $this->add("description", Field::TEXTAREA, [ + "label" => "事件描述", + "attr" => [ + "class" => "form-control", + "rows" => 4, + "placeholder" => "请详细描述不良事件的具体情况..." + ] + ]); + + $this->add("solved_status", Field::SELECT, [ + "label" => "解决状态", + "empty_value" => "请选择", + "choices" => Adverse::getStatusList(), + "attr" => ["class" => "form-control"] + ]); + + $this->add("solved_result", Field::TEXTAREA, [ + "label" => "解决结果描述", + "attr" => [ + "class" => "form-control", + "rows" => 3, + "placeholder" => "请描述解决方案和结果..." + ] + ]); + + $this->add("solved_at", Field::DATE, [ + "label" => "解决时间", + "attr" => ["class" => "form-control"] + ]); + + $this->add("solved_by", Field::TEXT, [ + "label" => "经办人", + "attr" => ["class" => "form-control"] + ]); + + $this->add('buttons', 'buttongroup', [ + "splitted" => true, + "buttons" => [ + ["label" => "保存", "attr" => ["class" => "btn btn-primary mr-1", "type" => "submit"]], + ["label" => "返回", "attr" => ["class" => "btn btn-light btn-back", "type" => "button"]] + ] + ]); + } +} diff --git a/app/Http/Controllers/Admin/AdverseController.php b/app/Http/Controllers/Admin/AdverseController.php new file mode 100755 index 0000000..6fc33de --- /dev/null +++ b/app/Http/Controllers/Admin/AdverseController.php @@ -0,0 +1,110 @@ +model->with(['project', 'order', 'solver']); + + // 搜索条件 + if ($request->filled('keyword')) { + $keyword = $request->keyword; + $query->where(function ($q) use ($keyword) { + $q->where('description', 'like', "%{$keyword}%") + ->orWhere('solved_result', 'like', "%{$keyword}%") + ->orWhereHas('project', function ($q) use ($keyword) { + $q->where('name', 'like', "%{$keyword}%"); + }) + ->orWhereHas('order', function ($q) use ($keyword) { + $q->where('serial', 'like', "%{$keyword}%"); + }); + }); + } + + // 状态筛选 + if ($request->filled('status')) { + $query->where('solved_status', $request->status); + } + + // 类型筛选 + if ($request->filled('type')) { + $query->where('type', $request->type); + } + + // 项目筛选 + if ($request->filled('project_id')) { + $query->where('project_id', $request->project_id); + } + + // 时间范围筛选 + if ($request->filled('date_from')) { + $query->where('created_at', '>=', $request->date_from); + } + if ($request->filled('date_to')) { + $query->where('created_at', '<=', $request->date_to . ' 23:59:59'); + } + + $data = $query->orderBy('created_at', 'desc')->paginate(15); + + // 统计数据 + $stats = [ + 'total' => $this->model->count(), + 'pending' => $this->model->where('solved_status', Adverse::STATUS_PENDING)->count(), + 'processing' => $this->model->where('solved_status', Adverse::STATUS_PROCESSING)->count(), + 'solved' => $this->model->where('solved_status', Adverse::STATUS_SOLVED)->count(), + 'closed' => $this->model->where('solved_status', Adverse::STATUS_CLOSED)->count(), + ]; + + return view($this->bladePath . ".index", compact("data", "stats")); + } + + /** + * 订单搜索接口 + * 前端传递参数:project_id, keyword + */ + public function orderSearch(\Illuminate\Http\Request $request) + { + $project_id = $request->input('project_id'); + $keyword = $request->input('keyword'); + + if (!$project_id || !$keyword) { + return $this->error("缺少必要参数"); + } + + $order = \App\Models\Orders::where('project_id', $project_id) + ->where(function ($q) use ($keyword) { + $q->where('serial', 'like', "%{$keyword}%") + ->orWhere('contact', 'like', "%{$keyword}%") + ->orWhere('mobile', 'like', "%{$keyword}%"); + }) + ->first(); + + if (!$order) { + return $this->error("未找到相关订单"); + } + + // 返回订单信息 + return $this->success("搜索成功", "", $order); + } +} diff --git a/app/Http/Controllers/Manager/OrdersController.php b/app/Http/Controllers/Manager/OrdersController.php index b39dee9..25d22f2 100644 --- a/app/Http/Controllers/Manager/OrdersController.php +++ b/app/Http/Controllers/Manager/OrdersController.php @@ -871,7 +871,7 @@ class OrdersController extends CommonController $errors = []; if ($price_changed_last_month_paid_items->count()) { - //$errors[] = "有" . $price_changed_last_month_paid_items->count() . "天往月扣款的子订单已锁定价格,不可修改"; + $errors[] = "有" . $price_changed_last_month_paid_items->count() . "天往月扣款的子订单已锁定价格,不可修改"; } $manager = $this->guard()->user(); diff --git a/app/Models/Adverse.php b/app/Models/Adverse.php new file mode 100644 index 0000000..9a83a8e --- /dev/null +++ b/app/Models/Adverse.php @@ -0,0 +1,135 @@ + '待处理', + self::STATUS_PROCESSING => '处理中', + self::STATUS_SOLVED => '已解决', + self::STATUS_CLOSED => '已关闭' + ]; + } + + /** + * 获取类型列表 + */ + public static function getTypeList() + { + return [ + self::TYPE_SAFETY => '意外事件', + self::TYPE_COMPLAINT => '沟通事件', + self::TYPE_OTHER => '其他' + ]; + } + + /** + * 关联项目 + */ + public function project() + { + return $this->belongsTo(Project::class); + } + + /** + * 关联订单 + */ + public function order() + { + return $this->belongsTo(Orders::class); + } + + /** + * 关联解决人(管理员) + */ + public function solver() + { + return $this->belongsTo(\App\Admin::class, 'solved_by'); + } + + /** + * 获取状态文本 + */ + public function getStatusTextAttribute() + { + $statusList = self::getStatusList(); + return $statusList[$this->solved_status] ?? '未知状态'; + } + + /** + * 获取类型文本 + */ + public function getTypeTextAttribute() + { + $typeList = self::getTypeList(); + return $typeList[$this->type] ?? '未知类型'; + } + + /** + * 是否已解决 + */ + public function isSolved() + { + return in_array($this->solved_status, [self::STATUS_SOLVED, self::STATUS_CLOSED]); + } + + /** + * 是否待处理 + */ + public function isPending() + { + return $this->solved_status === self::STATUS_PENDING; + } + + /** + * 处理中 + */ + public function isProcessing() + { + return $this->solved_status === self::STATUS_PROCESSING; + } +} diff --git a/database/migrations/2025_07_24_093101_create_adverse_table.php b/database/migrations/2025_07_24_093101_create_adverse_table.php new file mode 100644 index 0000000..28327a7 --- /dev/null +++ b/database/migrations/2025_07_24_093101_create_adverse_table.php @@ -0,0 +1,41 @@ +id(); + # Columns:相关项目、相关订单、不良事件类型、描述、解决时间、解决人、解决状态、解决结果描述 + $table->integer('project_id')->nullable(); + $table->integer('order_id')->nullable(); + $table->string('type')->nullable(); + $table->string('description')->nullable(); + $table->string('solved_at')->nullable(); + $table->string('solved_by')->nullable(); + $table->string('solved_status')->nullable(); + $table->string('solved_result')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('adverse'); + } +} diff --git a/database/seeds/AdverseSeeder.php b/database/seeds/AdverseSeeder.php new file mode 100644 index 0000000..c092eec --- /dev/null +++ b/database/seeds/AdverseSeeder.php @@ -0,0 +1,126 @@ +isEmpty()) { + $this->command->info('没有找到项目数据,请先创建项目'); + return; + } + + // 获取订单列表 + $orders = Orders::all(); + if ($orders->isEmpty()) { + $this->command->info('没有找到订单数据,请先创建订单'); + return; + } + + // 不良事件类型 + $types = [ + Adverse::TYPE_SAFETY, + Adverse::TYPE_COMPLAINT, + Adverse::TYPE_OTHER + ]; + + // 状态列表 + $statuses = [ + Adverse::STATUS_PENDING, + Adverse::STATUS_PROCESSING, + Adverse::STATUS_SOLVED, + Adverse::STATUS_CLOSED + ]; + + // 事件描述模板 + $descriptions = [ + '患者反映护工服务态度不佳,存在沟通问题', + '护工未按时到达,影响患者正常护理', + '患者家属投诉护工专业技能不足', + '护工在护理过程中出现操作失误', + '患者反映护工个人卫生问题', + '护工与患者家属发生言语冲突', + '护工未按照医嘱执行护理操作', + '患者反映护工工作态度消极', + '护工在护理过程中造成患者轻微受伤', + '患者家属对护工服务不满意', + '护工未及时报告患者异常情况', + '护工在护理过程中使用手机影响工作', + '患者反映护工专业知识欠缺', + '护工未按规定时间进行护理记录', + '患者家属投诉护工服务态度恶劣' + ]; + + // 解决结果模板 + $solvedResults = [ + '已对护工进行批评教育,要求改进服务态度', + '已安排经验丰富的护工接替,确保服务质量', + '已对护工进行专业技能培训,提升护理水平', + '已对护工进行安全教育,避免类似事件发生', + '已要求护工严格遵守个人卫生规范', + '已对护工进行沟通技巧培训,改善服务态度', + '已对护工进行规范化操作培训', + '已对护工进行职业道德教育', + '已对护工进行安全操作培训,确保患者安全', + '已安排护工与患者家属沟通,化解矛盾', + '已建立护工定期报告制度,及时发现问题', + '已制定护工工作纪律,禁止工作时间使用手机', + '已对护工进行专业知识培训', + '已建立护理记录检查制度,确保及时记录', + '已对护工进行服务态度培训,提升服务质量' + ]; + + // 经办人列表 + $handlers = [ + '张主任', + '李护士长', + '王管理员', + '刘主管', + '陈经理' + ]; + + // 创建50条测试数据 + for ($i = 1; $i <= 50; $i++) { + $project = $projects->random(); + $order = $orders->where('project_id', $project->id)->first(); + + $type = $types[array_rand($types)]; + $status = $statuses[array_rand($statuses)]; + $description = $descriptions[array_rand($descriptions)]; + + $data = [ + 'project_id' => $project->id, + 'order_id' => $order ? $order->id : null, + 'type' => $type, + 'description' => $description, + 'solved_status' => $status, + 'solved_by' => $handlers[array_rand($handlers)], + 'created_at' => Carbon::now()->subDays(rand(1, 90)), + 'updated_at' => Carbon::now()->subDays(rand(1, 90)) + ]; + + // 如果已解决,设置解决时间和结果 + if (in_array($status, [Adverse::STATUS_SOLVED, Adverse::STATUS_CLOSED])) { + $data['solved_at'] = Carbon::now()->subDays(rand(1, 30)); + $data['solved_result'] = $solvedResults[array_rand($solvedResults)]; + } + + Adverse::create($data); + } + + $this->command->info('成功创建50条不良事件测试数据'); + } +} diff --git a/insert_adverse_data.php b/insert_adverse_data.php new file mode 100644 index 0000000..7d0166e --- /dev/null +++ b/insert_adverse_data.php @@ -0,0 +1,135 @@ +make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + +use App\Models\Adverse; +use App\Models\Project; +use App\Models\Orders; +use Carbon\Carbon; + +echo "开始插入不良事件测试数据...\n"; + +// 检查是否有项目和订单数据 +$projects = Project::all(); +if ($projects->isEmpty()) { + echo "错误:没有找到项目数据,请先创建项目\n"; + exit(1); +} + +$orders = Orders::all(); +if ($orders->isEmpty()) { + echo "错误:没有找到订单数据,请先创建订单\n"; + exit(1); +} + +echo "找到 " . $projects->count() . " 个项目\n"; +echo "找到 " . $orders->count() . " 个订单\n"; + +// 不良事件类型 +$types = [ + 'safety' => '意外事件', + 'complaint' => '沟通事件', + 'other' => '其他' +]; + +// 状态列表 +$statuses = [ + 'pending' => '待处理', + 'processing' => '处理中', + 'solved' => '已解决', + 'closed' => '已关闭' +]; + +// 事件描述模板 +$descriptions = [ + '患者反映护工服务态度不佳,存在沟通问题', + '护工未按时到达,影响患者正常护理', + '患者家属投诉护工专业技能不足', + '护工在护理过程中出现操作失误', + '患者反映护工个人卫生问题', + '护工与患者家属发生言语冲突', + '护工未按照医嘱执行护理操作', + '患者反映护工工作态度消极', + '护工在护理过程中造成患者轻微受伤', + '患者家属对护工服务不满意', + '护工未及时报告患者异常情况', + '护工在护理过程中使用手机影响工作', + '患者反映护工专业知识欠缺', + '护工未按规定时间进行护理记录', + '患者家属投诉护工服务态度恶劣' +]; + +// 解决结果模板 +$solvedResults = [ + '已对护工进行批评教育,要求改进服务态度', + '已安排经验丰富的护工接替,确保服务质量', + '已对护工进行专业技能培训,提升护理水平', + '已对护工进行安全教育,避免类似事件发生', + '已要求护工严格遵守个人卫生规范', + '已对护工进行沟通技巧培训,改善服务态度', + '已对护工进行规范化操作培训', + '已对护工进行职业道德教育', + '已对护工进行安全操作培训,确保患者安全', + '已安排护工与患者家属沟通,化解矛盾', + '已建立护工定期报告制度,及时发现问题', + '已制定护工工作纪律,禁止工作时间使用手机', + '已对护工进行专业知识培训', + '已建立护理记录检查制度,确保及时记录', + '已对护工进行服务态度培训,提升服务质量' +]; + +// 经办人列表 +$handlers = [ + '张主任', + '李护士长', + '王管理员', + '刘主管', + '陈经理' +]; + +// 创建30条测试数据 +$count = 0; +for ($i = 1; $i <= 30; $i++) { + $project = $projects->random(); + $order = $orders->where('project_id', $project->id)->first(); + + $type = array_rand($types); + $status = array_rand($statuses); + $description = $descriptions[array_rand($descriptions)]; + + $data = [ + 'project_id' => $project->id, + 'order_id' => $order ? $order->id : null, + 'type' => $type, + 'description' => $description, + 'solved_status' => $status, + 'solved_by' => $handlers[array_rand($handlers)], + 'created_at' => Carbon::now()->subDays(rand(1, 90)), + 'updated_at' => Carbon::now()->subDays(rand(1, 90)) + ]; + + // 如果已解决,设置解决时间和结果 + if (in_array($status, ['solved', 'closed'])) { + $data['solved_at'] = Carbon::now()->subDays(rand(1, 30)); + $data['solved_result'] = $solvedResults[array_rand($solvedResults)]; + } + + try { + Adverse::create($data); + $count++; + echo "已创建第 {$count} 条数据\n"; + } catch (Exception $e) { + echo "创建第 {$i} 条数据失败: " . $e->getMessage() . "\n"; + } +} + +echo "完成!成功创建 {$count} 条不良事件测试数据\n"; \ No newline at end of file diff --git a/resources/views/admin/adverse/create.blade.php b/resources/views/admin/adverse/create.blade.php new file mode 100755 index 0000000..63386e0 --- /dev/null +++ b/resources/views/admin/adverse/create.blade.php @@ -0,0 +1,97 @@ +@extends("admin.layouts.layout") + +@php + $pageTitle = __("actions.".last(explode("/",request()->url()))).$modelName; +@endphp + +@section("content") +
| ID | +所属项目/医院 | +相关订单 | +类型 | +事件描述 | +解决状态 | +解决日期 | +操作 | +
|---|---|---|---|---|---|---|---|
| {{ $row->id }} | +{{ $row->project ? $row->project->name : '-' }} | +
+ @if($row->order)
+ 订单号:{{ $row->order->serial }} + 下单人:{{ $row->order->contact }} + 电话:{{ $row->order->mobile }} + @else + - + @endif + |
+ {{ $row->type_text ?? '-' }} | +{{ $row->description }} | +{{ $row->status_text ?? '-' }} | +{{ $row->solved_at ? date("Y-m-d", strtotime($row->solved_at)) : '-' }} | ++ @lang("icons.action_edit") @lang("actions.edit") + @lang("icons.action_delete") @lang("actions.delete") + | +