diff --git a/.env.example b/.env.example index c0660ea..b70acca 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,12 @@ DB_CONNECTION=sqlite # DB_USERNAME=root # DB_PASSWORD= +MIS_DB_HOST= +MIS_DB_PORT= +MIS_DB_DATABASE= +MIS_DB_USERNAME= +MIS_DB_PASSWORD= + SESSION_DRIVER=database SESSION_LIFETIME=120 SESSION_ENCRYPT=false diff --git a/app/Contracts/MedicalHistoryRepositoryInterface.php b/app/Contracts/MedicalHistoryRepositoryInterface.php new file mode 100644 index 0000000..79442b0 --- /dev/null +++ b/app/Contracts/MedicalHistoryRepositoryInterface.php @@ -0,0 +1,9 @@ +json($archiveHistory); } - public function move(Request $request) + public function moveStore(Request $request) { $data = $request->validate([ - 'issue_at' => 'nullable|numeric:', - 'return_at' => 'nullable|numeric:', + 'issue_at' => ['nullable', new DateTimeOrStringOrNumber], + 'return_at' => ['nullable', new DateTimeOrStringOrNumber], 'org_id' => 'required|numeric', 'employee_name' => 'nullable|string', 'employee_post' => 'nullable|string', 'comment' => 'nullable|string', 'has_lost' => 'boolean', 'historyable_id' => 'nullable|numeric', + 'historyable_type' => 'required|string', ]); - // Находим связанную модель ?? - $historyable = SttMedicalHistory::findOrFail($data['historyable_id']); - // Преобразуем timestamp в дату, если пришли числа if (isset($data['issue_at']) && is_numeric($data['issue_at'])) { $data['issue_at'] = Carbon::createFromTimestampMs($data['issue_at'])->format('Y-m-d'); @@ -48,6 +49,89 @@ class ArchiveHistoryController extends Controller $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(); + } + } + } + return response()->json(ArchiveHistoryResource::make($archiveHistory)); } + + public function moveUpdate($id, Request $request) + { + $data = $request->validate([ + 'issue_at' => ['nullable', new DateTimeOrStringOrNumber], + 'return_at' => ['nullable', new DateTimeOrStringOrNumber], + 'org_id' => 'required|numeric', + 'employee_name' => 'nullable|string', + 'employee_post' => 'nullable|string', + 'comment' => 'nullable|string', + 'has_lost' => 'boolean', + 'historyable_id' => 'nullable|numeric', + 'historyable_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'); + } + + if (isset($data['return_at']) && is_numeric($data['return_at'])) { + $data['return_at'] = Carbon::createFromTimestampMs($data['return_at'])->format('Y-m-d'); + } + + $archiveHistory = ArchiveHistory::find($id); + + $hasUpdated = $archiveHistory->update($data); + + return response()->json(ArchiveHistoryResource::make($archiveHistory)); + } + + public function infoUpdate($patientId, Request $request) + { + $data = $request->validate([ + 'id' => 'required|numeric', + 'num' => 'nullable|string', + 'post_in' => ['nullable', new DateTimeOrStringOrNumber], + 'historyable_type' => 'required|string', + ]); + + // Преобразуем timestamp в дату, если пришли числа + if (isset($data['post_in']) && is_numeric($data['post_in'])) { + $data['post_in'] = Carbon::createFromTimestampMs($data['post_in'])->format('Y-m-d'); + } + + if ($patientId && $request->filled('historyable_type')) { + // Найти связанную модель + $historyableClass = $request->input('historyable_type'); + + // Проверяем, существует ли класс модели + if (class_exists($historyableClass)) { + $historyableModel = $historyableClass::find($patientId); + + if ($historyableModel) { + // Связываем модели + $historyableModel->archiveInfo()->updateOrCreate([ + 'historyable_type' => $historyableClass, + 'historyable_id' => $patientId, + ], $data); + return response()->json(ArchiveInfoResource::make($historyableModel->archiveInfo)); + } + } + } + + return response()->json()->setStatusCode(500); + } } diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index 0101be8..5eb80ca 100644 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -2,41 +2,119 @@ namespace App\Http\Controllers; -use App\Http\Resources\SI\SttMedicalHistoryResource; +use App\Http\Resources\SI\SttMedicalHistoryResource as SiSttMedicalHistoryResource; +use App\Http\Resources\Mis\SttMedicalHistoryResource as MisSttMedicalHistoryResource; +use App\Models\ArchiveStatus; use App\Models\SI\SttMedicalHistory; +use App\Repositories\MedicalHistoryRepository; use Illuminate\Http\Request; use Inertia\Inertia; class IndexController extends Controller { + private MedicalHistoryRepository $repository; + + public function __construct(MedicalHistoryRepository $repository) + { + $this->repository = $repository; + } + public function index(Request $request) { - $pageSize = $request->get('page_size', 15); + $pageSize = $request->get('page_size', 50); $searchText = $request->get('search', null); $dateExtractFrom = $request->get('date_extract_from', null); $dateExtractTo = $request->get('date_extract_to', null); - $viewType = $request->get('view_type', 'archive'); + $database = $request->get('database', 'separate'); // si, mis + $status = $request->get('status', null); - $cardsQuery = SttMedicalHistory::query(); + $data = []; + $databaseStats = $this->repository->getDatabaseStats(); - if (!empty($searchText)) { - $cardsQuery = $cardsQuery->search($searchText); + 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; } - if (!empty($dateExtractFrom)) { - $cardsQuery = $cardsQuery->whereDate('dateextract', '>=', $dateExtractFrom); - if (!empty($dateExtractTo)) { - $cardsQuery = $cardsQuery->whereDate('dateextract', '<=', $dateExtractTo); - } - } - - $cards = SttMedicalHistoryResource::collection($cardsQuery->paginate($pageSize)); + $statuses = ArchiveStatus::all()->map(function ($status) { + return [ + 'value' => $status->id, + 'label' => $status->text + ]; + }); return Inertia::render('Home/Index', [ - 'cards' => $cards, - 'filters' => $request->only([ - 'search', 'date_extract_from', 'date_extract_to', 'page_size', 'page', 'view_type' - ]), + 'cards' => $data, + 'statuses' => $statuses, + 'databaseStats' => $databaseStats, + 'filters' => array_merge($request->only([ + 'search', 'date_extract_from', 'date_extract_to', + 'page_size', 'page', 'view_type', 'database', '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 3f730c5..a644cfc 100644 --- a/app/Http/Controllers/MedicalHistoryController.php +++ b/app/Http/Controllers/MedicalHistoryController.php @@ -4,7 +4,10 @@ namespace App\Http\Controllers; use App\Http\Resources\ArchiveHistoryResource; use App\Http\Resources\ArchiveInfoResource; -use App\Models\SI\SttMedicalHistory; +use App\Http\Resources\PatientInfoResource; +use App\Models\SI\SttMedicalHistory as SiSttMedicalHistory; +use App\Models\Mis\SttMedicalHistory as MisSttMedicalHistory; +use App\Repositories\MedicalHistoryRepository; use Illuminate\Http\Request; class MedicalHistoryController extends Controller @@ -15,22 +18,27 @@ class MedicalHistoryController extends Controller $patientId = $request->get('patient_id'); $patientInfo = null; - if ($viewType == 'si') { - $patient = SttMedicalHistory::where('id', $id)->first(); - $archiveJournal = ArchiveHistoryResource::collection($patient->archiveHistory); - $archiveInfo = $patient->archiveInfo; + if ($viewType == 'si') $patient = SiSttMedicalHistory::where('id', $id)->first(); + else $patient = MisSttMedicalHistory::where('MedicalHistoryID', $id)->first(); + $archiveJournal = $patient->archiveHistory ? ArchiveHistoryResource::collection($patient->archiveHistory) : null; - if (!empty($archiveInfo)) { - $archiveInfo = ArchiveInfoResource::make($patient->archiveInfo); - } - - $patientInfo = [ - 'info' => $patient, - 'journal' => $archiveJournal, - 'archiveInfo' => $archiveInfo, - ]; + if (!empty($patient->archiveInfo)) { + $archiveInfo = ArchiveInfoResource::make($patient->archiveInfo)->toArray(request()); + } else { + $archiveInfo = null; } + $patientInfo = [ + 'historyable_type' => $viewType == 'si' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class, + 'info' => [ + 'historyable_type' => $viewType == 'si' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class, + ...PatientInfoResource::make($patient)->toArray(request()), + 'can_be_issued' => $patient->canBeIssued() + ], + 'journal' => $archiveJournal, + 'archiveInfo' => $archiveInfo + ]; + return response()->json($patientInfo); } } diff --git a/app/Http/Resources/ArchiveHistoryResource.php b/app/Http/Resources/ArchiveHistoryResource.php index 63c6042..05fa35c 100644 --- a/app/Http/Resources/ArchiveHistoryResource.php +++ b/app/Http/Resources/ArchiveHistoryResource.php @@ -18,7 +18,7 @@ class ArchiveHistoryResource extends JsonResource return [ 'id' => $this->id, 'issue_at' => Carbon::parse($this->issue_at)->format('d.m.Y'), - 'return_at' => Carbon::parse($this->return_at)->format('d.m.Y'), + '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, diff --git a/app/Http/Resources/ArchiveInfoResource.php b/app/Http/Resources/ArchiveInfoResource.php index 4bbb1f7..10b6731 100644 --- a/app/Http/Resources/ArchiveInfoResource.php +++ b/app/Http/Resources/ArchiveInfoResource.php @@ -18,7 +18,9 @@ class ArchiveInfoResource extends JsonResource 'id' => $this->id, 'num' => $this->num, 'post_in' => $this->post_in, - 'status' => $this->status + 'status' => $this->status, + 'historyable_id' => $this->historyable_id, + 'historyable_type' => $this->historyable_type, ]; } } diff --git a/app/Http/Resources/Mis/SttMedicalHistoryResource.php b/app/Http/Resources/Mis/SttMedicalHistoryResource.php new file mode 100644 index 0000000..638eae3 --- /dev/null +++ b/app/Http/Resources/Mis/SttMedicalHistoryResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + 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->archiveInfo?->post_at ? Carbon::parse($this->archiveInfo->post_at)->format('d.m.Y') : null, + 'medcardnum' => $this->MedCardNum, + 'dr' => Carbon::parse($this->BD)->format('d.m.Y'), + 'can_be_issue' => $this->canBeIssued() + ]; + } +} diff --git a/app/Http/Resources/PatientInfoResource.php b/app/Http/Resources/PatientInfoResource.php new file mode 100644 index 0000000..7abe469 --- /dev/null +++ b/app/Http/Resources/PatientInfoResource.php @@ -0,0 +1,25 @@ + + */ + 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, + 'ot' => $this->ot ?? $this->OT, + ]; + } +} diff --git a/app/Http/Resources/SI/SttMedicalHistoryResource.php b/app/Http/Resources/SI/SttMedicalHistoryResource.php index 1b5c7af..3b0522a 100644 --- a/app/Http/Resources/SI/SttMedicalHistoryResource.php +++ b/app/Http/Resources/SI/SttMedicalHistoryResource.php @@ -22,11 +22,10 @@ class SttMedicalHistoryResource extends JsonResource 'dateextract' => Carbon::parse($this->dateextract)->format('d.m.Y'), 'card_num' => $this->archiveInfo->num ?? null, 'status' => $this->archiveInfo->status ?? null, - 'datearhiv' => Carbon::parse($this->datearhiv)->format('d.m.Y'), - 'statgod' => $this->statgod, - 'enp' => $this->enp, + 'datearhiv' => $this->archiveInfo?->post_at ? Carbon::parse($this->archiveInfo->post_at)->format('d.m.Y') : null, 'medcardnum' => $this->medcardnum, 'dr' => Carbon::parse($this->dr)->format('d.m.Y'), + 'can_be_issue' => $this->canBeIssued() ]; } } diff --git a/app/Models/ArchiveHistory.php b/app/Models/ArchiveHistory.php index 3ca13ca..68ebc2c 100644 --- a/app/Models/ArchiveHistory.php +++ b/app/Models/ArchiveHistory.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; class ArchiveHistory extends Model { + protected $connection = 'pgsql'; + protected static function booted() { static::created(function ($archiveHistory) { diff --git a/app/Models/ArchiveInfo.php b/app/Models/ArchiveInfo.php index 3210de4..4e5f44d 100644 --- a/app/Models/ArchiveInfo.php +++ b/app/Models/ArchiveInfo.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; class ArchiveInfo extends Model { + protected $connection = 'pgsql'; + protected $table = 'archive_infos'; protected $fillable = [ 'historyable_type', 'historyable_id', diff --git a/app/Models/Mis/SttMedicalHistory.php b/app/Models/Mis/SttMedicalHistory.php new file mode 100644 index 0000000..06e7212 --- /dev/null +++ b/app/Models/Mis/SttMedicalHistory.php @@ -0,0 +1,58 @@ +FAMILY $this->Name $this->OT"; + } + + public function archiveHistory() + { + return $this->morphMany(ArchiveHistory::class, 'historyable'); + } + + public function archiveInfo() + { + return $this->morphOne(ArchiveInfo::class, 'historyable'); + } + + /** + * Проверяет, можно ли выдать эту карту + */ + public function canBeIssued(): bool + { + // Проверяем, есть ли открытые выдачи + $hasOpenIssue = $this->archiveHistory() + ->whereNotNull('issue_at') + ->whereNull('return_at') + ->where('has_lost', false) + ->exists(); + + return !$hasOpenIssue; + } + + /** + * Получает текущую открытую выдачу (если есть) + */ + public function getCurrentIssue(): ?ArchiveHistory + { + return $this->archiveHistory() + ->whereNotNull('issue_at') + ->whereNull('return_at') + ->where('has_lost', false) + ->latest('issue_at') + ->first(); + } +} diff --git a/app/Models/SI/SttMedicalHistory.php b/app/Models/SI/SttMedicalHistory.php index e2f057c..66de56a 100644 --- a/app/Models/SI/SttMedicalHistory.php +++ b/app/Models/SI/SttMedicalHistory.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model; class SttMedicalHistory extends Model { protected $table = 'si_stt_patients'; + protected $connection = 'pgsql'; protected $fillable = [ 'family', // Фамилия @@ -39,6 +40,34 @@ class SttMedicalHistory extends Model return $this->morphOne(ArchiveInfo::class, 'historyable'); } + /** + * Проверяет, можно ли выдать эту карту + */ + public function canBeIssued(): bool + { + // Проверяем, есть ли открытые выдачи + $hasOpenIssue = $this->archiveHistory() + ->whereNotNull('issue_at') + ->whereNull('return_at') + ->where('has_lost', false) + ->exists(); + + return !$hasOpenIssue; + } + + /** + * Получает текущую открытую выдачу (если есть) + */ + public function getCurrentIssue(): ?ArchiveHistory + { + return $this->archiveHistory() + ->whereNotNull('issue_at') + ->whereNull('return_at') + ->where('has_lost', false) + ->latest('issue_at') + ->first(); + } + public function scopeSearch($query, $searchText) { return $query->where(function($q) use ($searchText) { diff --git a/app/Repositories/MedicalHistoryRepository.php b/app/Repositories/MedicalHistoryRepository.php new file mode 100644 index 0000000..0704fc6 --- /dev/null +++ b/app/Repositories/MedicalHistoryRepository.php @@ -0,0 +1,318 @@ +siModel = $siModel; + $this->misModel = $misModel; + } + + /** + * ПОИСК ТОЛЬКО В POSTGRESQL (основной) + */ + public function searchInPostgres( + ?string $searchText, + ?string $dateFrom, + ?string $dateTo, + ?int $status, + int $pageSize = 15, + string $sortBy = 'dateextract', + string $sortDir = 'desc', + ): LengthAwarePaginator { + + $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) + ->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) + ->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(); + + return new LengthAwarePaginator( + $allResults, + $pgPaginator->total() + $mssqlResults->count(), + $pageSize, + request()->get('page', 1) + ); + } + + return $pgPaginator; + } + + /** + * РАЗДЕЛЬНЫЙ ПОИСК (отдельные результаты по БД) + */ + public function separateSearch( + ?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); + + return [ + 'si' => $pgPaginator, + 'mis' => $mssqlPaginator, + 'stats' => [ + 'si_total' => $pgPaginator->total(), + 'mis_total' => $mssqlPaginator->total(), + 'combined_total' => $pgPaginator->total() + $mssqlPaginator->total(), + ] + ]; + } + + /** + * ПРИМЕНЕНИЕ УСЛОВИЙ ПОИСКА + */ + private function applySearchConditions($query, ?string $searchText, string $dbType = 'postgresql'): void + { + // Разбиваем поисковую строку на слова + $words = preg_split('/\s+/', trim($searchText)); + $words = array_filter($words); + + if (empty($words)) { + return; + } + + $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($query, ?int $value) + { + if ($query->withExists('archiveInfo') && !empty($value)) { + $query->withWhereHas('archiveInfo', function ($q) use ($value) { + $q->where('status_id', '=', $value); + }); + } + } + + /** + * ФИЛЬТР ПО ДАТЕ + */ + private function applyDateFilter($query, string $dateField, ?string $dateFrom, ?string $dateTo): void + { + if (!empty($dateFrom)) { + $query->whereDate($dateField, '>=', $dateFrom); + } + + if (!empty($dateTo)) { + $query->whereDate($dateField, '<=', $dateTo); + } + } + + /** + * ПОЛУЧИТЬ СТАТИСТИКУ ПО БАЗАМ + */ + public function getDatabaseStats(): 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', + ] + ]; + } + + /** + * БЫСТРЫЙ ПОИСК ПО ТИПУ + */ + public function quickSearch(string $type, string $value): Collection + { + return match($type) { + 'medcard' => $this->searchByMedCard($value), + 'enp' => $this->searchByEnp($value), + 'fullname' => $this->searchByFullName($value), + default => collect(), + }; + } + + private function searchByMedCard(string $medCard): Collection + { + $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(); + } + + private function searchByFullName(string $name): Collection + { + $pattern = "%{$name}%"; + + $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'); + + $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'); + + return $pgResults->concat($mssqlResults) + ->sortBy('family') + ->values(); + } +} diff --git a/app/Rules/DateTimeOrStringOrNumber.php b/app/Rules/DateTimeOrStringOrNumber.php new file mode 100644 index 0000000..f239f92 --- /dev/null +++ b/app/Rules/DateTimeOrStringOrNumber.php @@ -0,0 +1,42 @@ + [ + 'mis' => [ + 'driver' => 'pgsql', + 'host' => env('MIS_DB_HOST', 'localhost'), + 'port' => env('MIS_DB_PORT', '5432'), + 'database' => env('MIS_DB_DATABASE', 'laravel'), + 'username' => env('MIS_DB_USERNAME', 'root'), + 'password' => env('MIS_DB_PASSWORD', ''), + 'charset' => env('MIS_DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + 'sqlite' => [ 'driver' => 'sqlite', 'url' => env('DB_URL'), diff --git a/database/seeders/ArchiveStatusSeeder.php b/database/seeders/ArchiveStatusSeeder.php index 9525fb6..cca34d4 100644 --- a/database/seeders/ArchiveStatusSeeder.php +++ b/database/seeders/ArchiveStatusSeeder.php @@ -27,5 +27,10 @@ class ArchiveStatusSeeder extends Seeder 'text' => 'Выдана', 'variant' => 'warning' ]); + + ArchiveStatus::create([ + 'text' => 'Утеряна', + 'variant' => 'error' + ]); } } diff --git a/resources/js/Composables/useMedicalHistoryFilter.js b/resources/js/Composables/useMedicalHistoryFilter.js index 732e57e..78f7f56 100644 --- a/resources/js/Composables/useMedicalHistoryFilter.js +++ b/resources/js/Composables/useMedicalHistoryFilter.js @@ -1,27 +1,66 @@ // composables/useMedicalHistoryFilter.js -import { router, usePage } from '@inertiajs/vue3' -import {ref, computed, watch} from 'vue' -import { stringifyQuery } from 'ufo' -import {format, isValid, parse, parseISO} from 'date-fns' -import {useDebounceFn} from "@vueuse/core"; +import { ref, computed, watch } from 'vue' +import { usePage, router } from '@inertiajs/vue3' +import { useDebounceFn } from '@vueuse/core' +import { format, parse, isValid, parseISO } from 'date-fns' -export const useMedicalHistoryFilter = (filters) => { +export const useMedicalHistoryFilter = (initialFilters = {}) => { const page = usePage() // Реактивные фильтры с начальными значениями из URL const filtersRef = ref({ - search: filters?.search || '', - date_extract_from: filters?.date_extract_from || null, - date_extract_to: filters?.date_extract_to || null, - page: filters?.page || 1, - page_size: filters?.page_size || 15, - sort_by: filters?.sort_by || 'date_extract', - sort_order: filters?.sort_order || 'desc', - view_type: filters?.view_type || 'archive' + search: initialFilters?.search || '', + date_extract_from: initialFilters?.date_extract_from || null, + date_extract_to: initialFilters?.date_extract_to || null, + page: initialFilters?.page || 1, + 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 + } + } }) - const meta = computed(() => 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) => { @@ -32,17 +71,8 @@ export const useMedicalHistoryFilter = (filters) => { return date } - // Навигация с фильтрами - const applyFilters = (updates = {}, resetPage = false) => { - // Обновляем фильтры - Object.assign(filtersRef.value, updates) - - // Если сбрасываем фильтры, обнуляем страницу - if (resetPage) { - filtersRef.value.page = 1 - } - - // Подготавливаем параметры для URL + // Подготовка параметров запроса + const prepareParams = () => { const params = { search: filtersRef.value.search || null, date_extract_from: formatDateForUrl(filtersRef.value.date_extract_from), @@ -52,19 +82,33 @@ export const useMedicalHistoryFilter = (filters) => { 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 } // Очищаем пустые значения - const cleanParams = Object.fromEntries( + return Object.fromEntries( Object.entries(params).filter(([_, value]) => value !== undefined && value !== null && value !== '' ) ) + } - const query = stringifyQuery(cleanParams) + // Навигация с фильтрами + const applyFilters = (updates = {}, resetPage = true) => { + // Обновляем фильтры + Object.assign(filtersRef.value, updates) + + // Если сбрасываем фильтры, обнуляем страницу + if (resetPage) { + filtersRef.value.page = 1 + } + + const params = prepareParams() + const query = new URLSearchParams(params).toString() isLoading.value = true - router.visit(`/${query ? `?${query}` : ''}`, { + router.get(`/${query ? `?${query}` : ''}`, params, { preserveState: true, preserveScroll: true, onFinish: () => { @@ -84,6 +128,10 @@ export const useMedicalHistoryFilter = (filters) => { debouncedSearch(value) } + const handleDatabaseChange = (database) => { + applyFilters({ database, page: 1 }, false) + } + // Конвертация строки даты в timestamp для NaiveUI const convertToTimestamp = (dateString) => { if (!dateString) return null @@ -128,12 +176,8 @@ export const useMedicalHistoryFilter = (filters) => { applyFilters(updates, true) } - const handleStatusChange = (status) => { - applyFilters({ status }, true) - } - - const handlePageChange = (page) => { - applyFilters({ page }) + const handlePageChange = (pageNum) => { + applyFilters({ page: pageNum }, false) } const handlePageSizeChange = (size) => { @@ -148,7 +192,14 @@ export const useMedicalHistoryFilter = (filters) => { applyFilters({ sort_by: sorter.columnKey, sort_order: sorter.order - }) + }, true) + } + + + const handleStatusChange = (status_id) => { + applyFilters({ + status: status_id + }, true) } const resetAllFilters = () => { @@ -158,34 +209,112 @@ export const useMedicalHistoryFilter = (filters) => { date_extract_to: null, page: 1, page_size: 15, - sort_by: 'created_at', + sort_by: 'dateextract', sort_order: 'desc', view_type: 'archive', + database: 'postgresql', } + dateRange.value = [null, null] applyFilters({}, true) } + // Быстрый поиск по типу (для тулбара) + const quickSearch = (type, value) => { + const params = { + type, + value, + database: filtersRef.value.database, + } + + router.get('/quick-search', params, { + preserveState: true, + preserveScroll: true, + }) + } + + // Приоритетный поиск + const prioritySearch = (searchTerm) => { + router.get('/priority-search', { + q: searchTerm + }, { + preserveState: true, + preserveScroll: true, + }) + } + // Активные фильтры для отображения const activeFilters = computed(() => { const active = [] + if (filtersRef.value.search) { - active.push({ key: 'search', label: `Поиск: ${filtersRef.value.search}` }) + active.push({ + key: 'search', + label: `Поиск: ${filtersRef.value.search}`, + icon: 'search' + }) } + if (filtersRef.value.date_extract_from || filtersRef.value.date_extract_to) { - const from = filtersRef.value.date_extract_from ? format(parseISO(filtersRef.value.date_extract_from), 'dd.MM.yyyy') : '' - const to = filtersRef.value.date_extract_to ? format(parseISO(filtersRef.value.date_extract_to), 'dd.MM.yyyy') : '' - active.push({ key: 'date', label: `Дата: ${from} - ${to}` }) + const from = filtersRef.value.date_extract_from + ? format(parseISO(filtersRef.value.date_extract_from), 'dd.MM.yyyy') + : '' + const to = filtersRef.value.date_extract_to + ? format(parseISO(filtersRef.value.date_extract_to), 'dd.MM.yyyy') + : '' + active.push({ + key: 'date', + label: `Дата выписки: ${from} ${to ? '- ' + to : ''}`, + icon: 'calendar' + }) } - // Добавьте другие фильтры по необходимости + + // Фильтр по базе данных + 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 }) + // Удаление конкретного фильтра + const removeFilter = (key) => { + const updates = {} + + switch (key) { + case 'search': + updates.search = '' + break + case 'date': + updates.date_extract_from = null + updates.date_extract_to = null + dateRange.value = [null, null] + break + case 'database': + updates.database = 'postgresql' + break + } + + Object.assign(filtersRef.value, updates) + applyFilters(updates, true) + } + // Следим за изменениями в page.props.filters watch( () => page.props.filters, (newFilters) => { if (newFilters) { - // Сохраняем все фильтры, включая search! + // Сохраняем все фильтры filtersRef.value = { ...filtersRef.value, ...newFilters @@ -209,14 +338,22 @@ export const useMedicalHistoryFilter = (filters) => { isLoading, activeFilters, dateRange, + databaseInfo, + databaseStats, + + // Обработчики + handleDatabaseChange, handleViewTypeChange, handleSearch, handleDateRangeChange, - handleStatusChange, handlePageChange, handlePageSizeChange, handleSortChange, + handleStatusChange, resetAllFilters, - applyFilters + removeFilter, + applyFilters, + quickSearch, + prioritySearch } } diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index 3aa0320..ce12593 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -19,19 +19,17 @@ const themeOverrides = {