From a5209f45c850c7a73b83cb15c59c762cb5d43446 Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Thu, 25 Dec 2025 17:30:50 +0900 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=87=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/ArchiveHistoryController.php | 99 +- app/Http/Controllers/IndexController.php | 83 +- .../Controllers/MedicalHistoryController.php | 20 +- app/Http/Resources/ArchiveHistoryResource.php | 2 +- app/Http/Resources/ArchiveInfoResource.php | 7 +- .../Mis/SttMedicalHistoryResource.php | 119 +- app/Http/Resources/PatientInfoResource.php | 8 +- app/Models/ArchiveHistory.php | 21 +- app/Models/ArchiveInfo.php | 25 +- app/Models/Mis/SttMedicalHistory.php | 10 +- app/Models/SI/SttMedicalHistory.php | 27 +- app/Repositories/MedicalHistoryRepository.php | 1120 +++++++++++++---- app/Services/ArchiveSearchService.php | 146 +++ composer.json | 1 + composer.lock | 161 ++- config/app.php | 2 +- ..._112144_create_archive_histories_table.php | 4 +- ...2_05_030609_create_archive_infos_table.php | 7 +- database/seeders/OrgSeeder.php | 21 + .../js/Composables/useMedicalHistoryFilter.js | 70 +- resources/js/Layouts/AppLayout.vue | 24 +- .../Pages/Home/ArchiveHistoryModal/Index.vue | 35 +- .../Home/ArchiveHistoryMoveModal/Index.vue | 15 +- resources/js/Pages/Home/DataTable/Index.vue | 55 +- resources/js/Pages/Home/Index.vue | 13 +- 25 files changed, 1521 insertions(+), 574 deletions(-) create mode 100644 app/Services/ArchiveSearchService.php create mode 100644 database/seeders/OrgSeeder.php diff --git a/app/Http/Controllers/ArchiveHistoryController.php b/app/Http/Controllers/ArchiveHistoryController.php index 99b173a..543e2b9 100644 --- a/app/Http/Controllers/ArchiveHistoryController.php +++ b/app/Http/Controllers/ArchiveHistoryController.php @@ -6,6 +6,7 @@ use App\Http\Resources\ArchiveHistoryResource; use App\Http\Resources\ArchiveInfoResource; use App\Models\ArchiveHistory; use App\Models\ArchiveInfo; +use App\Models\SI\SttMedicalHistory; use App\Rules\DateTimeOrStringOrNumber; use Illuminate\Http\Request; use Illuminate\Support\Carbon; @@ -21,7 +22,10 @@ class ArchiveHistoryController extends Controller { $archiveHistory = ArchiveHistory::find($id); - return response()->json($archiveHistory); + return response()->json([ + ...$archiveHistory->toArray(), + 'type' => $archiveHistory->historyType() + ]); } public function moveStore(Request $request) @@ -34,36 +38,46 @@ class ArchiveHistoryController extends Controller 'employee_post' => 'nullable|string', 'comment' => 'nullable|string', 'has_lost' => 'boolean', - 'historyable_id' => 'nullable|numeric', - 'historyable_type' => 'required|string', + 'archive_info_id' => 'required|numeric', + 'type' => 'required|string', ]); // Преобразуем timestamp в дату, если пришли числа if (isset($data['issue_at']) && is_numeric($data['issue_at'])) { - $data['issue_at'] = Carbon::createFromTimestampMs($data['issue_at'])->format('Y-m-d'); + $data['issue_at'] = Carbon::createFromTimestampMs($data['issue_at']) + ->setTimezone(config('app.timezone')) + ->format('Y-m-d'); } if (isset($data['return_at']) && is_numeric($data['return_at'])) { - $data['return_at'] = Carbon::createFromTimestampMs($data['return_at'])->format('Y-m-d'); + $data['return_at'] = Carbon::createFromTimestampMs($data['return_at']) + ->setTimezone(config('app.timezone')) + ->format('Y-m-d'); } - $archiveHistory = ArchiveHistory::create($data); - - // Если переданы данные для полиморфной связи - if ($request->filled('historyable_id') && $request->filled('historyable_type')) { - // Найти связанную модель - $historyableClass = $request->input('historyable_type'); - - // Проверяем, существует ли класс модели - if (class_exists($historyableClass)) { - $historyableModel = $historyableClass::find($request->input('historyable_id')); - - if ($historyableModel) { - // Связываем модели - $archiveHistory->historyable()->associate($historyableModel); - $archiveHistory->save(); - } - } + $archiveInfo = ArchiveInfo::whereId($data['archive_info_id'])->first(); + if ($data['type'] === 'mis') { + $archiveHistory = $archiveInfo->misHistory->archiveHistory()->create([ + 'issue_at' => $data['issue_at'], + 'return_at' => $data['return_at'], + 'org_id' => $data['org_id'], + 'employee_name' => $data['employee_name'], + 'employee_post' => $data['employee_post'], + 'comment' => $data['comment'], + 'has_lost' => $data['has_lost'], + 'mis_history_id' => $archiveInfo->mis_history_id + ]); + } else { + $archiveHistory = $archiveInfo->foxproHistory->archiveHistory()->create([ + 'issue_at' => $data['issue_at'], + 'return_at' => $data['return_at'], + 'org_id' => $data['org_id'], + 'employee_name' => $data['employee_name'], + 'employee_post' => $data['employee_post'], + 'comment' => $data['comment'], + 'has_lost' => $data['has_lost'], + 'foxpro_history_id' => $archiveInfo->foxpro_history_id, + ]); } return response()->json(ArchiveHistoryResource::make($archiveHistory)); @@ -79,17 +93,20 @@ class ArchiveHistoryController extends Controller 'employee_post' => 'nullable|string', 'comment' => 'nullable|string', 'has_lost' => 'boolean', - 'historyable_id' => 'nullable|numeric', - 'historyable_type' => 'required|string', + 'type' => 'required|string', ]); // Преобразуем timestamp в дату, если пришли числа if (isset($data['issue_at']) && is_numeric($data['issue_at'])) { - $data['issue_at'] = Carbon::createFromTimestampMs($data['issue_at'])->format('Y-m-d'); + $data['issue_at'] = Carbon::createFromTimestampMs($data['issue_at']) + ->setTimezone(config('app.timezone')) + ->format('Y-m-d'); } if (isset($data['return_at']) && is_numeric($data['return_at'])) { - $data['return_at'] = Carbon::createFromTimestampMs($data['return_at'])->format('Y-m-d'); + $data['return_at'] = Carbon::createFromTimestampMs($data['return_at']) + ->setTimezone(config('app.timezone')) + ->format('Y-m-d'); } $archiveHistory = ArchiveHistory::find($id); @@ -105,7 +122,8 @@ class ArchiveHistoryController extends Controller 'id' => 'required|numeric', 'num' => 'nullable|string', 'post_in' => ['nullable', new DateTimeOrStringOrNumber], - 'historyable_type' => 'required|string', + 'status' => 'nullable', + 'type' => 'nullable' ]); // Преобразуем timestamp в дату, если пришли числа @@ -113,25 +131,16 @@ class ArchiveHistoryController extends Controller $data['post_in'] = Carbon::createFromTimestampMs($data['post_in'])->format('Y-m-d'); } - if ($patientId && $request->filled('historyable_type')) { - // Найти связанную модель - $historyableClass = $request->input('historyable_type'); + $archiveInfo = ArchiveInfo::whereId($patientId)->first(); - // Проверяем, существует ли класс модели - if (class_exists($historyableClass)) { - $historyableModel = $historyableClass::find($patientId); + $archiveInfo->updateOrCreate( + ['id' => $patientId], + [ + 'archive_num' => $data['num'], + 'post_in' => $data['post_in'], + ] + ); - if ($historyableModel) { - // Связываем модели - $historyableModel->archiveInfo()->updateOrCreate([ - 'historyable_type' => $historyableClass, - 'historyable_id' => $patientId, - ], $data); - return response()->json(ArchiveInfoResource::make($historyableModel->archiveInfo)); - } - } - } - - return response()->json()->setStatusCode(500); + return response()->json()->setStatusCode(200); } } diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index bdcd486..6346769 100644 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -25,58 +25,15 @@ class IndexController extends Controller $searchText = $request->get('search', null); $dateExtractFrom = $request->get('date_extract_from', null); $dateExtractTo = $request->get('date_extract_to', null); - $database = $request->get('database', 'separate'); // si, mis $status = $request->get('status', null); - $data = []; - $databaseStats = $this->repository->getDatabaseStats(); - - switch ($database) { - case 'si': - $paginator = $this->repository->searchInPostgres( - $searchText, - $dateExtractFrom, - $dateExtractTo, - $pageSize - ); - $data['si'] = SiSttMedicalHistoryResource::collection($paginator); - break; - - case 'mis': - $paginator = $this->repository->searchInMssql( - $searchText, - $dateExtractFrom, - $dateExtractTo, - $pageSize - ); - $data['mis'] = MisSttMedicalHistoryResource::collection($paginator); - break; - - case 'smart': - $paginator = $this->repository->smartSearch( - $searchText, - $dateExtractFrom, - $dateExtractTo, - $pageSize - ); - $data['smart'] = SiSttMedicalHistoryResource::collection($paginator); - break; - - case 'separate': - $separateResults = $this->repository->separateSearch( - $searchText, - $dateExtractFrom, - $dateExtractTo, - $status, - $pageSize - ); - $data = [ - 'si' => SiSttMedicalHistoryResource::collection($separateResults['si']), - 'mis' => MisSttMedicalHistoryResource::collection($separateResults['mis']), - 'stats' => $separateResults['stats'], - ]; - break; - } + $data = $this->repository->unifiedSearch( + $searchText, + $dateExtractFrom, + $dateExtractTo, + $status, + $pageSize + ); $statuses = ArchiveStatus::all()->map(function ($status) { return [ @@ -91,35 +48,13 @@ class IndexController extends Controller ]); return Inertia::render('Home/Index', [ - 'cards' => $data, + 'cards' => MisSttMedicalHistoryResource::collection($data), 'statuses' => $statuses, - 'databaseStats' => $databaseStats, 'filters' => array_merge($request->only([ 'search', 'date_extract_from', 'date_extract_to', - 'page_size', 'page', 'view_type', 'database', 'status' + 'page_size', 'page', 'status' ])) ]); -// $cardsQuery = SttMedicalHistory::query(); -// -// if (!empty($searchText)) { -// $cardsQuery = $cardsQuery->search($searchText); -// } -// -// if (!empty($dateExtractFrom)) { -// $cardsQuery = $cardsQuery->whereDate('dateextract', '>=', $dateExtractFrom); -// if (!empty($dateExtractTo)) { -// $cardsQuery = $cardsQuery->whereDate('dateextract', '<=', $dateExtractTo); -// } -// } -// -// $cards = SttMedicalHistoryResource::collection($cardsQuery->paginate($pageSize)); -// -// return Inertia::render('Home/Index', [ -// 'cards' => $cards, -// 'filters' => $request->only([ -// 'search', 'date_extract_from', 'date_extract_to', 'page_size', 'page', 'view_type' -// ]), -// ]); } } diff --git a/app/Http/Controllers/MedicalHistoryController.php b/app/Http/Controllers/MedicalHistoryController.php index a644cfc..ae4f6f7 100644 --- a/app/Http/Controllers/MedicalHistoryController.php +++ b/app/Http/Controllers/MedicalHistoryController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Http\Resources\ArchiveHistoryResource; use App\Http\Resources\ArchiveInfoResource; use App\Http\Resources\PatientInfoResource; +use App\Models\ArchiveInfo; use App\Models\SI\SttMedicalHistory as SiSttMedicalHistory; use App\Models\Mis\SttMedicalHistory as MisSttMedicalHistory; use App\Repositories\MedicalHistoryRepository; @@ -14,24 +15,25 @@ class MedicalHistoryController extends Controller { public function patient($id, Request $request) { - $viewType = $request->get('view_type', 'si'); + $viewType = $request->get('view_type', 'mis'); $patientId = $request->get('patient_id'); - $patientInfo = null; - if ($viewType == 'si') $patient = SiSttMedicalHistory::where('id', $id)->first(); - else $patient = MisSttMedicalHistory::where('MedicalHistoryID', $id)->first(); - $archiveJournal = $patient->archiveHistory ? ArchiveHistoryResource::collection($patient->archiveHistory) : null; + $archiveInfo = ArchiveInfo::whereId($id)->first()->load('status'); + if ($viewType == 'foxpro') $patient = $archiveInfo->foxproHistory; + else $patient = $archiveInfo->misHistory; - if (!empty($patient->archiveInfo)) { - $archiveInfo = ArchiveInfoResource::make($patient->archiveInfo)->toArray(request()); + $archiveJournal = $patient->archiveHistory ? ArchiveHistoryResource::collection($patient->archiveHistory) : null; +// dd($archiveInfo); + if (!empty($archiveInfo)) { + $archiveInfo = ArchiveInfoResource::make($archiveInfo)->toArray(request()); } else { $archiveInfo = null; } $patientInfo = [ - 'historyable_type' => $viewType == 'si' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class, + 'historyable_type' => $viewType == 'foxpro' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class, 'info' => [ - 'historyable_type' => $viewType == 'si' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class, + 'historyable_type' => $viewType == 'foxpro' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class, ...PatientInfoResource::make($patient)->toArray(request()), 'can_be_issued' => $patient->canBeIssued() ], diff --git a/app/Http/Resources/ArchiveHistoryResource.php b/app/Http/Resources/ArchiveHistoryResource.php index 05fa35c..146d5dd 100644 --- a/app/Http/Resources/ArchiveHistoryResource.php +++ b/app/Http/Resources/ArchiveHistoryResource.php @@ -21,7 +21,7 @@ class ArchiveHistoryResource extends JsonResource 'return_at' => $this->return_at ? Carbon::parse($this->return_at)->format('d.m.Y') : null, 'comment' => $this->comment, 'org_id' => $this->org_id, - 'org' => $this->org->name, + 'org' => $this->org?->name, 'employee_name' => $this->employee_name, 'employee_post' => $this->employee_post, 'has_lost' => $this->has_lost, diff --git a/app/Http/Resources/ArchiveInfoResource.php b/app/Http/Resources/ArchiveInfoResource.php index 10b6731..7d189d9 100644 --- a/app/Http/Resources/ArchiveInfoResource.php +++ b/app/Http/Resources/ArchiveInfoResource.php @@ -16,11 +16,12 @@ class ArchiveInfoResource extends JsonResource { return [ 'id' => $this->id, - 'num' => $this->num, + 'num' => $this->archive_num, 'post_in' => $this->post_in, 'status' => $this->status, - 'historyable_id' => $this->historyable_id, - 'historyable_type' => $this->historyable_type, + 'foxpro_num' => $this->foxpro_num, + 'mis_num' => $this->mis_num, + 'type' => $this->historyType() ]; } } diff --git a/app/Http/Resources/Mis/SttMedicalHistoryResource.php b/app/Http/Resources/Mis/SttMedicalHistoryResource.php index 3195cba..c5b74cf 100644 --- a/app/Http/Resources/Mis/SttMedicalHistoryResource.php +++ b/app/Http/Resources/Mis/SttMedicalHistoryResource.php @@ -2,9 +2,9 @@ namespace App\Http\Resources\Mis; +use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Support\Carbon; class SttMedicalHistoryResource extends JsonResource { @@ -15,17 +15,114 @@ class SttMedicalHistoryResource extends JsonResource */ public function toArray(Request $request): array { + // Определяем источник данных + $isFromArchive = $this->resource['in_archive'] ?? false; + $isTemporary = $this->resource['is_temporary'] ?? false; + $historyType = $this->resource['history_type'] ?? 'mis'; + + // Формируем ФИО + $family = $this->resource['family'] ?? ''; + $name = $this->resource['name'] ?? ''; + $ot = $this->resource['ot'] ?? ''; + + // Для временных записей (не в архиве) используем данные из MIS + if ($isTemporary) { + // Данные из stt_medicalhistory (не в архиве) + $fullName = trim("{$family} {$name} {$ot}"); + $birthDate = $this->resource['birth_date'] ?? null; + $dateExtract = $this->resource['date_extract'] ?? null; + $dateRecipient = null; // Для MIS записей не в архиве может не быть + $cardNumber = $this->resource['card_number'] ?? null; + $archiveNum = null; + $postIn = null; + $status = $this->resource['status_text'] ?? 'Не в архиве'; + } else { + // Данные из archive_infos (в архиве) + $fullName = trim("{$family} {$name} {$ot}"); + $birthDate = $this->resource['birth_date'] ?? null; + + // Для архивных записей date_extract может быть из MIS или FoxPro + $dateExtract = $this->resource['date_extract'] ?? null; + + // Для MIS записей в архиве + if ($historyType === 'mis') { + $dateRecipient = $this->resource['date_recipient'] ?? null; + } else { + // Для FoxPro записей в архиве + $dateRecipient = $this->resource['mpostdate'] ?? null; + } + + $cardNumber = $this->resource['card_number'] ?? null; + $archiveNum = $this->resource['archive_num'] ?? null; + $postIn = $this->resource['post_in'] ?? null; + $status = $this->resource['status_text'] ?? 'Неизвестно'; + } + + // Форматирование дат + $formattedBirthDate = $birthDate ? Carbon::parse($birthDate)->format('d.m.Y') : null; + $formattedDateRecipient = $dateRecipient ? Carbon::parse($dateRecipient)->format('d.m.Y') : null; + $formattedDateExtract = $dateExtract ? Carbon::parse($dateExtract)->format('d.m.Y') : null; + $formattedPostIn = $postIn ? Carbon::parse($postIn)->format('d.m.Y') : null; + return [ - 'id' => $this->MedicalHistoryID, - 'fullname' => $this->getFullNameAttribute(), - 'daterecipient' => Carbon::parse($this->DateRecipient)->format('d.m.Y'), - 'dateextract' => Carbon::parse($this->DateExtract)->format('d.m.Y'), - 'card_num' => $this->archiveInfo->num ?? null, - 'status' => $this->archiveInfo->status ?? null, - 'datearhiv' => $this->whenLoaded('archiveInfo') ? Carbon::parse($this->archiveInfo->post_in)->format('d.m.Y') : null, - 'medcardnum' => $this->MedCardNum, - 'dr' => Carbon::parse($this->BD)->format('d.m.Y'), - 'can_be_issue' => $this->canBeIssued() + 'id' => $this->resource['id'], + 'history_type' => $historyType, + 'in_archive' => $isFromArchive, + 'is_temporary' => $isTemporary, + 'source' => $this->resource['source'] ?? 'archive', + + // Основные данные + 'fullname' => $fullName, + 'family' => $family, + 'name' => $name, + 'ot' => $ot, + 'dr' => $formattedBirthDate, + 'daterecipient' => $formattedDateRecipient, + 'dateextract' => $formattedDateExtract, + + // Номера карт + 'medcardnum' => $cardNumber, // MIS номер или FoxPro номер + 'mis_card_number' => $this->resource['mis_card_number'] ?? null, // Оригинальный MIS номер + 'foxpro_card_number' => $this->resource['foxpro_card_number'] ?? null, // Оригинальный FoxPro номер + 'card_num' => $archiveNum, // Архивный номер + 'datearhiv' => $formattedPostIn, + + // Статус и возможности + 'status' => $status, + 'status_id' => $this->resource['status_id'] ?? 0, + 'can_be_issued' => $this->resource['can_be_issued'] ?? false, + 'can_add_to_archive' => $this->resource['can_add_to_archive'] ?? false, + + // Дополнительные идентификаторы + 'mis_history_id' => $this->resource['mis_history_id'] ?? null, + 'foxpro_history_id' => $this->resource['foxpro_history_id'] ?? null, + + // Дополнительные данные + 'snils' => $this->resource['snils'] ?? null, + 'enp' => $this->resource['enp'] ?? null, +// 'created_at' => $this->resource->created_at ? Carbon::parse($this->resource->created_at)->format('d.m.Y H:i') : null, +// 'updated_at' => $this->resource->updated_at ? Carbon::parse($this->resource->updated_at)->format('d.m.Y H:i') : null, + + // Стили для фронта + 'row_class' => $this->resource['row_class'] ?? '', + 'status_color' => $this->getStatusColor($this->resource['status_id'] ?? 0, $isFromArchive), ]; } + + /** + * Получение цвета статуса для фронта + */ + private function getStatusColor(int $statusId, bool $inArchive): string + { + if (!$inArchive) { + return 'warning'; // Желтый для не в архиве + } + + return match($statusId) { + 1 => 'success', // Зеленый для в архиве + 2 => 'info', // Синий для выдано + 3, 4 => 'danger', // Красный для утрачено/списано + default => 'secondary', + }; + } } diff --git a/app/Http/Resources/PatientInfoResource.php b/app/Http/Resources/PatientInfoResource.php index 7abe469..3d368f5 100644 --- a/app/Http/Resources/PatientInfoResource.php +++ b/app/Http/Resources/PatientInfoResource.php @@ -15,10 +15,10 @@ class PatientInfoResource extends JsonResource public function toArray(Request $request): array { return [ - 'id' => $this->id ?? $this->MedicalHistoryID, - 'medcardnum' => $this->medcardnum ?? $this->MedCardNum, - 'family' => $this->family ?? $this->FAMILY, - 'name' => $this->name ?? $this->Name, + 'id' => $this->keykarta ?? $this->MedicalHistoryID, + 'medcardnum' => $this->nkarta ?? $this->MedCardNum, + 'family' => $this->fam ?? $this->FAMILY, + 'name' => $this->im ?? $this->Name, 'ot' => $this->ot ?? $this->OT, ]; } diff --git a/app/Models/ArchiveHistory.php b/app/Models/ArchiveHistory.php index 68ebc2c..5c77800 100644 --- a/app/Models/ArchiveHistory.php +++ b/app/Models/ArchiveHistory.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\Mis\SttMedicalHistory as MisMedicalHistory; +use App\Models\SI\SttMedicalHistory as SiMedicalHistory; use Illuminate\Database\Eloquent\Model; class ArchiveHistory extends Model @@ -54,8 +56,9 @@ class ArchiveHistory extends Model } protected $fillable = [ - 'historyable_type', - 'historyable_id', + 'keyarhiv', + 'foxpro_history_id', + 'mis_history_id', 'issue_at', 'return_at', 'comment', @@ -65,9 +68,19 @@ class ArchiveHistory extends Model 'has_lost', ]; - public function historyable() + public function foxproHistory() { - return $this->morphTo(); + return $this->belongsTo(SiMedicalHistory::class, 'keykarta', 'foxpro_history_id'); + } + + public function misHistory() + { + return $this->belongsTo(MisMedicalHistory::class, 'MedicalHistoryID', 'mis_history_id'); + } + + public function historyType() + { + return $this->mis_history_id !== null ? 'mis' : 'foxpro'; } public function org(): \Illuminate\Database\Eloquent\Relations\BelongsTo diff --git a/app/Models/ArchiveInfo.php b/app/Models/ArchiveInfo.php index b264279..24e8ab7 100644 --- a/app/Models/ArchiveInfo.php +++ b/app/Models/ArchiveInfo.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\Mis\SttMedicalHistory as MisMedicalHistory; +use App\Models\SI\SttMedicalHistory as SiMedicalHistory; use Illuminate\Database\Eloquent\Model; class ArchiveInfo extends Model @@ -16,17 +18,30 @@ class ArchiveInfo extends Model protected $connection = 'pgsql'; protected $table = 'archive_infos'; + protected $fillable = [ - 'historyable_type', - 'historyable_id', - 'num', + 'foxpro_history_id', + 'mis_history_id', + 'foxpro_num', + 'mis_num', + 'archive_num', 'post_in', 'status_id' ]; - public function historyable() + public function foxproHistory() { - return $this->morphTo(); + return $this->belongsTo(SiMedicalHistory::class, 'foxpro_history_id', 'keykarta'); + } + + public function misHistory() + { + return $this->belongsTo(MisMedicalHistory::class, 'mis_history_id', 'MedicalHistoryID'); + } + + public function historyType() + { + return $this->mis_history_id !== null ? 'mis' : 'foxpro'; } public function status() diff --git a/app/Models/Mis/SttMedicalHistory.php b/app/Models/Mis/SttMedicalHistory.php index f91dd30..e7c9253 100644 --- a/app/Models/Mis/SttMedicalHistory.php +++ b/app/Models/Mis/SttMedicalHistory.php @@ -11,6 +11,11 @@ class SttMedicalHistory extends Model { protected $primaryKey = 'MedicalHistoryID'; protected $table = 'stt_medicalhistory'; + protected $keyType = 'string'; + + protected $casts = [ + 'MedicalHistoryID' => 'string', + ]; public function getFullNameAttribute() { @@ -19,12 +24,12 @@ class SttMedicalHistory extends Model public function archiveHistory() { - return $this->morphMany(ArchiveHistory::class, 'historyable'); + return $this->hasMany(ArchiveHistory::class, 'mis_history_id', 'MedicalHistoryID'); } public function archiveInfo() { - return $this->morphOne(ArchiveInfo::class, 'historyable'); + return $this->hasOne(ArchiveInfo::class, 'mis_history_id', 'MedicalHistoryID'); } /** @@ -41,7 +46,6 @@ class SttMedicalHistory extends Model $hasNotBadStatus = $this->archiveInfo() ->whereNotNull('status_id') - ->whereNot('status_id', 1) ->whereNot('status_id', 3) ->whereNot('status_id', 4) ->exists(); diff --git a/app/Models/SI/SttMedicalHistory.php b/app/Models/SI/SttMedicalHistory.php index 43c2248..e759763 100644 --- a/app/Models/SI/SttMedicalHistory.php +++ b/app/Models/SI/SttMedicalHistory.php @@ -8,36 +8,43 @@ use Illuminate\Database\Eloquent\Model; class SttMedicalHistory extends Model { - protected $table = 'si_stt_patients'; + protected $table = 'foxpro_original_patient'; + protected $primaryKey = 'keykarta'; protected $connection = 'pgsql'; + protected $keyType = 'string'; + public $incrementing = false; + + protected $casts = [ + 'keykarta' => 'string', + ]; protected $fillable = [ - 'family', // Фамилия - 'name', // Имя + 'fam', // Фамилия + 'im', // Имя 'ot', // Отчество - 'daterecipient', // Д. пост. (mpostdate) - 'dateextract', // Д. вып. (menddate) + 'mpostdate', // Д. пост. (mpostdate) + 'menddate', // Д. вып. (menddate) 'narhiv', // № в архиве 'datearhiv', // Д. архив - 'statgod', // Год нахождения в стационаре + 'snils', // Год нахождения в стационаре 'enp', // ЕНП - 'medcardnum', // № карты + 'nkarta', // № карты 'dr', // ДР ]; public function getFullNameAttribute() { - return "$this->family $this->name $this->ot"; + return "$this->fam $this->im $this->ot"; } public function archiveHistory() { - return $this->morphMany(ArchiveHistory::class, 'historyable'); + return $this->hasMany(ArchiveHistory::class, 'foxpro_history_id', 'keykarta'); } public function archiveInfo() { - return $this->morphOne(ArchiveInfo::class, 'historyable'); + return $this->hasOne(ArchiveInfo::class, 'foxpro_history_id', 'keykarta'); } /** diff --git a/app/Repositories/MedicalHistoryRepository.php b/app/Repositories/MedicalHistoryRepository.php index fb0377a..8ae76a4 100644 --- a/app/Repositories/MedicalHistoryRepository.php +++ b/app/Repositories/MedicalHistoryRepository.php @@ -1,327 +1,941 @@ siModel = $siModel; - $this->misModel = $misModel; - } - /** - * ПОИСК ТОЛЬКО В POSTGRESQL (основной) + * Основной метод поиска через RAW SQL */ - public function searchInPostgres( + public function unifiedSearch( ?string $searchText, ?string $dateFrom, ?string $dateTo, ?int $status, - int $pageSize = 15, - string $sortBy = 'dateextract', - string $sortDir = 'desc', - ): LengthAwarePaginator { + int $pageSize = 50 + ): LengthAwarePaginator + { + $page = request()->get('page', 1); + $offset = ($page - 1) * $pageSize; - $query = $this->siModel->newQuery(); - - $this->applyStatusFilter($query, $status); - $this->applyDateFilter($query, 'dateextract', $dateFrom, $dateTo); - $this->applySearchConditions($query, $searchText); - - return $query->select($this->defaultFieldsSI) - ->orderBy($sortBy, $sortDir) - ->with(['archiveInfo']) - ->paginate($pageSize) - ->through(function ($item) { - $item->database_source = 'postgresql'; - return $item; - }); - } - - /** - * ПОИСК ТОЛЬКО В MSSQL (исторический) - */ - public function searchInMssql( - ?string $searchText, - ?string $dateFrom, - ?string $dateTo, - ?int $status, - int $pageSize = 15, - string $sortBy = 'DateExtract', - string $sortDir = 'desc' - ): LengthAwarePaginator { - - $query = $this->misModel->newQuery(); - - $this->applyStatusFilter($query, $status); - $this->applyDateFilter($query, 'DateExtract', $dateFrom, $dateTo); - $this->applySearchConditions($query, $searchText, 'mssql'); - - return $query->select($this->defaultFieldsMis) - ->orderBy($sortBy, $sortDir) - ->with(['archiveInfo']) - ->paginate($pageSize) - ->through(function ($item) { - $item->database_source = 'mssql'; - return $item; - }); - } - - /** - * УМНЫЙ ПОИСК (сначала PostgreSQL, потом MSSQL если мало) - */ - public function smartSearch( - ?string $searchText, - ?string $dateFrom, - ?string $dateTo, - int $pageSize = 15 - ): LengthAwarePaginator { - - // 1. Ищем в PostgreSQL - $pgPaginator = $this->searchInPostgres($searchText, $dateFrom, $dateTo, $pageSize); - - // 2. Если мало результатов, добавляем из MSSQL - if ($pgPaginator->total() < $pageSize && $pgPaginator->total() < 10) { - $needed = $pageSize - $pgPaginator->count(); - - $mssqlResults = $this->searchInMssql($searchText, $dateFrom, $dateTo, $needed) - ->getCollection(); - - $allResults = $pgPaginator->getCollection() - ->merge($mssqlResults) - ->sortByDesc('dateextract') - ->values(); + // 1. Получаем только ID и минимальные данные для пагинации + $idQuery = $this->buildPaginatedIdQuery($searchText, $dateFrom, $dateTo, $status, $pageSize, $offset); + $idResults = DB::select($idQuery['sql'], $idQuery['params']); + if (empty($idResults)) { return new LengthAwarePaginator( - $allResults, - $pgPaginator->total() + $mssqlResults->count(), + collect(), + 0, $pageSize, - request()->get('page', 1) + $page, + ['path' => request()->url(), 'query' => request()->query()] ); } - return $pgPaginator; + // 2. Группируем ID по типу (в архиве/не в архиве) + $archiveIds = []; + $misIds = []; + + foreach ($idResults as $row) { + if ($row->in_archive) { + $archiveIds[] = $row->id; + } else { + $misIds[] = $row->id; + } + } + + // 3. Получаем полные данные отдельными запросами (это быстрее чем UNION) + $results = []; + + if (!empty($archiveIds)) { + $archiveData = $this->getArchiveDataByIds($archiveIds); + $results = array_merge($results, $archiveData); + } + + if (!empty($misIds)) { + $misData = $this->getMisDataByIds($misIds); + $results = array_merge($results, $misData); + } + + // 4. Сортируем как в ID запросе (сначала в архиве) + usort($results, function($a, $b) { + if ($a['in_archive'] == $b['in_archive']) { + return 0; + } + return $a['in_archive'] ? -1 : 1; + }); + + // 5. Получаем общее количество + $total = $this->getTotalCount($searchText, $dateFrom, $dateTo, $status); + + return new LengthAwarePaginator( + $results, + $total, + $pageSize, + $page, + ['path' => request()->url(), 'query' => request()->query()] + ); } /** - * РАЗДЕЛЬНЫЙ ПОИСК (отдельные результаты по БД) + * Запрос для получения ID с пагинацией */ - public function separateSearch( + private function buildPaginatedIdQuery( ?string $searchText, ?string $dateFrom, ?string $dateTo, ?int $status, - int $perPage = 15, - ): array { - $pgPaginator = $this->searchInPostgres($searchText, $dateFrom, $dateTo, $status, $perPage); - $mssqlPaginator = $this->searchInMssql($searchText, $dateFrom, $dateTo, $status, $perPage); + int $limit, + int $offset + ): array + { + $params = []; + + // 1. Архивные записи - используем EXISTS для проверки связей + $archiveSql = " + SELECT + ai.id, + ai.mis_history_id as original_id, + true as in_archive, + ai.created_at, + 1 as priority + FROM archive_infos ai + WHERE 1=1 + "; + + $archiveConditions = []; + + // Фильтр по статусу (быстрый) + if ($status !== null && $status !== 0) { + $archiveConditions[] = "ai.status_id = ?"; + $params[] = $status; + } + + // Поиск по тексту + if ($searchText) { + if (is_numeric($searchText)) { + // Быстрый поиск по номерам + $archiveConditions[] = "(ai.mis_num ILIKE ? OR ai.foxpro_num ILIKE ? OR ai.archive_num ILIKE ?)"; + $params[] = $searchText . '%'; + $params[] = $searchText . '%'; + $params[] = $searchText . '%'; + } else { + // Поиск по ФИО - используем EXISTS для foreign table + $words = preg_split('/\s+/', trim($searchText)); + $words = array_filter($words); + + if (!empty($words)) { + if (count($words) === 1) { + $word = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $archiveConditions[] = "( + EXISTS ( + SELECT 1 FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id + AND (mh.\"FAMILY\" ILIKE ? AND (mh.\"Name\" ILIKE ? OR mh.\"OT\" ILIKE ?)) + ) OR + EXISTS ( + SELECT 1 FROM foxpro_original_patient fp + WHERE fp.keykarta = ai.foxpro_history_id + AND (fp.fam ILIKE ? OR fp.im ILIKE ? OR fp.ot ILIKE ?) + ) + )"; + $params = array_merge($params, array_fill(0, 6, $word . '%')); + } elseif (count($words) === 2) { + $family = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $name = mb_strtoupper(mb_substr($words[1], 0, 1)) . mb_substr($words[1], 1); + $archiveConditions[] = "( + EXISTS ( + SELECT 1 FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id + AND mh.\"FAMILY\" ILIKE ? AND mh.\"Name\" ILIKE ? + ) OR + EXISTS ( + SELECT 1 FROM foxpro_original_patient fp + WHERE fp.keykarta = ai.foxpro_history_id + AND fp.fam ILIKE ? AND fp.im ILIKE ? + ) + )"; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $family . '%'; + $params[] = $name . '%'; + } elseif (count($words) === 3) { + $family = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $name = mb_strtoupper(mb_substr($words[1], 0, 1)) . mb_substr($words[1], 1); + $ot = mb_strtoupper(mb_substr($words[2], 0, 1)) . mb_substr($words[2], 1); + $archiveConditions[] = "( + EXISTS ( + SELECT 1 FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id + AND mh.\"FAMILY\" ILIKE ? AND mh.\"Name\" ILIKE ? + AND mh.\"OT\" ILIKE ? + ) OR + EXISTS ( + SELECT 1 FROM foxpro_original_patient fp + WHERE fp.keykarta = ai.foxpro_history_id + AND fp.fam ILIKE ? AND fp.im ILIKE ? + AND fp.ot ILIKE ? + ) + )"; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $ot . '%'; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $ot . '%'; + } + } + } + } + + // Фильтр по дате + if ($dateFrom || $dateTo) { + $dateConditions = []; + $dateParams = []; + + if ($dateFrom) { + $dateConditions[] = "COALESCE( + (SELECT mh.\"DateExtract\" FROM stt_medicalhistory mh WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id), + (SELECT fp.menddate FROM foxpro_original_patient fp WHERE fp.keykarta = ai.foxpro_history_id), + ai.created_at + ) >= ?"; + $dateParams[] = $dateFrom; + } + + if ($dateTo) { + $dateConditions[] = "COALESCE( + (SELECT mh.\"DateExtract\" FROM stt_medicalhistory mh WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id), + (SELECT fp.menddate FROM foxpro_original_patient fp WHERE fp.keykarta = ai.foxpro_history_id), + ai.created_at + ) <= ?"; + $dateParams[] = $dateTo; + } + + if (!empty($dateConditions)) { + $archiveConditions[] = "(" . implode(' AND ', $dateConditions) . ")"; + $params = array_merge($params, $dateParams); + } + } + + if (!empty($archiveConditions)) { + $archiveSql .= " AND " . implode(' AND ', $archiveConditions); + } + + // 2. MIS записи не в архиве + $misSql = " + SELECT + mh.\"MedicalHistoryID\" as id, + mh.\"MedicalHistoryID\" as original_id, + false as in_archive, + mh.\"DateExtract\" as created_at, + 2 as priority + FROM stt_medicalhistory mh + WHERE NOT EXISTS ( + SELECT 1 FROM archive_infos ai + WHERE ai.mis_history_id = mh.\"MedicalHistoryID\" + ) + "; + + $misConditions = $this->buildMisConditions($searchText, $status); + + if (!empty($misConditions['conditions'])) { + $misSql .= " AND " . implode(' AND ', $misConditions['conditions']); + } + + $misParams = $misConditions['params']; + + // Объединяем параметки + $allParams = array_merge($params, $misParams, [$limit, $offset]); + + $sql = " + SELECT * FROM ( + {$archiveSql} + UNION ALL + {$misSql} + ) as combined + ORDER BY priority, created_at DESC + LIMIT ? OFFSET ? + "; + + return ['sql' => $sql, 'params' => $allParams]; + } + + private function buildMisConditions( + ?string $searchText, + ?int $status + ): array + { + $conditions = []; + $params = []; + + // Только если статус 0 или null показываем записи не в архиве + if ($status !== null && $status !== 0) { + return ['conditions' => ["1 = 0"], 'params' => []]; + } + + // Поиск по тексту + if ($searchText && !is_numeric($searchText)) { + $words = preg_split('/\s+/', trim($searchText)); + $words = array_filter($words); + + if (!empty($words)) { + if (count($words) === 1) { + $word = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $conditions[] = "(mh.\"FAMILY\" ILIKE ? OR mh.\"Name\" ILIKE ? OR mh.\"OT\" ILIKE ?)"; + $params = array_fill(0, 3, $word . '%'); + } elseif (count($words) === 2) { + $family = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $name = mb_strtoupper(mb_substr($words[1], 0, 1)) . mb_substr($words[1], 1); + $conditions[] = "(mh.\"FAMILY\" ILIKE ? AND mh.\"Name\" ILIKE ?)"; + $params[] = $family . '%'; + $params[] = $name . '%'; + } elseif (count($words) >= 3) { + $family = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $name = mb_strtoupper(mb_substr($words[1], 0, 1)) . mb_substr($words[1], 1); + $ot = mb_strtoupper(mb_substr($words[2], 0, 1)) . mb_substr($words[2], 1); + $conditions[] = "(mh.\"FAMILY\" ILIKE ? AND mh.\"Name\" ILIKE ? AND mh.\"OT\" ILIKE ?)"; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $ot . '%'; + } + } + } return [ - 'si' => $pgPaginator, - 'mis' => $mssqlPaginator, - 'stats' => [ - 'si_total' => $pgPaginator->total(), - 'mis_total' => $mssqlPaginator->total(), - 'combined_total' => $pgPaginator->total() + $mssqlPaginator->total(), - ] + 'conditions' => $conditions, + 'params' => $params ]; } /** - * ПРИМЕНЕНИЕ УСЛОВИЙ ПОИСКА + * Условия для MIS записей не в архиве */ - private function applySearchConditions($query, ?string $searchText, string $dbType = 'postgresql'): void + private function buildMisWhere( + ?string $searchText, + ?int $status + ): array { - // Разбиваем поисковую строку на слова + $wheres = ["NOT EXISTS (SELECT 1 FROM archive_infos ai WHERE ai.mis_history_id = mh.\"MedicalHistoryID\")"]; + $params = []; + + // Только если статус 0 или null показываем записи не в архиве + if ($status !== null && $status !== 0) { + $wheres[] = "1 = 0"; + } + + // Поиск по тексту + if ($searchText) { + $searchConditions = $this->parseSearchTextForMis($searchText, $params); + if (!empty($searchConditions)) { + $wheres[] = "(" . implode(' OR ', $searchConditions) . ")"; + } + } + + $whereClause = 'WHERE ' . implode(' AND ', $wheres); + + return [ + 'where' => $whereClause, + 'params' => $params + ]; + } + + /** + * Парсинг поисковой строки для MIS таблицы + */ + private function parseSearchTextForMis(string $searchText, array &$params): array + { + $conditions = []; + + // Если строка содержит только цифры - это номер карты + if (is_numeric($searchText)) { + $conditions[] = 'mh."MedCardNum" ILIKE ?'; + $params[] = $searchText . '%'; + return $conditions; + } + + // Разбиваем строку на слова $words = preg_split('/\s+/', trim($searchText)); $words = array_filter($words); if (empty($words)) { - return; + return $conditions; } - $query->where(function($q) use ($words, $dbType) { - // Если одно слово - ищем в любом поле - if (count($words) === 1) { - $word = Str::ucfirst($words[0]); - $pattern = $word . '%'; // Префиксный поиск для использования индекса - - if ($dbType === 'postgresql') { - $q->where('family', 'LIKE', $pattern) - ->orWhere('name', 'LIKE', $pattern) - ->orWhere('ot', 'LIKE', $pattern); - } else { - $q->where('FAMILY', 'LIKE', $pattern) - ->orWhere('Name', 'LIKE', $pattern) - ->orWhere('OT', 'LIKE', $pattern); - } - } - // Если несколько слов - предполагаем Ф+И+О - else { - // Берем первые 3 слова - $family = !empty($words[0]) ? Str::ucfirst($words[0]) : null; - $name = !empty($words[1]) ? Str::ucfirst($words[1]) : null; - $ot = !empty($words[2]) ? Str::ucfirst($words[2]) : null; - - if ($dbType === 'postgresql') { - $q->where('family', 'LIKE', $family . '%'); - } else { - $q->where('FAMILY', 'LIKE', $family . '%'); - } - - if ($name) { - if ($dbType === 'postgresql') { - $q->where('name', 'LIKE', $name . '%'); - } else { - $q->where('Name', 'LIKE', $name . '%'); - } - } - - if ($ot) { - if ($dbType === 'postgresql') { - $q->where('ot', 'LIKE', $ot . '%'); - } else { - $q->where('OT', 'LIKE', $ot . '%'); - } - } - } - }); - } - - private function applyStatusFilter(Builder $query, ?int $value) - { - if ($value === 0) { - $query->doesntHave('archiveInfo'); - } else { - if ($query->withExists('archiveInfo') && !empty($value)) { - $query->withWhereHas('archiveInfo', function ($q) use ($value) { - $q->where('status_id', '=', $value); - }); - } + // Если одно слово - ищем по всем полям + if (count($words) === 1) { + $word = $words[0]; + $conditions[] = 'mh."MedCardNum" ILIKE ?'; + $conditions[] = 'mh."FAMILY" ILIKE ?'; + $conditions[] = 'mh."Name" ILIKE ?'; + $conditions[] = 'mh."OT" ILIKE ?'; + $params = array_merge($params, array_fill(0, 4, $word . '%')); } + // Если два слова - фамилия и имя + elseif (count($words) === 2) { + $family = $words[0]; + $name = $words[1]; + $conditions[] = '(mh."FAMILY" ILIKE ? AND mh."Name" ILIKE ?)'; + $params[] = $family . '%'; + $params[] = $name . '%'; + } + // Если три слова - фамилия, имя и отчество + elseif (count($words) >= 3) { + $family = $words[0]; + $name = $words[1]; + $ot = $words[2]; + $conditions[] = '(mh."FAMILY" ILIKE ? AND (mh."Name" ILIKE ? OR mh."OT" ILIKE ?))'; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $ot . '%'; + } + + return $conditions; } /** - * ФИЛЬТР ПО ДАТЕ + * Получение данных архивных записей по ID */ - private function applyDateFilter($query, string $dateField, ?string $dateFrom, ?string $dateTo): void + private function getArchiveDataByIds(array $ids): array { - if (!empty($dateFrom)) { - $query->whereDate($dateField, '>=', $dateFrom); + if (empty($ids)) { + return []; } - if (!empty($dateTo)) { - $query->whereDate($dateField, '<=', $dateTo); - } + $placeholders = implode(',', array_fill(0, count($ids), '?')); + + $sql = " + SELECT + ai.id, + ai.mis_history_id, + ai.foxpro_history_id, + COALESCE(ai.mis_num, ai.foxpro_num) as card_number, + ai.archive_num, + ai.post_in, + ai.status_id, + ai.created_at, + ai.updated_at, + COALESCE(mh.\"FAMILY\", fp.fam) as family, + COALESCE(mh.\"Name\", fp.im) as name, + COALESCE(mh.\"OT\", fp.ot) as ot, + CONCAT( + COALESCE(mh.\"FAMILY\", fp.fam), ' ', + COALESCE(mh.\"Name\", fp.im), ' ', + COALESCE(mh.\"OT\", fp.ot) + ) as full_name, + COALESCE(mh.\"DateExtract\", fp.menddate) as date_extract, + COALESCE(mh.\"BD\", fp.dr) as birth_date, + true as in_archive, + false as is_temporary, + 'archive' as source, + CASE + WHEN ai.mis_history_id IS NOT NULL THEN 'mis' + ELSE 'foxpro' + END as history_type, + mh.\"MedCardNum\" as mis_card_number, + mh.\"SS\" as snils, + fp.nkarta as foxpro_card_number, + fp.snils as foxpro_snils, + fp.enp as enp + FROM archive_infos ai + LEFT JOIN stt_medicalhistory mh ON ai.mis_history_id = mh.\"MedicalHistoryID\" + LEFT JOIN foxpro_original_patient fp ON ai.foxpro_history_id = fp.keykarta + WHERE ai.id IN ({$placeholders}) + "; + + $results = DB::select($sql, $ids); + + return array_map(function($item) { + return $this->transformResult($item); + }, $results); } /** - * ПОЛУЧИТЬ СТАТИСТИКУ ПО БАЗАМ + * Получение данных MIS записей по ID */ - public function getDatabaseStats(): array + private function getMisDataByIds(array $ids): array + { + if (empty($ids)) { + return []; + } + + $placeholders = implode(',', array_fill(0, count($ids), '?')); + + $sql = " + SELECT + mh.\"MedicalHistoryID\" as id, + mh.\"MedicalHistoryID\" as mis_history_id, + NULL as foxpro_history_id, + mh.\"MedCardNum\" as card_number, + NULL as archive_num, + NULL as post_in, + 0 as status_id, + mh.\"DateExtract\" as created_at, + NULL as updated_at, + mh.\"FAMILY\" as family, + mh.\"Name\" as name, + mh.\"OT\" as ot, + CONCAT(mh.\"FAMILY\", ' ', mh.\"Name\", ' ', COALESCE(mh.\"OT\", '')) as full_name, + mh.\"DateExtract\" as date_extract, + mh.\"BD\" as birth_date, + false as in_archive, + true as is_temporary, + 'mis' as source, + 'mis' as history_type, + mh.\"MedCardNum\" as mis_card_number, + mh.\"SS\" as snils, + NULL as foxpro_card_number, + NULL as foxpro_snils, + NULL as enp + FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" IN ({$placeholders}) + "; + + $results = DB::select($sql, $ids); + + return array_map(function($item) { + return $this->transformResult($item); + }, $results); + } + + /** + * Получение общего количества записей + */ + private function getTotalCount( + ?string $searchText, + ?string $dateFrom, + ?string $dateTo, + ?int $status + ): int + { + $archiveCount = $this->getArchiveCount($searchText, $dateFrom, $dateTo, $status); + $misCount = $this->getMisCount($searchText, $status); + + return $archiveCount + $misCount; + } + + /** + * Количество архивных записей + */ + private function getArchiveCount( + ?string $searchText, + ?string $dateFrom, + ?string $dateTo, + ?int $status + ): int + { + $params = []; + $conditions = []; + + // Фильтр по статусу + if ($status !== null && $status !== 0) { + $conditions[] = "ai.status_id = ?"; + $params[] = $status; + } + + // Поиск по тексту + if ($searchText) { + if (is_numeric($searchText)) { + $conditions[] = "(ai.mis_num ILIKE ? OR ai.foxpro_num ILIKE ? OR ai.archive_num ILIKE ?)"; + $params[] = $searchText . '%'; + $params[] = $searchText . '%'; + $params[] = $searchText . '%'; + } else { + $words = preg_split('/\s+/', trim($searchText)); + $words = array_filter($words); + + if (!empty($words)) { + if (count($words) === 1) { + $word = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $conditions[] = "( + EXISTS ( + SELECT 1 FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id + AND (mh.\"FAMILY\" ILIKE ? OR mh.\"Name\" ILIKE ? OR mh.\"OT\" ILIKE ?) + ) OR + EXISTS ( + SELECT 1 FROM foxpro_original_patient fp + WHERE fp.keykarta = ai.foxpro_history_id + AND (fp.fam ILIKE ? OR fp.im ILIKE ? OR fp.ot ILIKE ?) + ) + )"; + $params = array_merge($params, array_fill(0, 6, $word . '%')); + } elseif (count($words) === 2) { + $family = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $name = mb_strtoupper(mb_substr($words[1], 0, 1)) . mb_substr($words[1], 1); + $conditions[] = "( + EXISTS ( + SELECT 1 FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id + AND mh.\"FAMILY\" ILIKE ? AND mh.\"Name\" ILIKE ? + ) OR + EXISTS ( + SELECT 1 FROM foxpro_original_patient fp + WHERE fp.keykarta = ai.foxpro_history_id + AND fp.fam ILIKE ? AND fp.im ILIKE ? + ) + )"; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $family . '%'; + $params[] = $name . '%'; + } elseif (count($words) === 3) { + $family = mb_strtoupper(mb_substr($words[0], 0, 1)) . mb_substr($words[0], 1); + $name = mb_strtoupper(mb_substr($words[1], 0, 1)) . mb_substr($words[1], 1); + $ot = mb_strtoupper(mb_substr($words[2], 0, 1)) . mb_substr($words[2], 1); + $conditions[] = "( + EXISTS ( + SELECT 1 FROM stt_medicalhistory mh + WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id + AND mh.\"FAMILY\" ILIKE ? AND mh.\"Name\" ILIKE ? + AND mh.\"OT\" ILIKE ? + ) OR + EXISTS ( + SELECT 1 FROM foxpro_original_patient fp + WHERE fp.keykarta = ai.foxpro_history_id + AND fp.fam ILIKE ? AND fp.im ILIKE ? + AND fp.ot ILIKE ? + ) + )"; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $ot . '%'; + $params[] = $family . '%'; + $params[] = $name . '%'; + $params[] = $ot . '%'; + } + } + } + } + + // Фильтр по дате + if ($dateFrom || $dateTo) { + $dateConditions = []; + + if ($dateFrom) { + $dateConditions[] = "COALESCE( + (SELECT mh.\"DateExtract\" FROM stt_medicalhistory mh WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id), + (SELECT fp.menddate FROM foxpro_original_patient fp WHERE fp.keykarta = ai.foxpro_history_id), + ai.created_at + ) >= ?"; + $params[] = $dateFrom; + } + + if ($dateTo) { + $dateConditions[] = "COALESCE( + (SELECT mh.\"DateExtract\" FROM stt_medicalhistory mh WHERE mh.\"MedicalHistoryID\" = ai.mis_history_id), + (SELECT fp.menddate FROM foxpro_original_patient fp WHERE fp.keykarta = ai.foxpro_history_id), + ai.created_at + ) <= ?"; + $params[] = $dateTo; + } + + if (!empty($dateConditions)) { + $conditions[] = "(" . implode(' AND ', $dateConditions) . ")"; + } + } + + $sql = "SELECT COUNT(*) as count FROM archive_infos ai"; + if (!empty($conditions)) { + $sql .= " WHERE " . implode(' AND ', $conditions); + } + + $result = DB::selectOne($sql, $params); + return (int) $result->count; + } + + /** + * Количество MIS записей не в архиве + */ + private function getMisCount( + ?string $searchText, + ?int $status + ): int + { + $conditions = $this->buildMisConditions($searchText, $status); + + // Добавляем NOT EXISTS в начало + $allConditions = array_merge( + ["NOT EXISTS (SELECT 1 FROM archive_infos ai WHERE ai.mis_history_id = mh.\"MedicalHistoryID\")"], + $conditions['conditions'] + ); + + $sql = " + SELECT COUNT(*) as count + FROM stt_medicalhistory mh + WHERE " . implode(' AND ', $allConditions); + + $result = DB::selectOne($sql, $conditions['params']); + return (int) $result->count; + } + + /** + * Преобразование результата + */ + private function transformResult($item): array { return [ - 'postgresql' => [ - 'total' => $this->siModel->count(), - 'connection' => config('database.connections.pgsql.database'), - 'status' => 'primary', - ], - 'mssql' => [ - 'total' => $this->misModel->count(), - 'connection' => config('database.connections.sqlsrv.database'), - 'status' => 'secondary', - ] + 'id' => $item->id, + 'mis_history_id' => $item->mis_history_id, + 'foxpro_history_id' => $item->foxpro_history_id, + 'card_number' => $item->card_number, + 'archive_num' => $item->archive_num, + 'post_in' => $item->post_in, + 'status_id' => (int)$item->status_id, + 'full_name' => $item->full_name, + 'family' => $item->family, + 'name' => $item->name, + 'ot' => $item->ot, + 'date_extract' => $item->date_extract, + 'birth_date' => $item->birth_date, + 'created_at' => $item->created_at, + 'updated_at' => $item->updated_at, + 'in_archive' => (bool)$item->in_archive, + 'is_temporary' => (bool)$item->is_temporary, + 'source' => $item->source, + 'history_type' => $item->history_type, + 'snils' => $item->snils ?? $item->foxpro_snils, + 'enp' => $item->enp, + 'mis_card_number' => $item->mis_card_number, + 'foxpro_card_number' => $item->foxpro_card_number, + 'status_text' => $this->getStatusText((int)$item->status_id, (bool)$item->in_archive), + 'can_be_issued' => $this->canBeIssued((int)$item->status_id, (bool)$item->in_archive), + 'can_add_to_archive' => !(bool)$item->in_archive, + 'row_class' => $this->getRowClass((int)$item->status_id, (bool)$item->in_archive), ]; } - /** - * БЫСТРЫЙ ПОИСК ПО ТИПУ - */ - public function quickSearch(string $type, string $value): Collection + private function getStatusText(int $statusId, bool $inArchive): string { - return match($type) { - 'medcard' => $this->searchByMedCard($value), - 'enp' => $this->searchByEnp($value), - 'fullname' => $this->searchByFullName($value), - default => collect(), + if (!$inArchive) { + return 'Не в архиве'; + } + + return match($statusId) { + 1 => 'В архиве', + 2 => 'Выдано', + 3 => 'Утрачено', + 4 => 'Списано', + default => 'Неизвестно', }; } - private function searchByMedCard(string $medCard): Collection + private function canBeIssued(int $statusId, bool $inArchive): bool { - $pgResults = $this->siModel->where('medcardnum', 'LIKE', "%{$medCard}%") - ->limit(10) - ->get() - ->each(fn($item) => $item->database_source = 'postgresql'); - - $mssqlResults = $this->misModel->where('medcardnum', 'LIKE', "%{$medCard}%") - ->limit(10) - ->get() - ->each(fn($item) => $item->database_source = 'mssql'); - - return $pgResults->concat($mssqlResults) - ->sortBy('medcardnum') - ->values(); + return $inArchive && $statusId === 1; } - private function searchByFullName(string $name): Collection + private function getRowClass(int $statusId, bool $inArchive): string { - $pattern = "%{$name}%"; + if (!$inArchive) { + return 'table-warning'; + } - $pgResults = $this->siModel->where('family', 'ILIKE', $pattern) - ->orWhere('name', 'ILIKE', $pattern) - ->orWhere('ot', 'ILIKE', $pattern) - ->limit(10) - ->get() - ->each(fn($item) => $item->database_source = 'postgresql'); + return match($statusId) { + 1 => 'table-success', + 2 => 'table-info', + 3, 4 => 'table-danger', + default => '', + }; + } - $mssqlResults = $this->misModel->where('family', 'LIKE', $pattern) - ->orWhere('name', 'LIKE', $pattern) - ->orWhere('ot', 'LIKE', $pattern) - ->limit(10) - ->get() - ->each(fn($item) => $item->database_source = 'mssql'); + /** + * Быстрый поиск для автокомплита + */ + public function quickSearch(string $searchText, int $limit = 20): array + { + if (is_numeric($searchText)) { + return $this->quickSearchByCardNumber($searchText, $limit); + } - return $pgResults->concat($mssqlResults) - ->sortBy('family') - ->values(); + return $this->quickSearchByName($searchText, $limit); + } + + private function quickSearchByCardNumber(string $cardNumber, int $limit): array + { + $searchPattern = '%' . $cardNumber . '%'; + + $sql = " + SELECT * FROM ( + -- Архивные записи + SELECT + ai.id, + COALESCE(ai.mis_num, ai.foxpro_num) as card_number, + CONCAT( + COALESCE(mh.\"FAMILY\", fp.fam), ' ', + COALESCE(mh.\"Name\", fp.im) + ) as full_name, + true as in_archive, + 'archive' as source + FROM archive_infos ai + LEFT JOIN stt_medicalhistory mh ON ai.mis_history_id = mh.\"MedicalHistoryID\" + LEFT JOIN foxpro_original_patient fp ON ai.foxpro_history_id = fp.keykarta + WHERE ai.mis_num ILIKE ? OR ai.foxpro_num ILIKE ? + + UNION ALL + + -- MIS записи не в архиве + SELECT + mh.\"MedicalHistoryID\" as id, + mh.\"MedCardNum\" as card_number, + CONCAT(mh.\"FAMILY\", ' ', mh.\"Name\") as full_name, + false as in_archive, + 'mis' as source + FROM stt_medicalhistory mh + WHERE NOT EXISTS ( + SELECT 1 FROM archive_infos ai + WHERE ai.mis_history_id = mh.\"MedicalHistoryID\" + ) + AND mh.\"MedCardNum\" ILIKE ? + ) as results + ORDER BY in_archive DESC, full_name + LIMIT ? + "; + + $params = [$searchPattern, $searchPattern, $searchPattern, $limit]; + $results = DB::select($sql, $params); + + return array_map(function($item) { + return [ + 'id' => $item->id, + 'card_number' => $item->card_number, + 'full_name' => $item->full_name, + 'in_archive' => (bool)$item->in_archive, + 'source' => $item->source, + 'can_be_issued' => (bool)$item->in_archive, + ]; + }, $results); + } + + private function quickSearchByName(string $name, int $limit): array + { + $searchPattern = '%' . $name . '%'; + + $sql = " + SELECT * FROM ( + -- Архивные записи + SELECT + ai.id, + COALESCE(ai.mis_num, ai.foxpro_num) as card_number, + CONCAT( + COALESCE(mh.\"FAMILY\", fp.fam), ' ', + COALESCE(mh.\"Name\", fp.im) + ) as full_name, + true as in_archive, + 'archive' as source + FROM archive_infos ai + LEFT JOIN stt_medicalhistory mh ON ai.mis_history_id = mh.\"MedicalHistoryID\" + LEFT JOIN foxpro_original_patient fp ON ai.foxpro_history_id = fp.keykarta + WHERE mh.\"FAMILY\" ILIKE ? OR mh.\"Name\" ILIKE ? + + UNION ALL + + -- MIS записи не в архиве + SELECT + mh.\"MedicalHistoryID\" as id, + mh.\"MedCardNum\" as card_number, + CONCAT(mh.\"FAMILY\", ' ', mh.\"Name\") as full_name, + false as in_archive, + 'mis' as source + FROM stt_medicalhistory mh + WHERE NOT EXISTS ( + SELECT 1 FROM archive_infos ai + WHERE ai.mis_history_id = mh.\"MedicalHistoryID\" + ) + AND (mh.\"FAMILY\" ILIKE ? OR mh.\"Name\" ILIKE ?) + ) as results + ORDER BY in_archive DESC, full_name + LIMIT ? + "; + + $params = [$searchPattern, $searchPattern, $searchPattern, $searchPattern, $limit]; + $results = DB::select($sql, $params); + + return array_map(function($item) { + return [ + 'id' => $item->id, + 'card_number' => $item->card_number, + 'full_name' => $item->full_name, + 'in_archive' => (bool)$item->in_archive, + 'source' => $item->source, + 'can_be_issued' => (bool)$item->in_archive, + ]; + }, $results); + } + + /** + * Поиск по точному номеру карты (для выдачи) + */ + public function searchByExactCardNumber(string $cardNumber): ?array + { + // Сначала ищем в архиве + $sql = " + SELECT + ai.id, + ai.mis_history_id, + ai.foxpro_history_id, + COALESCE(ai.mis_num, ai.foxpro_num) as card_number, + COALESCE(mh.\"FAMILY\", fp.fam) as family, + COALESCE(mh.\"Name\", fp.im) as name, + COALESCE(mh.\"OT\", fp.ot) as ot, + ai.archive_num, + ai.post_in, + ai.status_id, + true as in_archive, + 'archive' as source, + ai.created_at, + COALESCE(mh.\"DateExtract\", fp.menddate) as date_extract + FROM archive_infos ai + LEFT JOIN stt_medicalhistory mh ON ai.mis_history_id = mh.\"MedicalHistoryID\" + LEFT JOIN foxpro_original_patient fp ON ai.foxpro_history_id = fp.keykarta + WHERE ai.mis_num = ? OR ai.foxpro_num = ? + LIMIT 1 + "; + + $result = DB::selectOne($sql, [$cardNumber, $cardNumber]); + + if ($result) { + $result->full_name = trim($result->family . ' ' . $result->name . ' ' . ($result->ot ?? '')); + $result->is_temporary = false; + return $this->transformResult($result); + } + + // Если нет в архиве, ищем в MIS + $sql = " + SELECT + mh.\"MedicalHistoryID\" as id, + mh.\"MedicalHistoryID\" as mis_history_id, + NULL as foxpro_history_id, + mh.\"MedCardNum\" as card_number, + mh.\"FAMILY\" as family, + mh.\"Name\" as name, + mh.\"OT\" as ot, + NULL as archive_num, + NULL as post_in, + 0 as status_id, + false as in_archive, + 'mis' as source, + mh.\"DateExtract\" as created_at, + mh.\"DateExtract\" as date_extract + FROM stt_medicalhistory mh + WHERE mh.\"MedCardNum\" = ? + AND NOT EXISTS ( + SELECT 1 FROM archive_infos ai + WHERE ai.mis_history_id = mh.\"MedicalHistoryID\" + ) + LIMIT 1 + "; + + $result = DB::selectOne($sql, [$cardNumber]); + + if ($result) { + $result->full_name = trim($result->family . ' ' . $result->name . ' ' . ($result->ot ?? '')); + $result->is_temporary = true; + return $this->transformResult($result); + } + + return null; } } diff --git a/app/Services/ArchiveSearchService.php b/app/Services/ArchiveSearchService.php new file mode 100644 index 0000000..cbfa5d6 --- /dev/null +++ b/app/Services/ArchiveSearchService.php @@ -0,0 +1,146 @@ +searchInArchiveInfos($searchText); + if ($archiveInfo) { + return $archiveInfo; + } + + // 2. Если нет в archive_infos, ищем в MIS + $misHistory = $this->searchInMis($searchText); + if ($misHistory) { + // Создаем запись в archive_infos для MIS карты + return $this->createArchiveInfoForMis($misHistory); + } + + // 3. Если нет в MIS, ищем в FoxPro + $foxproHistory = $this->searchInFoxPro($searchText); + if ($foxproHistory) { + // Создаем запись в archive_infos для FoxPro карты + return $this->createArchiveInfoForFoxPro($foxproHistory); + } + + return null; + } + + /** + * Поиск по ФИО или другим критериям + */ + public function searchByName(string $searchText): array + { + // Поиск в MIS + $misResults = MisMedicalHistory::query() + ->where(function($query) use ($searchText) { + $query->whereRaw("CONCAT(\"FAMILY\", ' ', \"Name\", ' ', COALESCE(\"OT\", '')) ILIKE ?", ["$searchText%"]) + ->orWhere('FAMILY', 'ILIKE', "$searchText%") + ->orWhere('Name', 'ILIKE', "$searchText%") + ->orWhere('OT', 'ILIKE', "$searchText%"); + }) + ->get(); + + return $misResults; + } + + /** + * Поиск в archive_infos + */ + private function searchInArchiveInfos(string $cardNumber): ?ArchiveInfo + { + return ArchiveInfo::query() + ->where('mis_num', $cardNumber) + ->orWhere('foxpro_num', $cardNumber) + ->with(['misHistory', 'foxproHistory']) + ->first(); + } + + /** + * Поиск в MIS + */ + private function searchInMis(string $cardNumber): ?MisMedicalHistory + { + return MisMedicalHistory::where('MedCardNum', $cardNumber)->first(); + } + + /** + * Поиск в FoxPro + */ + private function searchInFoxPro(string $cardNumber): ?SiMedicalHistory + { + return SiMedicalHistory::where('nkarta', $cardNumber)->first(); + } + + /** + * Создание записи в archive_infos для MIS карты + */ + private function createArchiveInfoForMis(MisMedicalHistory $misHistory): ArchiveInfo + { + // Проверяем, нет ли уже записи + $existingArchiveInfo = ArchiveInfo::where('mis_history_id', $misHistory->MedicalHistoryID)->first(); + if ($existingArchiveInfo) { + return $existingArchiveInfo; + } + + // Ищем связанную запись в FoxPro + $foxproHistory = SiMedicalHistory::where('snils', $misHistory->SS) + ->where('menddate', $misHistory->DateExtract) + ->where('dr', '!=', $misHistory->BD) + ->first(); + + // Создаем запись + return ArchiveInfo::create([ + 'mis_history_id' => $misHistory->MedicalHistoryID, + 'mis_num' => $misHistory->MedCardNum, + 'foxpro_history_id' => $foxproHistory?->keykarta, + 'foxpro_num' => $foxproHistory?->nkarta, + 'archive_num' => $foxproHistory?->narhiv, + 'post_in' => $foxproHistory?->datearhiv, + 'status_id' => 2, // Статус по умолчанию + ]); + } + + /** + * Создание записи в archive_infos для FoxPro карты + */ + private function createArchiveInfoForFoxPro(SiMedicalHistory $foxproHistory): ArchiveInfo + { + // Проверяем, нет ли уже записи + $existingArchiveInfo = ArchiveInfo::where('foxpro_history_id', $foxproHistory->keykarta)->first(); + if ($existingArchiveInfo) { + return $existingArchiveInfo; + } + + // Ищем связанную запись в MIS + $misHistory = MisMedicalHistory::where('SS', $foxproHistory->snils) + ->where('DateExtract', $foxproHistory->menddate) + ->where('BD', '!=', $foxproHistory->dr) + ->first(); + + // Создаем запись + return ArchiveInfo::create([ + 'foxpro_history_id' => $foxproHistory->keykarta, + 'foxpro_num' => $foxproHistory->nkarta, + 'archive_num' => $foxproHistory->narhiv, + 'post_in' => $foxproHistory->datearhiv, + 'mis_history_id' => $misHistory?->MedicalHistoryID, + 'mis_num' => $misHistory?->MedCardNum, + 'status_id' => 2, // Статус по умолчанию + ]); + } +} diff --git a/composer.json b/composer.json index 3a5aceb..0919543 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "laravel/tinker": "^2.10.1" }, "require-dev": { + "barryvdh/laravel-debugbar": "^3.16", "fakerphp/faker": "^1.23", "laravel/pail": "^1.2.2", "laravel/pint": "^1.24", diff --git a/composer.lock b/composer.lock index e89e1ac..1d0eb00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4ecf76c8e987c4f4186a17e150248317", + "content-hash": "d35c2af4354943a6ab7e9b9ca3c4fed0", "packages": [ { "name": "brick/math", @@ -6136,6 +6136,91 @@ } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.16.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/730dbf8bf41f5691e026dd771e64dd54ad1b10b3", + "reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3", + "shasum": "" + }, + "require": { + "illuminate/routing": "^10|^11|^12", + "illuminate/session": "^10|^11|^12", + "illuminate/support": "^10|^11|^12", + "php": "^8.1", + "php-debugbar/php-debugbar": "^2.2.4", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^7|^8|^9|^10", + "phpunit/phpunit": "^9.5.10|^10|^11", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + }, + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.16-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "dev", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.2" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T14:52:46+00:00" + }, { "name": "brianium/paratest", "version": "v7.14.2", @@ -7614,6 +7699,80 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-debugbar/php-debugbar", + "version": "v2.2.5", + "source": { + "type": "git", + "url": "https://github.com/php-debugbar/php-debugbar.git", + "reference": "c5dce08e98dd101c771e55949fd89124b216271d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/c5dce08e98dd101c771e55949fd89124b216271d", + "reference": "c5dce08e98dd101c771e55949fd89124b216271d", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.4|^7.3|^8.0" + }, + "replace": { + "maximebf/debugbar": "self.version" + }, + "require-dev": { + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^10", + "symfony/browser-kit": "^6.0|7.0", + "symfony/panther": "^1|^2.1", + "twig/twig": "^3.11.2" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/php-debugbar/php-debugbar", + "keywords": [ + "debug", + "debug bar", + "debugbar", + "dev" + ], + "support": { + "issues": "https://github.com/php-debugbar/php-debugbar/issues", + "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.5" + }, + "time": "2025-12-21T08:50:08+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", diff --git a/config/app.php b/config/app.php index 423eed5..02d4ed4 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => 'Asia/Yakutsk', /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2025_11_30_112144_create_archive_histories_table.php b/database/migrations/2025_11_30_112144_create_archive_histories_table.php index 3ef879f..baf0a5f 100644 --- a/database/migrations/2025_11_30_112144_create_archive_histories_table.php +++ b/database/migrations/2025_11_30_112144_create_archive_histories_table.php @@ -13,7 +13,9 @@ return new class extends Migration { Schema::create('archive_histories', function (Blueprint $table) { $table->id(); - $table->morphs('historyable'); + $table->string('keyarhiv')->nullable(); + $table->string('foxpro_history_id')->nullable(); + $table->bigInteger('mis_history_id')->nullable(); $table->timestamp('issue_at'); $table->timestamp('return_at')->nullable(); $table->text('comment')->nullable(); diff --git a/database/migrations/2025_12_05_030609_create_archive_infos_table.php b/database/migrations/2025_12_05_030609_create_archive_infos_table.php index 71122f0..7debc34 100644 --- a/database/migrations/2025_12_05_030609_create_archive_infos_table.php +++ b/database/migrations/2025_12_05_030609_create_archive_infos_table.php @@ -13,8 +13,11 @@ return new class extends Migration { Schema::create('archive_infos', function (Blueprint $table) { $table->id(); - $table->morphs('historyable'); - $table->string('num'); + $table->string('foxpro_history_id')->nullable(); + $table->bigInteger('mis_history_id')->nullable(); + $table->string('foxpro_num')->nullable(); + $table->string('mis_num')->nullable(); + $table->string('archive_num'); $table->date('post_in'); $table->foreignIdFor(\App\Models\ArchiveStatus::class, 'status_id')->default(1); $table->timestamps(); diff --git a/database/seeders/OrgSeeder.php b/database/seeders/OrgSeeder.php new file mode 100644 index 0000000..1b06331 --- /dev/null +++ b/database/seeders/OrgSeeder.php @@ -0,0 +1,21 @@ + '0', + 'name' => 'Неизвестно' + ]); + } +} diff --git a/resources/js/Composables/useMedicalHistoryFilter.js b/resources/js/Composables/useMedicalHistoryFilter.js index f95a127..a4387d5 100644 --- a/resources/js/Composables/useMedicalHistoryFilter.js +++ b/resources/js/Composables/useMedicalHistoryFilter.js @@ -16,51 +16,15 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { page_size: initialFilters?.page_size || 50, sort_by: initialFilters?.sort_by || 'dateextract', sort_order: initialFilters?.sort_order || 'desc', - view_type: initialFilters?.view_type || 'si', status: initialFilters?.status || null, - database: initialFilters?.database || 'separate', // НОВЫЙ ПАРАМЕТР: postgresql, mssql, smart, separate }) // Метаданные для разных типов данных const meta = computed(() => { - const cards = page.props.cards - - // Для раздельного поиска - if (filtersRef.value.database === 'separate') { - return { - si: cards.si.meta || {}, - mis: cards.mis.meta || {}, - stats: cards.stats || {}, - } - } else if (filtersRef.value.database === 'mis') { - return { - mis: cards.mis?.meta - } - } else { - return { - si: cards.si?.meta - } - } + return page.props.cards.meta }) const isLoading = ref(false) - const databaseStats = computed(() => page.props.databaseStats || {}) - - // Статистика по базам - const databaseInfo = computed(() => ({ - postgresql: { - count: databaseStats.value.postgresql?.total || 0, - label: 'PostgreSQL', - color: 'blue', - description: 'Основной архив' - }, - mssql: { - count: databaseStats.value.mssql?.total || 0, - label: 'MSSQL', - color: 'purple', - description: 'Исторический архив' - } - })) // Форматирование даты для URL const formatDateForUrl = (date) => { @@ -81,8 +45,6 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { page_size: filtersRef.value.page_size, sort_by: filtersRef.value.sort_by, sort_order: filtersRef.value.sort_order, - view_type: filtersRef.value.view_type, - database: filtersRef.value.database, status: filtersRef.value.status } @@ -128,10 +90,6 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { debouncedSearch(value) } - const handleDatabaseChange = (database) => { - applyFilters({ database, page: 1 }, false) - } - // Конвертация строки даты в timestamp для NaiveUI const convertToTimestamp = (dateString) => { if (!dateString) return null @@ -186,10 +144,6 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { applyFilters({ page_size: size, page: 1 }) } - const handleViewTypeChange = (view_type) => { - applyFilters({ view_type, page: 1 }) - } - const handleSortChange = (sorter) => { applyFilters({ sort_by: sorter.columnKey, @@ -213,8 +167,6 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { page_size: 15, sort_by: 'dateextract', sort_order: 'desc', - view_type: 'archive', - database: 'postgresql', } dateRange.value = [null, null] applyFilters({}, true) @@ -270,22 +222,6 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { }) } - // Фильтр по базе данных - if (filtersRef.value.database) { - const dbLabel = { - postgresql: 'PostgreSQL', - mssql: 'MSSQL', - smart: 'Умный поиск', - separate: 'Раздельно' - }[filtersRef.value.database] || filtersRef.value.database - - active.push({ - key: 'database', - label: `База: ${dbLabel}`, - icon: 'database' - }) - } - return active }) @@ -341,12 +277,8 @@ export const useMedicalHistoryFilter = (initialFilters = {}) => { activeFilters, dateRange, searchValue, - databaseInfo, - databaseStats, // Обработчики - handleDatabaseChange, - handleViewTypeChange, handleSearch, handleDateRangeChange, handlePageChange, diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index ce12593..c9e0bbd 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -1,5 +1,5 @@