Compare commits
34 Commits
54f36e91fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb36ef3a40 | ||
|
|
56be95caa4 | ||
| e263697f7d | |||
| c7dcfe1a51 | |||
| d7d959ed53 | |||
|
|
c5c1a2b3e1 | ||
|
|
329304076d | ||
|
|
a5209f45c8 | ||
|
|
c4bb7ec6f9 | ||
|
|
e6af72a778 | ||
|
|
eed6607296 | ||
|
|
255a117c20 | ||
|
|
2bafa7f073 | ||
|
|
9057d3e8ad | ||
|
|
b86cdaec90 | ||
|
|
0a1304e55b | ||
|
|
3f9d7fe30b | ||
|
|
cc10b54ece | ||
|
|
76d1df235d | ||
|
|
fdcfaec862 | ||
|
|
d15491097c | ||
|
|
d1df87b817 | ||
|
|
84f4cbb273 | ||
|
|
6b670c4d29 | ||
|
|
3f4a0d4c59 | ||
|
|
2e31421d73 | ||
|
|
1e9f5cb0ce | ||
|
|
1aaa459a5f | ||
|
|
6ee36d4ca1 | ||
|
|
640ff50d7f | ||
|
|
435d4e16be | ||
|
|
ab12a692fa | ||
|
|
cb2394ebba | ||
|
|
98e9f8b52e |
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
||||
# .dockerignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.example
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
/.idea
|
||||
/.vscode
|
||||
.git
|
||||
103
.github/workflows/build-docker.yml
vendored
Normal file
103
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
env:
|
||||
REGISTRY: registry.brusoff.su
|
||||
IMAGE_NAME: ${{ gitea.repository }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql, pdo_pgsql, gd, redis, zip
|
||||
coverage: none
|
||||
|
||||
- name: Validate composer.json
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Run PHP tests
|
||||
run: vendor/bin/phpunit
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
|
||||
- name: Scan image for vulnerabilities
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
|
||||
- name: Upload Trivy scan results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
207
Dockerfile
Normal file
207
Dockerfile
Normal file
@@ -0,0 +1,207 @@
|
||||
# Этап 1: PHP зависимости
|
||||
FROM dh-mirror.gitverse.ru/php:8.3-fpm AS phpbuild
|
||||
|
||||
# Установка системных зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libxml2-dev \
|
||||
libicu-dev \
|
||||
libonig-dev \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
libwebp-dev \
|
||||
libpq-dev \
|
||||
libxslt1-dev \
|
||||
libexif-dev \
|
||||
libffi-dev \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Установка PHP расширений
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype \
|
||||
--with-jpeg \
|
||||
--with-webp \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
bcmath \
|
||||
intl \
|
||||
mbstring \
|
||||
zip \
|
||||
opcache \
|
||||
pdo \
|
||||
pdo_mysql \
|
||||
pdo_pgsql \
|
||||
gd \
|
||||
exif \
|
||||
sockets \
|
||||
xsl \
|
||||
ffi \
|
||||
pcntl
|
||||
|
||||
# Установка Redis расширения
|
||||
RUN pecl install redis && docker-php-ext-enable redis
|
||||
|
||||
# Настройка opcache для production
|
||||
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.interned_strings_buffer=32" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.max_accelerated_files=32531" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini
|
||||
|
||||
# Установка Composer
|
||||
COPY --from=dh-mirror.gitverse.ru/composer:2.7 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
# Копирование файлов для установки зависимостей
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
# Установка PHP зависимостей
|
||||
RUN composer install \
|
||||
--no-progress \
|
||||
--no-scripts \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader \
|
||||
--apcu-autoloader
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Установка прав
|
||||
RUN chown -R www-data:www-data /var/www && \
|
||||
chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
||||
|
||||
# Этап 2: Сборка фронтенда (Inertia + Vue 3)
|
||||
FROM dh-mirror.gitverse.ru/node:20 AS jsbuild
|
||||
|
||||
# Установка дополнительных зависимостей для сборки
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
# Копируем зависимости Node.js
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Установка зависимостей Node.js
|
||||
RUN npm ci \
|
||||
--no-audit \
|
||||
--progress=false
|
||||
|
||||
# Копируем файлы для сборки фронтенда
|
||||
COPY vite.config.js ./
|
||||
COPY resources/js ./resources/js/
|
||||
COPY resources/css ./resources/css/
|
||||
|
||||
# Сборка ассетов Vite
|
||||
RUN npm run build
|
||||
|
||||
# Этап 3: Финальный образ
|
||||
FROM dh-mirror.gitverse.ru/php:8.3-fpm
|
||||
|
||||
# Установка runtime зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libxml2 \
|
||||
libonig5 \
|
||||
libpng16-16 \
|
||||
libjpeg62-turbo \
|
||||
libfreetype6 \
|
||||
libwebp7 \
|
||||
libpq5 \
|
||||
libxslt1.1 \
|
||||
libexif12 \
|
||||
libffi8 \
|
||||
supervisor \
|
||||
nginx \
|
||||
cron \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Установка системных зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libxml2-dev \
|
||||
libicu-dev \
|
||||
libonig-dev \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
libwebp-dev \
|
||||
libpq-dev \
|
||||
libxslt1-dev \
|
||||
libexif-dev \
|
||||
libffi-dev \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Установка PHP расширений
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype \
|
||||
--with-jpeg \
|
||||
--with-webp \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
bcmath \
|
||||
intl \
|
||||
mbstring \
|
||||
zip \
|
||||
opcache \
|
||||
pdo \
|
||||
pdo_mysql \
|
||||
pdo_pgsql \
|
||||
gd \
|
||||
exif \
|
||||
sockets \
|
||||
xsl \
|
||||
ffi \
|
||||
pcntl
|
||||
|
||||
# Установка Redis расширения
|
||||
RUN pecl install redis && docker-php-ext-enable redis
|
||||
|
||||
# Настройка opcache для production
|
||||
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.interned_strings_buffer=32" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.max_accelerated_files=32531" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||
echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini
|
||||
|
||||
# Копируем PHP расширения из первого этапа
|
||||
#COPY --from=phpbuild /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
|
||||
#COPY --from=phpbuild /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ /usr/local/lib/php/extensions/no-debug-non-zts-20230831/
|
||||
#COPY --from=phpbuild /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
# Копируем конфигурации
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/app.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
# Копируем приложение
|
||||
COPY --chown=www-data:www-data --from=phpbuild /var/www .
|
||||
COPY --chown=www-data:www-data --from=jsbuild /var/www/public/build ./public/build
|
||||
COPY --chown=www-data:www-data --from=jsbuild /var/www/node_modules ./node_modules
|
||||
|
||||
# Настройка прав и оптимизация Laravel
|
||||
RUN mkdir -p /var/log/supervisor && \
|
||||
chown -R www-data:www-data /var/www /var/log/supervisor && \
|
||||
chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
||||
|
||||
# Создание ссылки на Storage
|
||||
RUN php artisan storage:link
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
9
app/Contracts/MedicalHistoryRepositoryInterface.php
Normal file
9
app/Contracts/MedicalHistoryRepositoryInterface.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface MedicalHistoryRepositoryInterface
|
||||
{
|
||||
public function get(array $filters);
|
||||
public function search(string $term);
|
||||
}
|
||||
@@ -3,7 +3,11 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
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;
|
||||
|
||||
@@ -18,36 +22,131 @@ class ArchiveHistoryController extends Controller
|
||||
{
|
||||
$archiveHistory = ArchiveHistory::find($id);
|
||||
|
||||
return response()->json($archiveHistory);
|
||||
return response()->json([
|
||||
...$archiveHistory->toArray(),
|
||||
'type' => $archiveHistory->historyType()
|
||||
]);
|
||||
}
|
||||
|
||||
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',
|
||||
'history_id' => 'required',
|
||||
'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');
|
||||
$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 ($data['type'] === 'mis') {
|
||||
$patient = \App\Models\Mis\SttMedicalHistory::where('MedicalHistoryID', $data['history_id'])->first();
|
||||
} else {
|
||||
$patient = \App\Models\Si\SttMedicalHistory::where('keykarta', $data['history_id'])->first();
|
||||
}
|
||||
|
||||
// $archiveInfo = ArchiveInfo::whereId($data['archive_info_id'])->first();
|
||||
|
||||
if ($data['type'] === 'mis') {
|
||||
$archiveHistory = $patient->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' => $patient->MedicalHistoryID
|
||||
]);
|
||||
} else {
|
||||
$archiveHistory = $patient->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' => $patient->keykarta,
|
||||
]);
|
||||
}
|
||||
|
||||
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',
|
||||
'type' => 'required|string',
|
||||
]);
|
||||
|
||||
// Преобразуем timestamp в дату, если пришли числа
|
||||
if (isset($data['issue_at']) && is_numeric($data['issue_at'])) {
|
||||
$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'])
|
||||
->setTimezone(config('app.timezone'))
|
||||
->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],
|
||||
'status' => 'nullable',
|
||||
'type' => 'nullable'
|
||||
]);
|
||||
|
||||
// Преобразуем timestamp в дату, если пришли числа
|
||||
if (isset($data['post_in']) && is_numeric($data['post_in'])) {
|
||||
$data['post_in'] = Carbon::createFromTimestampMs($data['post_in'])->format('Y-m-d');
|
||||
}
|
||||
|
||||
$archiveInfo = ArchiveInfo::whereId($data['id'])->first();
|
||||
|
||||
$archiveInfo->update(
|
||||
[
|
||||
'archive_num' => $data['num'],
|
||||
'post_in' => $data['post_in'],
|
||||
]
|
||||
);
|
||||
|
||||
return response()->json()->setStatusCode(200);
|
||||
}
|
||||
}
|
||||
|
||||
38
app/Http/Controllers/ArchiveInfoController.php
Normal file
38
app/Http/Controllers/ArchiveInfoController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ArchiveInfo;
|
||||
use App\Models\Mis\SttMedicalHistory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ArchiveInfoController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'id' => 'required',
|
||||
'num' => 'required',
|
||||
'post_in' => 'required'
|
||||
]);
|
||||
|
||||
// Преобразуем timestamp в дату, если пришли числа
|
||||
if (isset($data['post_in']) && is_numeric($data['post_in'])) {
|
||||
$data['post_in'] = Carbon::createFromTimestampMs($data['post_in'])
|
||||
->setTimezone(config('app.timezone'))
|
||||
->format('Y-m-d');
|
||||
}
|
||||
|
||||
$history = SttMedicalHistory::where('MedicalHistoryID', $data['id'])->first();
|
||||
|
||||
$hasCreated = ArchiveInfo::create([
|
||||
'mis_num' => $history->MedCardNum,
|
||||
'mis_history_id' => $data['id'],
|
||||
'archive_num' => $data['num'],
|
||||
'post_in' => $data['post_in'],
|
||||
]);
|
||||
|
||||
return $hasCreated;
|
||||
}
|
||||
}
|
||||
@@ -2,41 +2,68 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\SI\SttMedicalHistoryResource;
|
||||
use App\Http\Resources\IndexSttMedicalHistoryResource;
|
||||
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 App\Services\ArchiveCardService;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class IndexController extends Controller
|
||||
{
|
||||
private MedicalHistoryRepository $repository;
|
||||
private ArchiveCardService $cardService;
|
||||
|
||||
public function __construct(MedicalHistoryRepository $repository, ArchiveCardService $cardService)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->cardService = $cardService;
|
||||
}
|
||||
|
||||
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');
|
||||
$status = $request->get('status', null);
|
||||
|
||||
$cardsQuery = SttMedicalHistory::query();
|
||||
// $data = $this->repository->unifiedSearch(
|
||||
// $searchText,
|
||||
// $dateExtractFrom,
|
||||
// $dateExtractTo,
|
||||
// $status,
|
||||
// $pageSize
|
||||
// );
|
||||
|
||||
if (!empty($searchText)) {
|
||||
$cardsQuery = $cardsQuery->search($searchText);
|
||||
}
|
||||
$data = $this->cardService->get(
|
||||
$searchText,
|
||||
$dateExtractFrom,
|
||||
$dateExtractTo,
|
||||
$status,
|
||||
$pageSize
|
||||
);
|
||||
|
||||
if (!empty($dateExtractFrom)) {
|
||||
$cardsQuery = $cardsQuery->whereDate('dateextract', '>=', $dateExtractFrom);
|
||||
if (!empty($dateExtractTo)) {
|
||||
$cardsQuery = $cardsQuery->whereDate('dateextract', '<=', $dateExtractTo);
|
||||
}
|
||||
}
|
||||
// dd($data);
|
||||
|
||||
$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' => IndexSttMedicalHistoryResource::collection($data),
|
||||
'statuses' => $statuses,
|
||||
'filters' => array_merge($request->only([
|
||||
'search', 'date_extract_from', 'date_extract_to',
|
||||
'page_size', 'page', 'status'
|
||||
]))
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,49 @@ 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\Http\Resources\SI\SttMedicalHistoryResource;
|
||||
use App\Models\ArchiveInfo;
|
||||
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
|
||||
{
|
||||
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 = SttMedicalHistory::where('id', $id)->first();
|
||||
$archiveJournal = ArchiveHistoryResource::collection($patient->archiveHistory);
|
||||
$archiveInfo = $patient->archiveInfo;
|
||||
if ($viewType == 'foxpro') $patient = SiSttMedicalHistory::where('keykarta', $id)->first();
|
||||
else $patient = MisSttMedicalHistory::where('MedicalHistoryID', $id)->first();
|
||||
|
||||
if (!empty($archiveInfo)) {
|
||||
$archiveInfo = ArchiveInfoResource::make($patient->archiveInfo);
|
||||
if($patient instanceof MisSttMedicalHistory) {
|
||||
if ($patient->archiveHistory->count() === 0) {
|
||||
$foxproCardId = $patient->archiveInfo->foxpro_history_id;
|
||||
$foxproPatient = SiSttMedicalHistory::where('keykarta', $foxproCardId)->first();
|
||||
|
||||
$journalHistory = $foxproPatient->archiveHistory;
|
||||
} else {
|
||||
$journalHistory = $patient->archiveHistory;
|
||||
}
|
||||
} else {
|
||||
$journalHistory = $patient->archiveHistory;
|
||||
}
|
||||
|
||||
$archiveInfo = $patient->archiveInfo ? $patient->archiveInfo : null;
|
||||
|
||||
$patientInfo = [
|
||||
'info' => $patient,
|
||||
'journal' => $archiveJournal,
|
||||
'archiveInfo' => $archiveInfo,
|
||||
'historyable_type' => $viewType == 'foxpro' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class,
|
||||
'info' => [
|
||||
'historyable_type' => $viewType == 'foxpro' ? SiSttMedicalHistory::class : MisSttMedicalHistory::class,
|
||||
...PatientInfoResource::make($patient)->toArray(request()),
|
||||
'can_be_issued' => $patient->canBeIssued()
|
||||
],
|
||||
'journal' => ArchiveHistoryResource::collection($journalHistory),
|
||||
'archiveInfo' => $archiveInfo ? ArchiveInfoResource::make($archiveInfo) : null
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json($patientInfo);
|
||||
}
|
||||
|
||||
74
app/Http/Controllers/MisHistoryController.php
Normal file
74
app/Http/Controllers/MisHistoryController.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Mis\SttMedicalHistory;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MisHistoryController extends Controller
|
||||
{
|
||||
public function search(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'query' => 'required|string'
|
||||
]);
|
||||
|
||||
$searchText = $data['query'];
|
||||
$searchParts = preg_split('/\s+/', trim($searchText));
|
||||
|
||||
$query = SttMedicalHistory::query();
|
||||
|
||||
// Проверяем, начинается ли строка с цифры (вероятно, это номер карты)
|
||||
$isCardNumberSearch = is_numeric($searchParts[0]);
|
||||
|
||||
if ($isCardNumberSearch) {
|
||||
// Паттерн: № Ф И О
|
||||
$query->where('MedCardNum', 'ILIKE', "$searchParts[0]%");
|
||||
|
||||
// ФИО начинается со второй части
|
||||
$fioStartIndex = 1;
|
||||
} else {
|
||||
// Паттерн: Ф И О (без номера)
|
||||
$fioStartIndex = 0;
|
||||
}
|
||||
|
||||
// Ищем ФИО в зависимости от количества оставшихся частей
|
||||
$fioPartsCount = count($searchParts) - $fioStartIndex;
|
||||
|
||||
if ($fioPartsCount === 1) {
|
||||
// Одно слово - ищем в фамилии, имени или отчестве
|
||||
$query->where(function ($q) use ($searchParts, $fioStartIndex) {
|
||||
$q->where('FAMILY', 'ILIKE', "{$searchParts[$fioStartIndex]}%")
|
||||
->orWhere('Name', 'ILIKE', "{$searchParts[$fioStartIndex]}%")
|
||||
->orWhere('OT', 'ILIKE', "{$searchParts[$fioStartIndex]}%");
|
||||
});
|
||||
} elseif ($fioPartsCount === 2) {
|
||||
// Два слова - фамилия и инициал имени
|
||||
$query->where(function ($q) use ($searchParts, $fioStartIndex) {
|
||||
$q->where('FAMILY', 'ILIKE', "{$searchParts[$fioStartIndex]}%")
|
||||
->where('Name', 'ILIKE', "{$searchParts[$fioStartIndex + 1]}%");
|
||||
});
|
||||
} elseif ($fioPartsCount === 3) {
|
||||
// Три слова - полное ФИО
|
||||
$query->where(function ($q) use ($searchParts, $fioStartIndex) {
|
||||
$q->where('FAMILY', 'ILIKE', "{$searchParts[$fioStartIndex]}%")
|
||||
->where('Name', 'ILIKE', "{$searchParts[$fioStartIndex + 1]}%")
|
||||
->where('OT', 'ILIKE', "{$searchParts[$fioStartIndex + 2]}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Также ищем полную строку в объединенных полях
|
||||
$query->orWhereRaw("CONCAT(\"MedCardNum\", ' ', \"FAMILY\", ' ', \"Name\", ' ', \"OT\") ILIKE ?", ["{$searchText}%"]);
|
||||
|
||||
$results = $query->select([
|
||||
'MedicalHistoryID', 'MedCardNum', 'FAMILY', 'Name', 'OT'
|
||||
])->get()->map(function ($item) {
|
||||
return [
|
||||
'label' => "$item->MedCardNum - $item->FAMILY $item->Name $item->OT",
|
||||
'value' => $item->MedicalHistoryID
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($results);
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ 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,
|
||||
'org' => $this->org?->name,
|
||||
'employee_name' => $this->employee_name,
|
||||
'employee_post' => $this->employee_post,
|
||||
'has_lost' => $this->has_lost,
|
||||
|
||||
@@ -16,9 +16,11 @@ class ArchiveInfoResource extends JsonResource
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'num' => $this->num,
|
||||
'num' => $this->archive_num,
|
||||
'post_in' => $this->post_in,
|
||||
'status' => $this->status
|
||||
'status' => $this->status,
|
||||
'foxpro_num' => $this->foxpro_num,
|
||||
'mis_num' => $this->mis_num,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
85
app/Http/Resources/IndexSttMedicalHistoryResource.php
Normal file
85
app/Http/Resources/IndexSttMedicalHistoryResource.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\ArchiveStatus;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class IndexSttMedicalHistoryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
// Получаем оригинальную модель
|
||||
$model = $this->resource;
|
||||
|
||||
$archiveInfo = $model->archiveInfo;
|
||||
$historyType = $archiveInfo->historyType();
|
||||
|
||||
$inArchive = $archiveInfo->exists;
|
||||
$hasMisType = $historyType === 'mis';
|
||||
|
||||
$family = $hasMisType ? $model->FAMILY : $model->fam;
|
||||
$im = $hasMisType ? $this->Name : $this->im;
|
||||
$ot = $hasMisType ? $this->OT : $this->ot;
|
||||
|
||||
$fio = "$family $im $ot";
|
||||
|
||||
$birthDate = $hasMisType ? $this->BD : $this->dr;
|
||||
$dateRecipient = $hasMisType ? $this->DateRecipient : $this->mpostdate;
|
||||
$dateExtract = $hasMisType ? $this->DateExtract : $this->menddate;
|
||||
$postIn = $inArchive ? $archiveInfo->post_in : null;
|
||||
|
||||
$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;
|
||||
|
||||
$id = $historyType === 'mis' ? $this->MedicalHistoryID : $this->keykarta;
|
||||
$archiveNum = $inArchive ? $archiveInfo->archive_num : null;
|
||||
$status = $inArchive ? $archiveInfo->status : ArchiveStatus::find(5)->first();
|
||||
|
||||
$misCardNumber = $this->MedCardNum;
|
||||
$foxproCardNumber = $this->nkarta;
|
||||
|
||||
$archiveInfoMisCardNumber = $inArchive ? $archiveInfo->mis_num : null;
|
||||
$archiveInfoFoxproCardNumber = $inArchive ? $archiveInfo->foxpro_num : null;
|
||||
|
||||
$hasDividerCardNumber = isset($archiveInfoMisCardNumber) && isset($archiveInfoFoxproCardNumber);
|
||||
|
||||
$cardNumber = $hasDividerCardNumber
|
||||
? "$archiveInfoMisCardNumber / $archiveInfoFoxproCardNumber"
|
||||
: $archiveInfoMisCardNumber ?? $archiveInfoFoxproCardNumber;
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'history_type' => $historyType,
|
||||
|
||||
// Основные данные
|
||||
'fullname' => $fio,
|
||||
'family' => $family,
|
||||
'name' => $im,
|
||||
'ot' => $ot,
|
||||
'dr' => $formattedBirthDate,
|
||||
'daterecipient' => $formattedDateRecipient,
|
||||
'dateextract' => $formattedDateExtract,
|
||||
|
||||
// Номера карт
|
||||
'medcardnum' => $cardNumber, // MIS номер или 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
128
app/Http/Resources/Mis/SttMedicalHistoryResource.php
Normal file
128
app/Http/Resources/Mis/SttMedicalHistoryResource.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Mis;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class SttMedicalHistoryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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'] ?? '';
|
||||
|
||||
$misCardNumber = $this->resource['mis_card_number'];
|
||||
$foxproCardNumber = $this->resource['foxpro_card_number'];
|
||||
|
||||
$hasDividerCardNumber = isset($misCardNumber) && isset($foxproCardNumber);
|
||||
|
||||
$cardNumber = $hasDividerCardNumber ? "$misCardNumber / $foxproCardNumber" : $misCardNumber ?? $foxproCardNumber;
|
||||
|
||||
// Для временных записей (не в архиве) используем данные из MIS
|
||||
if ($isTemporary) {
|
||||
// Данные из stt_medicalhistory (не в архиве)
|
||||
$fullName = trim("{$family} {$name} {$ot}");
|
||||
$birthDate = $this->resource['birth_date'] ?? null;
|
||||
$dateExtract = $this->resource['date_extract'] ?? null;
|
||||
$dateRecipient = $this->resource['date_recipient'] ?? 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;
|
||||
|
||||
$dateRecipient = $this->resource['date_recipient'] ?? 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;
|
||||
|
||||
$id = $historyType === 'mis' ? $this->resource['mis_history_id'] : $this->resource['foxpro_history_id'];
|
||||
|
||||
return [
|
||||
'id' => $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',
|
||||
};
|
||||
}
|
||||
}
|
||||
25
app/Http/Resources/PatientInfoResource.php
Normal file
25
app/Http/Resources/PatientInfoResource.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PatientInfoResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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->whenLoaded('archiveInfo') ? Carbon::parse($this->archiveInfo->post_in)->format('d.m.Y') : null,
|
||||
'medcardnum' => $this->medcardnum,
|
||||
'dr' => Carbon::parse($this->dr)->format('d.m.Y'),
|
||||
'can_be_issue' => $this->canBeIssued()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
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
|
||||
{
|
||||
protected $connection = 'pgsql';
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($archiveHistory) {
|
||||
@@ -19,16 +23,12 @@ class ArchiveHistory extends Model
|
||||
|
||||
public function updateArchiveInfoStatus()
|
||||
{
|
||||
// Получаем связанную модель через морф
|
||||
$historyable = $this->historyable;
|
||||
|
||||
if (!$historyable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли у модели архивная информация
|
||||
if (method_exists($historyable, 'archiveInfo') && $historyable->archiveInfo) {
|
||||
$historyable->archiveInfo->update([
|
||||
if ($this->mis_history_id) {
|
||||
$this->misHistory->archiveInfo->update([
|
||||
'status_id' => $this->determineStatusId()
|
||||
]);
|
||||
} else {
|
||||
$this->foxproHistory->archiveInfo->update([
|
||||
'status_id' => $this->determineStatusId()
|
||||
]);
|
||||
}
|
||||
@@ -52,8 +52,9 @@ class ArchiveHistory extends Model
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'historyable_type',
|
||||
'historyable_id',
|
||||
'keyarhiv',
|
||||
'foxpro_history_id',
|
||||
'mis_history_id',
|
||||
'issue_at',
|
||||
'return_at',
|
||||
'comment',
|
||||
@@ -63,9 +64,19 @@ class ArchiveHistory extends Model
|
||||
'has_lost',
|
||||
];
|
||||
|
||||
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 org(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
|
||||
@@ -2,21 +2,46 @@
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function (ArchiveInfo $model) {
|
||||
$model->status_id = 2;
|
||||
});
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
68
app/Models/Mis/SttMedicalHistory.php
Normal file
68
app/Models/Mis/SttMedicalHistory.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Mis;
|
||||
|
||||
use App\Models\ArchiveHistory;
|
||||
use App\Models\ArchiveInfo;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SttMedicalHistory extends Model
|
||||
{
|
||||
protected $primaryKey = 'MedicalHistoryID';
|
||||
protected $table = 'stt_medicalhistory';
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $casts = [
|
||||
'MedicalHistoryID' => 'string',
|
||||
];
|
||||
|
||||
public function getFullNameAttribute()
|
||||
{
|
||||
return "$this->FAMILY $this->Name $this->OT";
|
||||
}
|
||||
|
||||
public function archiveHistory()
|
||||
{
|
||||
return $this->hasMany(ArchiveHistory::class, 'mis_history_id', 'MedicalHistoryID');
|
||||
}
|
||||
|
||||
public function archiveInfo()
|
||||
{
|
||||
return $this->hasOne(ArchiveInfo::class, 'mis_history_id', 'MedicalHistoryID');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, можно ли выдать эту карту
|
||||
*/
|
||||
public function canBeIssued(): bool
|
||||
{
|
||||
// Проверяем, есть ли открытые выдачи
|
||||
$hasOpenIssue = $this->archiveHistory()
|
||||
->whereNotNull('issue_at')
|
||||
->whereNull('return_at')
|
||||
->where('has_lost', false)
|
||||
->exists();
|
||||
|
||||
$hasNotBadStatus = $this->archiveInfo()
|
||||
->whereNotNull('status_id')
|
||||
->whereNot('status_id', 3)
|
||||
->whereNot('status_id', 4)
|
||||
->exists();
|
||||
|
||||
return ($hasOpenIssue !== true && $hasNotBadStatus === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущую открытую выдачу (если есть)
|
||||
*/
|
||||
public function getCurrentIssue(): ?ArchiveHistory
|
||||
{
|
||||
return $this->archiveHistory()
|
||||
->whereNotNull('issue_at')
|
||||
->whereNull('return_at')
|
||||
->where('has_lost', false)
|
||||
->latest('issue_at')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -8,35 +8,78 @@ 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, можно ли выдать эту карту
|
||||
*/
|
||||
public function canBeIssued(): bool
|
||||
{
|
||||
// Проверяем, есть ли открытые выдачи
|
||||
$hasOpenIssue = $this->archiveHistory()
|
||||
->whereNotNull('issue_at')
|
||||
->whereNull('return_at')
|
||||
->where('has_lost', false)
|
||||
->exists();
|
||||
|
||||
$hasNotBadStatus = $this->archiveInfo()
|
||||
->whereNotNull('status_id')
|
||||
->whereNot('status_id', 1)
|
||||
->whereNot('status_id', 3)
|
||||
->whereNot('status_id', 4)
|
||||
->exists();
|
||||
|
||||
return ($hasOpenIssue !== true && $hasNotBadStatus === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущую открытую выдачу (если есть)
|
||||
*/
|
||||
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)
|
||||
|
||||
948
app/Repositories/MedicalHistoryRepository.php
Normal file
948
app/Repositories/MedicalHistoryRepository.php
Normal file
@@ -0,0 +1,948 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MedicalHistoryRepository
|
||||
{
|
||||
/**
|
||||
* Основной метод поиска через RAW SQL
|
||||
*/
|
||||
public function unifiedSearch(
|
||||
?string $searchText,
|
||||
?string $dateFrom,
|
||||
?string $dateTo,
|
||||
?int $status,
|
||||
int $pageSize = 50
|
||||
): LengthAwarePaginator
|
||||
{
|
||||
$page = request()->get('page', 1);
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
// 1. Получаем только ID и минимальные данные для пагинации
|
||||
$idQuery = $this->buildPaginatedIdQuery($searchText, $dateFrom, $dateTo, $status, $pageSize, $offset);
|
||||
$idResults = DB::select($idQuery['sql'], $idQuery['params']);
|
||||
|
||||
if (empty($idResults)) {
|
||||
return new LengthAwarePaginator(
|
||||
collect(),
|
||||
0,
|
||||
$pageSize,
|
||||
$page,
|
||||
['path' => request()->url(), 'query' => request()->query()]
|
||||
);
|
||||
}
|
||||
|
||||
// 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 с пагинацией
|
||||
*/
|
||||
private function buildPaginatedIdQuery(
|
||||
?string $searchText,
|
||||
?string $dateFrom,
|
||||
?string $dateTo,
|
||||
?int $status,
|
||||
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\"
|
||||
) AND mh.\"DateExtract\" > CAST('01-01-1900' as date)
|
||||
";
|
||||
|
||||
$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 [
|
||||
'conditions' => $conditions,
|
||||
'params' => $params
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Условия для MIS записей не в архиве
|
||||
*/
|
||||
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 $conditions;
|
||||
}
|
||||
|
||||
// Если одно слово - ищем по всем полям
|
||||
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 getArchiveDataByIds(array $ids): array
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$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.\"DateRecipient\", fp.mpostdate) as date_recipient,
|
||||
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
|
||||
*/
|
||||
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.\"DateRecipient\" as date_recipient,
|
||||
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})
|
||||
AND mh.\"DateExtract\" > CAST('01-01-1900' AS DATE)
|
||||
";
|
||||
|
||||
$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'],
|
||||
["mh.\"DateExtract\" > CAST('01-01-1900' AS DATE)"]
|
||||
);
|
||||
|
||||
$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 [
|
||||
'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_recipient' => $item->date_recipient,
|
||||
'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),
|
||||
];
|
||||
}
|
||||
|
||||
private function getStatusText(int $statusId, bool $inArchive): string
|
||||
{
|
||||
if (!$inArchive) {
|
||||
return 'Не в архиве';
|
||||
}
|
||||
|
||||
return match($statusId) {
|
||||
1 => 'В архиве',
|
||||
2 => 'Выдано',
|
||||
3 => 'Утрачено',
|
||||
4 => 'Списано',
|
||||
default => 'Неизвестно',
|
||||
};
|
||||
}
|
||||
|
||||
private function canBeIssued(int $statusId, bool $inArchive): bool
|
||||
{
|
||||
return $inArchive && $statusId === 1;
|
||||
}
|
||||
|
||||
private function getRowClass(int $statusId, bool $inArchive): string
|
||||
{
|
||||
if (!$inArchive) {
|
||||
return 'table-warning';
|
||||
}
|
||||
|
||||
return match($statusId) {
|
||||
1 => 'table-success',
|
||||
2 => 'table-info',
|
||||
3, 4 => 'table-danger',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Быстрый поиск для автокомплита
|
||||
*/
|
||||
public function quickSearch(string $searchText, int $limit = 20): array
|
||||
{
|
||||
if (is_numeric($searchText)) {
|
||||
return $this->quickSearchByCardNumber($searchText, $limit);
|
||||
}
|
||||
|
||||
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.\"DateRecipient\", fp.mpostdate) as date_recipient,
|
||||
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.\"DateRecipient\" as date_recipient,
|
||||
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;
|
||||
}
|
||||
}
|
||||
42
app/Rules/DateTimeOrStringOrNumber.php
Normal file
42
app/Rules/DateTimeOrStringOrNumber.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class DateTimeOrStringOrNumber implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
// Если null - пропускаем
|
||||
if (is_null($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Если число
|
||||
if (is_numeric($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Если строка
|
||||
if (is_string($value)) {
|
||||
// Проверяем, можно ли преобразовать в дату
|
||||
try {
|
||||
Carbon::parse($value);
|
||||
return;
|
||||
} catch (\Exception $e) {
|
||||
// Если не дата, но это строка - все равно ок
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$fail('Ошибка при проверки даты');
|
||||
}
|
||||
}
|
||||
140
app/Services/ArchiveCardService.php
Normal file
140
app/Services/ArchiveCardService.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ArchiveInfo;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ArchiveCardService
|
||||
{
|
||||
public function get(?string $searchText,
|
||||
?string $dateExtractFrom,
|
||||
?string $dateExtractTo,
|
||||
?int $status,
|
||||
string $pageSize
|
||||
) : LengthAwarePaginator
|
||||
{
|
||||
$query = ArchiveInfo::with(['misHistory', 'foxproHistory'])
|
||||
->orderBy('post_in', 'desc');
|
||||
|
||||
// Поиск по тексту (если передан)
|
||||
if (!empty($searchText)) {
|
||||
$query->where(function ($q) use ($searchText) {
|
||||
// Разбиваем строку поиска на части (по пробелам)
|
||||
$searchParts = preg_split('/\s+/', trim($searchText));
|
||||
|
||||
$q->where(function ($subQuery) use ($searchParts, $searchText) {
|
||||
// Сначала проверяем MIS историю
|
||||
$subQuery->whereHas('misHistory', function ($misQuery) use ($searchParts, $searchText) {
|
||||
$this->applySearchToHistory($misQuery, $searchParts, $searchText, 'mis');
|
||||
});
|
||||
|
||||
// ИЛИ проверяем Foxpro историю (только если нет MIS истории)
|
||||
$subQuery->orWhere(function ($orWhereSubQuery) use ($searchParts, $searchText) {
|
||||
$orWhereSubQuery->whereNull('mis_history_id') // Нет MIS истории
|
||||
->whereHas('foxproHistory', function ($foxproQuery) use ($searchParts, $searchText) {
|
||||
$this->applySearchToHistory($foxproQuery, $searchParts, $searchText, 'foxpro');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Ищем по архивным номерам
|
||||
->orWhere('archive_num', 'ILIKE', "{$searchText}%")
|
||||
->orWhere('mis_num', 'ILIKE', "{$searchText}%")
|
||||
->orWhere('foxpro_num', 'ILIKE', "{$searchText}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Фильтрация по дате выписки
|
||||
if (!empty($dateExtractFrom)) {
|
||||
$query->where(function ($q) use ($dateExtractFrom) {
|
||||
$q->whereHas('misHistory', function ($misQuery) use ($dateExtractFrom) {
|
||||
$misQuery->whereDate('DateExtract', '>=', $dateExtractFrom);
|
||||
})->orWhereHas('foxproHistory', function ($foxproQuery) use ($dateExtractFrom) {
|
||||
$foxproQuery->whereDate('menddate', '>=', $dateExtractFrom);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($dateExtractTo)) {
|
||||
$query->where(function ($q) use ($dateExtractTo) {
|
||||
$q->whereHas('misHistory', function ($misQuery) use ($dateExtractTo) {
|
||||
$misQuery->whereDate('DateExtract', '<=', $dateExtractTo);
|
||||
})->orWhereHas('foxproHistory', function ($foxproQuery) use ($dateExtractTo) {
|
||||
$foxproQuery->whereDate('menddate', '<=', $dateExtractTo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Фильтрация по статусу (если передан)
|
||||
if (!empty($status)) {
|
||||
$query->where('status_id', $status);
|
||||
}
|
||||
|
||||
// Получаем результаты с пагинацией
|
||||
$results = $query->paginate($pageSize)
|
||||
->through(function ($archiveInfo) {
|
||||
// Приоритет MIS истории
|
||||
if ($archiveInfo->misHistory) {
|
||||
$history = $archiveInfo->misHistory;
|
||||
$history->history_type = 'mis';
|
||||
} else {
|
||||
$history = $archiveInfo->foxproHistory;
|
||||
$history->history_type = 'foxpro';
|
||||
}
|
||||
|
||||
return $history;
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function applySearchToHistory($query, $searchParts, $searchText, $type)
|
||||
{
|
||||
if ($type === 'mis') {
|
||||
$query->where('MedCardNum', 'ILIKE', "{$searchText}%");
|
||||
|
||||
// Ищем ФИО в зависимости от количества частей
|
||||
if (count($searchParts) === 1) {
|
||||
$query->orWhere('FAMILY', 'ILIKE', "{$searchParts[0]}%")
|
||||
->orWhere('Name', 'ILIKE', "{$searchParts[0]}%")
|
||||
->orWhere('OT', 'ILIKE', "{$searchParts[0]}%");
|
||||
} elseif (count($searchParts) === 2) {
|
||||
$query->orWhere(function ($subQuery) use ($searchParts) {
|
||||
$subQuery->where('FAMILY', 'ILIKE', "{$searchParts[0]}%")
|
||||
->where('Name', 'ILIKE', "{$searchParts[1]}%");
|
||||
});
|
||||
} elseif (count($searchParts) === 3) {
|
||||
$query->orWhere(function ($subQuery) use ($searchParts) {
|
||||
$subQuery->where('FAMILY', 'ILIKE', "{$searchParts[0]}%")
|
||||
->where('Name', 'LIKE', "{$searchParts[1]}%")
|
||||
->where('OT', 'ILIKE', "{$searchParts[2]}%");
|
||||
});
|
||||
}
|
||||
|
||||
$query->orWhereRaw("CONCAT(\"FAMILY\", ' ', \"Name\", ' ', \"OT\") ILIKE ?", ["{$searchText}%"]);
|
||||
} else { // foxpro
|
||||
$query->where('nkarta', 'ILIKE', "{$searchText}%");
|
||||
|
||||
if (count($searchParts) === 1) {
|
||||
$query->orWhere('fam', 'ILIKE', "{$searchParts[0]}%")
|
||||
->orWhere('im', 'ILIKE', "{$searchParts[0]}%")
|
||||
->orWhere('ot', 'ILIKE', "{$searchParts[0]}%");
|
||||
} elseif (count($searchParts) === 2) {
|
||||
$query->orWhere(function ($subQuery) use ($searchParts) {
|
||||
$subQuery->where('fam', 'ILIKE', "{$searchParts[0]}%")
|
||||
->where('im', 'ILIKE', "{$searchParts[1]}%");
|
||||
});
|
||||
} elseif (count($searchParts) === 3) {
|
||||
$query->orWhere(function ($subQuery) use ($searchParts) {
|
||||
$subQuery->where('fam', 'ILIKE', "{$searchParts[0]}%")
|
||||
->where('im', 'ILIKE', "{$searchParts[1]}%")
|
||||
->where('ot', 'ILIKE', "{$searchParts[2]}%");
|
||||
});
|
||||
}
|
||||
|
||||
$query->orWhereRaw("CONCAT(fam, ' ', im, ' ', ot) ILIKE ?", ["{$searchText}%"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
146
app/Services/ArchiveSearchService.php
Normal file
146
app/Services/ArchiveSearchService.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ArchiveInfo;
|
||||
use App\Models\Mis\SttMedicalHistory as MisMedicalHistory;
|
||||
use App\Models\SI\SttMedicalHistory as SiMedicalHistory;
|
||||
|
||||
class ArchiveSearchService
|
||||
{
|
||||
/**
|
||||
* Поиск карты по номеру
|
||||
*/
|
||||
public function searchByNumber(string $searchText): ?ArchiveInfo
|
||||
{
|
||||
if (!is_numeric($searchText)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. Проверяем, есть ли запись в archive_infos
|
||||
$archiveInfo = $this->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, // Статус по умолчанию
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
161
composer.lock
generated
161
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -65,7 +65,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => 'Asia/Yakutsk',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -30,7 +30,6 @@ return [
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -27,5 +27,10 @@ class ArchiveStatusSeeder extends Seeder
|
||||
'text' => 'Выдана',
|
||||
'variant' => 'warning'
|
||||
]);
|
||||
|
||||
ArchiveStatus::create([
|
||||
'text' => 'Утеряна',
|
||||
'variant' => 'error'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
21
database/seeders/OrgSeeder.php
Normal file
21
database/seeders/OrgSeeder.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Org;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class OrgSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
Org::create([
|
||||
'code' => '0',
|
||||
'name' => 'Неизвестно'
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
#PHP Service
|
||||
app:
|
||||
image: registry.brusoff.su/kartoteka:1.0
|
||||
build: .
|
||||
container_name: kartoteka_app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8090:80"
|
||||
working_dir: /var/www
|
||||
volumes:
|
||||
- ./.env:/var/www/.env
|
||||
- ./docker/php.ini:/usr/local/etc/php/conf.d/app.ini
|
||||
- ./docker/blocked_ips.map:/etc/nginx/blocked_ips.map
|
||||
- ./storage/logs:/var/www/storage/logs
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
#Docker Networks
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
204
docker/app.conf
Normal file
204
docker/app.conf
Normal file
@@ -0,0 +1,204 @@
|
||||
# Определяем переменную для блокировки
|
||||
map $remote_addr $blocked_ip {
|
||||
default 0;
|
||||
include /etc/nginx/blocked_ips.map;
|
||||
}
|
||||
|
||||
# Определяем типы запросов
|
||||
map $request_uri $request_type {
|
||||
default "general";
|
||||
|
||||
# API endpoints
|
||||
~*^/api/ "api";
|
||||
|
||||
# Auth endpoints
|
||||
~*^/(login|register|password-reset|auth|forgot-password|verify-email) "auth";
|
||||
|
||||
# Admin endpoints
|
||||
~*^/(admin|dashboard|cp|control-panel|manager) "admin";
|
||||
|
||||
# Static files by extension
|
||||
~*\.(jpg|jpeg|png|gif|ico|css|js|woff2?|ttf|eot|svg|pdf|zip|mp4|webm)$ "static";
|
||||
|
||||
# Static files by directory
|
||||
~*^/(storage|uploads|media|images|css|js|fonts)/ "static";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /var/www/public;
|
||||
index index.php index.html;
|
||||
|
||||
# ========== ОСНОВНЫЕ НАСТРОЙКИ БЕЗОПАСНОСТИ ==========
|
||||
|
||||
# Защитные заголовки
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# ========== ПРЕДВАРИТЕЛЬНЫЕ ПРОВЕРКИ ==========
|
||||
|
||||
# Если IP в черном списке
|
||||
if ($blocked_ip) {
|
||||
return 444;
|
||||
}
|
||||
|
||||
# Блокировка пустых User-Agent
|
||||
if ($http_user_agent = "") {
|
||||
return 444;
|
||||
}
|
||||
|
||||
# Блокировка нестандартных методов
|
||||
if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$) {
|
||||
return 444;
|
||||
}
|
||||
|
||||
# ========== ГЛОБАЛЬНЫЕ ОГРАНИЧЕНИЯ ==========
|
||||
|
||||
limit_conn conn_limit_per_ip 25;
|
||||
limit_req zone=req_limit_per_ip burst=30 delay=15;
|
||||
|
||||
# ========== СТАТИЧЕСКИЕ ФАЙЛЫ ==========
|
||||
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|webp|avif)$ {
|
||||
limit_req zone=static_limit burst=100 nodelay;
|
||||
limit_conn conn_limit_per_ip 100;
|
||||
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Pragma "public";
|
||||
|
||||
try_files $uri =404;
|
||||
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
location ~* \.(css|js)$ {
|
||||
limit_req zone=static_limit burst=80 nodelay;
|
||||
limit_conn conn_limit_per_ip 80;
|
||||
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
try_files $uri =404;
|
||||
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~* \.(woff2?|ttf|eot|svg)$ {
|
||||
limit_req zone=static_limit burst=60 nodelay;
|
||||
limit_conn conn_limit_per_ip 60;
|
||||
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
|
||||
try_files $uri =404;
|
||||
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ========== API ENDPOINTS ==========
|
||||
|
||||
location ~* ^/api/ {
|
||||
limit_req zone=api_limit burst=30 delay=15;
|
||||
limit_conn conn_limit_per_ip 30;
|
||||
|
||||
access_log /var/log/nginx/api_access.log main;
|
||||
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
# ========== АУТЕНТИФИКАЦИЯ ==========
|
||||
|
||||
location ~* ^/(login|register|password-reset|auth|forgot-password|verify-email) {
|
||||
limit_req zone=auth_limit burst=5 delay=3;
|
||||
limit_conn conn_limit_per_ip 5;
|
||||
|
||||
access_log /var/log/nginx/auth_access.log main;
|
||||
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
# ========== ОБРАБОТКА PHP ==========
|
||||
|
||||
location ~ \.php$ {
|
||||
# Дефолтные лимиты для всех PHP запросов
|
||||
limit_req zone=req_limit_per_ip burst=30 delay=15;
|
||||
limit_conn conn_limit_per_ip 30;
|
||||
|
||||
try_files $uri =404;
|
||||
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
|
||||
# Оптимизация из основного конфига
|
||||
fastcgi_buffers 16 16k;
|
||||
fastcgi_buffer_size 32k;
|
||||
fastcgi_busy_buffers_size 240k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
|
||||
# Таймауты
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 600s;
|
||||
fastcgi_read_timeout 600s;
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
# ========== ОСНОВНОЙ LOCATION ==========
|
||||
|
||||
location / {
|
||||
|
||||
if ($query_string ~* "(?:^|[^a-z])(?:union|select|insert|update|delete|drop|create|alter|exec)(?:[^a-z]|$)") {
|
||||
return 444;
|
||||
}
|
||||
|
||||
if ($request_uri ~ "//") {
|
||||
return 444;
|
||||
}
|
||||
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
|
||||
gzip_static on;
|
||||
gzip_vary on;
|
||||
}
|
||||
|
||||
# ========== HEALTH CHECK ==========
|
||||
|
||||
location = /health {
|
||||
access_log off;
|
||||
|
||||
# Прямой прокси в Laravel
|
||||
try_files $uri /index.php?$query_string;
|
||||
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# ========== ЗАЩИТА ФАЙЛОВОЙ СИСТЕМЫ ==========
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* (\.env|\.git|\.svn|\.htaccess|composer\.json|composer\.lock) {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* (eval|base64_encode|system\(|shell_exec|passthru|exec|phpinfo) {
|
||||
deny all;
|
||||
return 444;
|
||||
}
|
||||
}
|
||||
0
docker/blocked_ips.map
Normal file
0
docker/blocked_ips.map
Normal file
81
docker/nginx.conf
Normal file
81
docker/nginx.conf
Normal file
@@ -0,0 +1,81 @@
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 65535;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
accept_mutex_delay 100ms;
|
||||
}
|
||||
|
||||
http {
|
||||
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=6r/s;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=3r/s;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=bot_limit:10m rate=1r/s;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=static_limit:10m rate=100r/s;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
|
||||
|
||||
map $http_user_agent $limit_bots {
|
||||
default "";
|
||||
~*(googlebot|bingbot|yandex|baiduspider) $binary_remote_addr;
|
||||
}
|
||||
limit_req_zone $limit_bots zone=bots:10m rate=20r/m;
|
||||
|
||||
limit_req_zone $request_method zone=post_limit:10m rate=5r/s;
|
||||
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=40m use_temp_path=off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 75;
|
||||
keepalive_requests 100;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
client_body_timeout 12;
|
||||
client_header_timeout 12;
|
||||
send_timeout 30;
|
||||
reset_timedout_connection on;
|
||||
|
||||
client_header_buffer_size 1k;
|
||||
large_client_header_buffers 4 8k;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss
|
||||
application/atom+xml image/svg+xml;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||
|
||||
log_format ddos_log '$remote_addr - [$time_local] "$request" $status '
|
||||
'rate=$limit_req_status conn=$limit_conn_status '
|
||||
'user_agent="$http_user_agent"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
limit_req_status 429;
|
||||
limit_conn_status 429;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
2
docker/php.ini
Normal file
2
docker/php.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
upload_max_filesize=40M
|
||||
post_max_size=40M
|
||||
25
docker/supervisord.conf
Normal file
25
docker/supervisord.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/var/log/supervisor/supervisor.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=3
|
||||
|
||||
[program:php-fpm]
|
||||
command=/usr/local/sbin/php-fpm -F
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
stdout_logfile=/var/log/supervisor/php-fpm.log
|
||||
stderr_logfile=/var/log/supervisor/php-fpm_err.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=3
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g "daemon off;"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/var/log/supervisor/nginx.log
|
||||
stderr_logfile=/var/log/supervisor/nginx_err.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=3
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1634,6 +1634,7 @@
|
||||
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emotion/hash": "~0.8.0",
|
||||
"csstype": "~3.0.5"
|
||||
@@ -1657,6 +1658,7 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@@ -2498,6 +2500,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2902,6 +2905,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3013,6 +3017,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
// 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 { 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',
|
||||
status: initialFilters?.status || null,
|
||||
})
|
||||
|
||||
// Метаданные для разных типов данных
|
||||
const meta = computed(() => {
|
||||
return page.props.cards.meta
|
||||
})
|
||||
|
||||
const meta = computed(() => page.props.cards?.meta || {})
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Форматирование даты для URL
|
||||
@@ -32,17 +35,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),
|
||||
@@ -51,20 +45,32 @@ export const useMedicalHistoryFilter = (filters) => {
|
||||
page_size: filtersRef.value.page_size,
|
||||
sort_by: filtersRef.value.sort_by,
|
||||
sort_order: filtersRef.value.sort_order,
|
||||
view_type: filtersRef.value.view_type,
|
||||
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: () => {
|
||||
@@ -116,6 +122,13 @@ export const useMedicalHistoryFilter = (filters) => {
|
||||
filtersRef.value.date_extract_to ? convertToTimestamp(filtersRef.value.date_extract_to) : null
|
||||
])
|
||||
|
||||
const searchValue = ref(filtersRef.value.search ?? null)
|
||||
|
||||
const statusValue = computed(() => {
|
||||
if (filtersRef.value.status !== null) return Number(filtersRef.value.status)
|
||||
else return filtersRef.value.status
|
||||
})
|
||||
|
||||
const handleDateRangeChange = (timestamps) => {
|
||||
dateRange.value = timestamps || [null, null]
|
||||
|
||||
@@ -128,27 +141,26 @@ 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) => {
|
||||
applyFilters({ page_size: size, page: 1 })
|
||||
}
|
||||
|
||||
const handleViewTypeChange = (view_type) => {
|
||||
applyFilters({ view_type, page: 1 })
|
||||
}
|
||||
|
||||
const handleSortChange = (sorter) => {
|
||||
applyFilters({
|
||||
sort_by: sorter.columnKey,
|
||||
sort_order: sorter.order
|
||||
})
|
||||
}, true)
|
||||
}
|
||||
|
||||
|
||||
const handleStatusChange = (status_id) => {
|
||||
applyFilters({
|
||||
status: status_id
|
||||
}, true)
|
||||
}
|
||||
|
||||
const resetAllFilters = () => {
|
||||
@@ -158,34 +170,91 @@ 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',
|
||||
}
|
||||
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'
|
||||
})
|
||||
}
|
||||
// Добавьте другие фильтры по необходимости
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 +278,20 @@ export const useMedicalHistoryFilter = (filters) => {
|
||||
isLoading,
|
||||
activeFilters,
|
||||
dateRange,
|
||||
handleViewTypeChange,
|
||||
searchValue,
|
||||
statusValue,
|
||||
|
||||
// Обработчики
|
||||
handleSearch,
|
||||
handleDateRangeChange,
|
||||
handleStatusChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
handleSortChange,
|
||||
handleStatusChange,
|
||||
resetAllFilters,
|
||||
applyFilters
|
||||
removeFilter,
|
||||
applyFilters,
|
||||
quickSearch,
|
||||
prioritySearch
|
||||
}
|
||||
}
|
||||
|
||||
43
resources/js/Composables/useNotification.js
Normal file
43
resources/js/Composables/useNotification.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import {h} from "vue";
|
||||
import {NCode, NLog} from "naive-ui"
|
||||
|
||||
export function useNotification() {
|
||||
const showNotification = (options) => {
|
||||
if (window.$notification) {
|
||||
return window.$notification.create(options)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const success = () => {
|
||||
|
||||
}
|
||||
|
||||
const errorApi = (data, content = null, options = {}) => {
|
||||
return showNotification(
|
||||
{
|
||||
title: 'Произошла ошибка',
|
||||
description: data
|
||||
? `Код: ${data.code}\nURL: ${data.response.config.url}\nМетод: ${data.response.config.method.toUpperCase()}`
|
||||
: null,
|
||||
content: () => {
|
||||
return content ? content : h(
|
||||
NLog,
|
||||
{
|
||||
rows: 4,
|
||||
log: data.response.config.data,
|
||||
}
|
||||
)
|
||||
},
|
||||
meta: options.meta || new Date().toLocaleDateString(),
|
||||
...options,
|
||||
type: 'error'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
errorApi
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { NLayout, NH1, NLayoutSider, NFlex, NButton, NConfigProvider, ruRU, dateRuRU } from "naive-ui";
|
||||
import {NLayout, NH1, NLayoutSider, NFlex, NButton, NConfigProvider, ruRU, dateRuRU, darkTheme} from "naive-ui";
|
||||
import SideMenu from "./Components/SideMenu.vue";
|
||||
import { generate } from '@arco-design/color'
|
||||
|
||||
@@ -7,33 +7,41 @@ const colors = generate('#EC6608', {
|
||||
list: true,
|
||||
})
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
primaryColor: colors[5],
|
||||
primaryColorHover: colors[4],
|
||||
primaryColorSuppl: colors[4],
|
||||
primaryColorPressed: colors[6],
|
||||
// common: {
|
||||
// primaryColor: colors[5],
|
||||
// primaryColorHover: colors[4],
|
||||
// primaryColorSuppl: colors[4],
|
||||
// primaryColorPressed: colors[6],
|
||||
// },
|
||||
Modal: {
|
||||
peers: {
|
||||
Dialog: {
|
||||
borderRadius: '8px'
|
||||
},
|
||||
Card: {
|
||||
borderRadius: '8px'
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NConfigProvider :theme-overrides="themeOverrides" :locale="ruRU" :date-locale="dateRuRU">
|
||||
<NConfigProvider :theme="darkTheme" :theme-overrides="themeOverrides" :locale="ruRU" :date-locale="dateRuRU">
|
||||
<NLayout class="h-screen">
|
||||
<NLayout position="absolute" has-sider>
|
||||
<NLayoutSider
|
||||
:native-scrollbar="false"
|
||||
bordered
|
||||
>
|
||||
<SideMenu />
|
||||
</NLayoutSider>
|
||||
<NLayout content-class="p-6 relative" :native-scrollbar="false">
|
||||
<NLayout position="absolute" content-class="p-6 relative" :native-scrollbar="false">
|
||||
<!-- <NLayoutSider-->
|
||||
<!-- :native-scrollbar="false"-->
|
||||
<!-- bordered-->
|
||||
<!-- >-->
|
||||
<!-- <SideMenu />-->
|
||||
<!-- </NLayoutSider>-->
|
||||
<div>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<slot />
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
|
||||
|
||||
166
resources/js/Pages/Home/ArchiveHistoryCreateModal/Index.vue
Normal file
166
resources/js/Pages/Home/ArchiveHistoryCreateModal/Index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script setup>
|
||||
import { NModal, NSelect, NSpace, NFlex, NButton, NForm, NFormItem, NInput, NDatePicker, NDivider, NSpin, NTag } from 'naive-ui'
|
||||
import ArchiveHistoryMoveModal from '../ArchiveHistoryMoveModal/Index.vue'
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useMedicalHistoryFilter} from "../../../Composables/useMedicalHistoryFilter.js";
|
||||
import {useNotification} from "../../../Composables/useNotification.js";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
|
||||
const open = defineModel('open')
|
||||
|
||||
const {errorApi} = useNotification()
|
||||
const loading = ref(false)
|
||||
const archiveInfo = ref({
|
||||
id: null,
|
||||
num: null,
|
||||
post_in: null,
|
||||
})
|
||||
const archiveInfoRules = {
|
||||
id: [
|
||||
{
|
||||
required: true,
|
||||
validator(rule, value) {
|
||||
console.log(value)
|
||||
if (!value) {
|
||||
return new Error('Это поле необходимо заполнить')
|
||||
}
|
||||
else if (!Number(value)) {
|
||||
return new Error('Ошибка при получении ID')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
num: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Это поле необходимо заполнить',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
post_in: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Это поле необходимо заполнить',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
const loadingFilterPatients = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const emits = defineEmits(['historyUpdated', 'closeWithoutSave'])
|
||||
|
||||
const onResetData = () => {
|
||||
archiveInfo.value = {
|
||||
id: null,
|
||||
num: null,
|
||||
post_in: null,
|
||||
}
|
||||
}
|
||||
|
||||
const onCloseWithoutSave = () => {
|
||||
emits('closeWithoutSave')
|
||||
open.value = false
|
||||
onResetData()
|
||||
}
|
||||
|
||||
const filteredPatients = ref([])
|
||||
|
||||
const debounceSearchCard = useDebounceFn(async (query) => {
|
||||
loadingFilterPatients.value = true
|
||||
await axios.post('/api/mis/patients/search', {
|
||||
query
|
||||
}).then(res => {
|
||||
filteredPatients.value = res.data
|
||||
}).finally(() => {
|
||||
loadingFilterPatients.value = false
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
const handleSearchCard = (query) => {
|
||||
debounceSearchCard(query)
|
||||
}
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
loading.value = true
|
||||
try {
|
||||
await axios.post(`/api/archive/create`, archiveInfo.value).then(res => {
|
||||
open.value = false
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
errorApi(error)
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(errors)
|
||||
errorApi(null, 'Проверьте заполненность формы')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="open"
|
||||
preset="card"
|
||||
class="max-w-xl relative"
|
||||
:mask-closable="false"
|
||||
closable
|
||||
title="Добавить карту в архив"
|
||||
@close="onCloseWithoutSave">
|
||||
<NFlex class="w-full" :wrap="false">
|
||||
<NForm ref="formRef" id="formAddToArchive" class="w-full" :model="archiveInfo" :rules="archiveInfoRules">
|
||||
<NFormItem label="Карта пациента" path="id">
|
||||
<NSelect placeholder="№ карты фамилия имя отчество ИЛИ фамилия имя отчество"
|
||||
size="large"
|
||||
:remote
|
||||
filterable
|
||||
:loading="loadingFilterPatients"
|
||||
:options="filteredPatients"
|
||||
v-model:value="archiveInfo.id"
|
||||
@search="handleSearchCard"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem size="large" label="Дата поступления карты в архив" path="post_in">
|
||||
<NDatePicker class="w-full"
|
||||
v-model:value="archiveInfo.post_in"
|
||||
format="dd.MM.yyyy"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem size="large" label="№ в архиве" path="num">
|
||||
<NInput v-model:value="archiveInfo.num" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NFlex>
|
||||
<template #action>
|
||||
<NFlex justify="end" align="center">
|
||||
<NSpace align="center" :size="0">
|
||||
<NButton secondary
|
||||
type="primary"
|
||||
attr-type="submit"
|
||||
form="formAddToArchive"
|
||||
@click="onSubmit">
|
||||
Добавить карту
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<div v-show="loading"
|
||||
class="absolute inset-0 z-10 backdrop-blur flex items-center justify-center rounded-[8px]">
|
||||
<NSpin :show="true" />
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,36 +1,69 @@
|
||||
<script setup>
|
||||
import { NModal, NDataTable, NSpace, NFlex, NButton, NForm, NFormItem, NInput, NDatePicker, NDivider, NSwitch, NTag } from 'naive-ui'
|
||||
import { NModal, NDataTable, NSpace, NFlex, NButton, NForm, NFormItem, NInput, NDatePicker, NDivider, NSpin, NTag } from 'naive-ui'
|
||||
import ArchiveHistoryMoveModal from '../ArchiveHistoryMoveModal/Index.vue'
|
||||
import {ref, watch} from "vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useMedicalHistoryFilter} from "../../../Composables/useMedicalHistoryFilter.js";
|
||||
import {useNotification} from "../../../Composables/useNotification.js";
|
||||
|
||||
const open = defineModel('open')
|
||||
const props = defineProps({
|
||||
patientId: {
|
||||
patientInfo: {
|
||||
type: Number,
|
||||
}
|
||||
})
|
||||
|
||||
const {filtersRef} = useMedicalHistoryFilter()
|
||||
const {errorApi} = useNotification()
|
||||
const loading = ref(true)
|
||||
const patient = ref({})
|
||||
const showArchiveHistoryModal = ref(false)
|
||||
const selectedArchiveHistoryId = ref(null)
|
||||
const selectedArchiveHistoryType = ref(props.patientInfo.type)
|
||||
const isCreateNewArchiveHistoryModal = ref(false)
|
||||
const archiveInfo = ref({
|
||||
id: props.patientInfo.id,
|
||||
num: null,
|
||||
post_in: null,
|
||||
status: null
|
||||
})
|
||||
const emits = defineEmits(['historyUpdated', 'closeWithoutSave'])
|
||||
|
||||
const onResetData = () => {
|
||||
archiveInfo.value = {
|
||||
id: props.patientInfo.id,
|
||||
num: null,
|
||||
post_in: null,
|
||||
status: null
|
||||
}
|
||||
}
|
||||
|
||||
const onCloseWithoutSave = () => {
|
||||
emits('closeWithoutSave')
|
||||
open.value = false
|
||||
}
|
||||
|
||||
const patientData = ref({
|
||||
...patient.value.info
|
||||
})
|
||||
|
||||
const loadPatientData = async () => {
|
||||
if (!props.patientId) return
|
||||
if (!props.patientInfo.id) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
axios.get(`/api/si/patients/${props.patientId}`).then(res => {
|
||||
await axios.get(`/api/si/patients/${props.patientInfo.id}?view_type=${props.patientInfo.type}`).then(res => {
|
||||
patient.value = res.data
|
||||
patientData.value = res.data.info
|
||||
archiveInfo.value = res.data.archiveInfo ?? {
|
||||
// historyable_type: res.data.historyable_type,
|
||||
id: props.patientInfo.id,
|
||||
num: null,
|
||||
post_in: null,
|
||||
status: null,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// message.error('Ошибка при загрузке данных пациента')
|
||||
errorApi(error)
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -38,7 +71,14 @@ const loadPatientData = async () => {
|
||||
}
|
||||
|
||||
const onShowArchiveHistoryModal = (id) => {
|
||||
selectedArchiveHistoryId.value = id
|
||||
if (id === null) {
|
||||
isCreateNewArchiveHistoryModal.value = true
|
||||
selectedArchiveHistoryId.value = Number(props.patientInfo.id)
|
||||
} else {
|
||||
isCreateNewArchiveHistoryModal.value = false
|
||||
selectedArchiveHistoryId.value = Number(id)
|
||||
}
|
||||
selectedArchiveHistoryType.value = props.patientInfo.type
|
||||
showArchiveHistoryModal.value = true
|
||||
}
|
||||
|
||||
@@ -75,16 +115,51 @@ const rowProps = (row) => ({
|
||||
}
|
||||
})
|
||||
|
||||
const onUpdateHistory = async ({data}) => {
|
||||
await loadPatientData()
|
||||
const updatedData = {
|
||||
status: archiveInfo.value.status,
|
||||
}
|
||||
emits('historyUpdated', {
|
||||
data: updatedData,
|
||||
patientId: props.patientInfo.id
|
||||
})
|
||||
}
|
||||
|
||||
const hasCreateNew = computed(() => archiveInfo.value === null)
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await axios.post(`/api/archive/histories/info/${props.patientInfo.id}`, archiveInfo.value).then(res => {
|
||||
// onCloseWithoutSave()
|
||||
const updatedData = {
|
||||
status: archiveInfo.value.status,
|
||||
}
|
||||
emits('historyUpdated', {
|
||||
data: updatedData,
|
||||
patientId: props.patientInfo.id
|
||||
})
|
||||
open.value = false
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
errorApi(error)
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаем за изменением patientId
|
||||
watch(() => props.patientId, (newId) => {
|
||||
watch(() => props.patientInfo, async (newId) => {
|
||||
if (newId) {
|
||||
loadPatientData()
|
||||
await loadPatientData()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="open" preset="card" class="max-w-4xl" closable @close="open = false">
|
||||
<NModal v-model:show="open" preset="card" class="max-w-4xl relative overflow-clip" closable @close="onCloseWithoutSave">
|
||||
<template #header>
|
||||
{{ patient.info?.medcardnum }} {{ patient.info?.family }} {{ patient.info?.name }} {{ patient.info?.ot }}
|
||||
</template>
|
||||
@@ -92,19 +167,24 @@ watch(() => props.patientId, (newId) => {
|
||||
<NFlex inline justify="space-between" align="center" :wrap="false">
|
||||
<NForm inline :show-feedback="false">
|
||||
<NFormItem label="Статус карты">
|
||||
<NTag :type="patient.archiveInfo?.status?.variant ?? 'error'" :bordered="false" round>
|
||||
{{ patient.archiveInfo?.status?.text ?? 'Нет в архиве' }}
|
||||
<NTag :type="archiveInfo.status?.variant ?? 'error'" :bordered="false" round>
|
||||
{{ archiveInfo.status?.text ?? 'Нет в архиве' }}
|
||||
</NTag>
|
||||
</NFormItem>
|
||||
<NFormItem label="№ в архиве">
|
||||
<NInput v-model:value="patientData.narhiv" />
|
||||
<NInput v-model:value="archiveInfo.num" />
|
||||
</NFormItem>
|
||||
<NFormItem label="Дата поступления карты в архив">
|
||||
<NDatePicker v-model:value="patientData.datearhiv" format="dd.MM.yyyy" />
|
||||
<NDatePicker v-model:value="archiveInfo.post_in" format="dd.MM.yyyy" />
|
||||
</NFormItem>
|
||||
<NFormItem label="№ карты в МИС / СофтИнфо">
|
||||
<NTag :bordered="false" round class="w-full justify-center">
|
||||
{{ archiveInfo.mis_num }} / {{ archiveInfo.foxpro_num }}
|
||||
</NTag>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NFlex>
|
||||
<NButton @click="onShowArchiveHistoryModal(null)">
|
||||
<NButton @click="onShowArchiveHistoryModal(null)" :disabled="!patientData.can_be_issued">
|
||||
Добавить
|
||||
</NButton>
|
||||
<NDataTable :row-props="rowProps" min-height="420px" max-height="420px" :columns="columns" :data="patient?.journal" />
|
||||
@@ -112,18 +192,22 @@ watch(() => props.patientId, (newId) => {
|
||||
<template #action>
|
||||
<NFlex justify="end" align="center">
|
||||
<NSpace align="center" :size="0">
|
||||
<NButton secondary>
|
||||
<NButton secondary @click="onCloseWithoutSave">
|
||||
Закрыть без сохранения
|
||||
</NButton>
|
||||
<NDivider vertical />
|
||||
<NButton secondary type="primary">
|
||||
<NButton secondary type="primary" @click="onSubmit">
|
||||
Сохранить
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<div v-show="loading" class="absolute inset-0 z-10 backdrop-blur flex items-center justify-center">
|
||||
<NSpin :show="true" />
|
||||
</div>
|
||||
</NModal>
|
||||
<ArchiveHistoryMoveModal @close-without-save="selectedArchiveHistoryId = null" v-model:open="showArchiveHistoryModal" :archive-history-id="selectedArchiveHistoryId" />
|
||||
<ArchiveHistoryMoveModal @history-updated="onUpdateHistory" @close-without-save="selectedArchiveHistoryId = null" :is-create-new="isCreateNewArchiveHistoryModal" v-model:open="showArchiveHistoryModal" :type="selectedArchiveHistoryType" :archive-history-id="selectedArchiveHistoryId" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
<script setup>
|
||||
import {NButton, NDatePicker, NDivider, NFlex, NForm, NFormItem, NInput, NModal, NSpace, NSwitch, NSelect} from "naive-ui";
|
||||
import {
|
||||
NButton,
|
||||
NDatePicker,
|
||||
NDivider,
|
||||
NFlex,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NModal,
|
||||
NSpace,
|
||||
NSwitch,
|
||||
NSelect,
|
||||
NSpin
|
||||
} from "naive-ui";
|
||||
import {ref, watch} from "vue";
|
||||
import {router} from "@inertiajs/vue3";
|
||||
const open = defineModel('open')
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String
|
||||
},
|
||||
archiveHistoryId: {
|
||||
type: Number
|
||||
},
|
||||
isCreateNew: {
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['closeWithoutSave'])
|
||||
const emits = defineEmits(['closeWithoutSave', 'historyUpdated'])
|
||||
|
||||
const loading = ref(false)
|
||||
const archiveHistory = ref({
|
||||
historyable_id: props.archiveHistoryId,
|
||||
type: props.type,
|
||||
issue_at: null,
|
||||
org_id: null,
|
||||
return_at: null,
|
||||
@@ -24,7 +44,8 @@ const orgs = ref([])
|
||||
|
||||
const onResetData = () => {
|
||||
archiveHistory.value = {
|
||||
historyable_id: props.archiveHistoryId,
|
||||
history_id: props.archiveHistoryId,
|
||||
type: props.type,
|
||||
issue_at: null,
|
||||
org_id: null,
|
||||
return_at: null,
|
||||
@@ -59,9 +80,19 @@ const loadArchiveHistoryData = async () => {
|
||||
|
||||
const submit = () => {
|
||||
try {
|
||||
axios.post('/api/archive/histories/move', archiveHistory.value).then(res => {
|
||||
const url = props.isCreateNew
|
||||
? '/api/archive/histories/move'
|
||||
: `/api/archive/histories/move/${archiveHistory.value.id}`
|
||||
|
||||
axios.post(url, archiveHistory.value).then(res => {
|
||||
emits('historyUpdated', {
|
||||
type: props.isCreateNew ? 'created' : 'updated',
|
||||
data: res.data,
|
||||
historyId: archiveHistory.value.id
|
||||
})
|
||||
onCloseWithoutSave()
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -71,7 +102,7 @@ const submit = () => {
|
||||
|
||||
// Наблюдаем за изменением archiveHistoryId
|
||||
watch(() => props.archiveHistoryId, async (newId) => {
|
||||
if (newId) {
|
||||
if (!props.isCreateNew) {
|
||||
await loadArchiveHistoryData()
|
||||
} else {
|
||||
onResetData()
|
||||
@@ -80,9 +111,9 @@ watch(() => props.archiveHistoryId, async (newId) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="open" preset="card" class="max-w-2xl" closable @close="open = false">
|
||||
<NModal v-model:show="open" preset="card" class="max-w-2xl relative overflow-clip" closable @close="onCloseWithoutSave">
|
||||
<template #header>
|
||||
{{ archiveHistoryId === null ? 'Добавить' : 'Редактировать' }} запись выдачи
|
||||
{{ isCreateNew ? 'Добавить' : 'Редактировать' }} запись выдачи
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="Дата выдачи">
|
||||
@@ -121,6 +152,10 @@ watch(() => props.archiveHistoryId, async (newId) => {
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<div v-show="loading" class="absolute inset-0 z-10 backdrop-blur flex items-center justify-center">
|
||||
<NSpin :show="true" />
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import {NDataTable, NEllipsis, NTag} from "naive-ui"
|
||||
import {computed, h, reactive, ref} from "vue"
|
||||
import {computed, h, reactive, ref, watch} from "vue"
|
||||
import {useMedicalHistoryFilter} from "../../../Composables/useMedicalHistoryFilter.js";
|
||||
import ArchiveHistoryModal from '../ArchiveHistoryModal/Index.vue'
|
||||
|
||||
@@ -23,18 +23,18 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const { isLoading, handlePageChange, handlePageSizeChange, meta } = useMedicalHistoryFilter(props.filters)
|
||||
const { isLoading, handlePageChange, handlePageSizeChange, meta, filtersRef } = useMedicalHistoryFilter(props.filters)
|
||||
const dataTable = ref(props.data ?? [])
|
||||
|
||||
const archiveStatusColumn = (status) => {
|
||||
const tagType = status?.variant ?? 'error'
|
||||
const tagText = status?.text ?? 'Нет в архиве'
|
||||
console.log(tagType)
|
||||
|
||||
return h(
|
||||
NEllipsis,
|
||||
null,
|
||||
{
|
||||
default: () => h(NTag, {type: tagType, round: true, size: 'small'}, tagText)
|
||||
default: () => h(NTag, {type: tagType, bordered: false, round: true, size: 'small'}, tagText)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,15 +89,16 @@ const columns = ref([
|
||||
}
|
||||
])
|
||||
const showArchiveHistoryModal = ref(false)
|
||||
const selectedPatientId = ref(null)
|
||||
const selectedPatientInfo = ref({})
|
||||
const rowProps = (row) => ({
|
||||
onDblclick: () => {
|
||||
selectedPatientId.value = row.id
|
||||
selectedPatientInfo.value = {id: row.id, type: row.history_type}
|
||||
showArchiveHistoryModal.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const pagination = computed(() => ({
|
||||
const pagination = computed(() => {
|
||||
return {
|
||||
page: meta.value.current_page || 1,
|
||||
pageCount: meta.value.last_page || 1,
|
||||
pageSize: meta.value.per_page || 15,
|
||||
@@ -105,18 +106,51 @@ const pagination = computed(() => ({
|
||||
pageSizes: [15, 50, 100],
|
||||
showSizePicker: true,
|
||||
prefix({ itemCount }) {
|
||||
return `Всего карт ${itemCount}.`
|
||||
return `Всего карт ${itemCount}`
|
||||
},
|
||||
onUpdatePage: (page) => {
|
||||
handlePageChange(page)
|
||||
},
|
||||
onUpdatePageSize: handlePageSizeChange
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const onCloseWithoutSave = () => {
|
||||
selectedPatientInfo.value = null
|
||||
}
|
||||
|
||||
const onUpdateHistory = ({data, patientId}) => {
|
||||
if (dataTable.value.length > 0) {
|
||||
let needUpdateItem = dataTable.value.findIndex(itm => itm.id === patientId)
|
||||
dataTable.value[needUpdateItem] = {
|
||||
...dataTable.value[needUpdateItem],
|
||||
status: data.status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.data, (newData) => {
|
||||
dataTable.value = newData
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDataTable remote striped :loading="isLoading" :row-props="rowProps" :columns="columns" :pagination="pagination" :max-height="maxHeight" size="small" :min-height="minHeight" :data="data" />
|
||||
<ArchiveHistoryModal v-model:open="showArchiveHistoryModal" :patient-id="selectedPatientId" />
|
||||
<NDataTable remote
|
||||
striped
|
||||
:loading="isLoading"
|
||||
:row-props="rowProps"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:max-height="maxHeight"
|
||||
size="small"
|
||||
:min-height="minHeight"
|
||||
:data="dataTable"
|
||||
/>
|
||||
<ArchiveHistoryModal v-model:open="showArchiveHistoryModal"
|
||||
:patient-info="selectedPatientInfo"
|
||||
@history-updated="onUpdateHistory"
|
||||
@close-without-save="onCloseWithoutSave"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,54 +1,116 @@
|
||||
<script setup>
|
||||
import AppLayout from "../../Layouts/AppLayout.vue"
|
||||
import TableCards from './DataTable/Index.vue'
|
||||
import { NInput, NFlex, NDivider, NDatePicker, NSpace, NRadioGroup, NRadioButton, NH1 } from 'naive-ui'
|
||||
import {useMedicalHistory} from "../../Composables/useMedicalHistory.js";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
import ArchiveHistoryCreateModal from './ArchiveHistoryCreateModal/Index.vue'
|
||||
import { NInput, NFlex, NDivider, NDatePicker, NSpace, NFormItem, NRadioButton, NH1, NTabs, NButton, NSelect } from 'naive-ui'
|
||||
import {useMedicalHistoryFilter} from "../../Composables/useMedicalHistoryFilter.js";
|
||||
import {computed, ref} from "vue";
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
cards: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
statuses: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
filters: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
})
|
||||
|
||||
const ArchiveHistoryCreateModalShow = ref(false)
|
||||
const {
|
||||
isLoading, applyFilters, filtersRef, handleSearch, dateRange,
|
||||
handleDateRangeChange, handleViewTypeChange
|
||||
isLoading, applyFilters, filtersRef, handleSearch, dateRange, searchValue,
|
||||
statusValue, handleDateRangeChange, handleViewTypeChange, handleStatusChange
|
||||
} = useMedicalHistoryFilter(props.filters)
|
||||
|
||||
const viewType = ref('archive')
|
||||
const searchRef = ref()
|
||||
|
||||
const onHandleSearch = (search) => {
|
||||
handleSearch(search)
|
||||
}
|
||||
|
||||
const handleBeforeLeave = (tabName) => {
|
||||
handleViewTypeChange(tabName)
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<template #header>
|
||||
<NSpace vertical>
|
||||
<NH1 class="!my-0 mb-5">
|
||||
Архив
|
||||
</NH1>
|
||||
<NRadioGroup v-model:value="viewType" @update:value="val => handleViewTypeChange(val)">
|
||||
<NRadioButton value="archive">Архив</NRadioButton>
|
||||
<NRadioButton value="mis">МИС</NRadioButton>
|
||||
<NRadioButton value="softinfo">СофтИнфо</NRadioButton>
|
||||
</NRadioGroup>
|
||||
<NFlex class="pb-4" align="center" :wrap="false">
|
||||
<NInput placeholder="Поиск по ФИО, № карты" @update:value="val => handleSearch(val)"/>
|
||||
<NFormItem class="w-[720px]" label="Поиск" :show-feedback="false">
|
||||
<NInput placeholder="Поиск по ФИО, № карты"
|
||||
autofocus
|
||||
ref="searchRef"
|
||||
clearable
|
||||
v-model:value="searchValue"
|
||||
@update:value="val => onHandleSearch(val)"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</NFormItem>
|
||||
<div class="mt-6">
|
||||
<NDivider vertical />
|
||||
<NDatePicker v-model:value="dateRange" @update:value="handleDateRangeChange" type="daterange" clearable format="dd.MM.yyyy" start-placeholder="Дата выписки с" end-placeholder="по" />
|
||||
</div>
|
||||
<NFormItem class="w-[340px]" label="Дата выписки" :show-feedback="false">
|
||||
<NDatePicker v-model:value="dateRange"
|
||||
@update:value="handleDateRangeChange"
|
||||
type="daterange"
|
||||
clearable
|
||||
format="dd.MM.yyyy"
|
||||
start-placeholder="Дата выписки с"
|
||||
end-placeholder="по"
|
||||
size="large"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem class="w-[340px]" label="Статус карты" :show-feedback="false">
|
||||
<NSelect :options="statuses"
|
||||
:value="statusValue"
|
||||
@update:value="val => handleStatusChange(val)"
|
||||
clearable
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</NFormItem>
|
||||
<div class="mt-6">
|
||||
<NDivider vertical />
|
||||
</div>
|
||||
<NFormItem class="w-[340px]" :show-feedback="false">
|
||||
<NButton type="primary"
|
||||
@click="ArchiveHistoryCreateModalShow = true"
|
||||
secondary
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
Добавить карту в архив
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NFlex>
|
||||
</NSpace>
|
||||
</template>
|
||||
<TableCards :filters="filters" :data="cards.data" :meta="cards.meta" min-height="calc(100vh - 277px)" max-height="calc(100vh - 320px)" />
|
||||
<TableCards :filters="filters"
|
||||
:data="cards.data"
|
||||
:meta="cards.meta"
|
||||
min-height="calc(100vh - 212px)"
|
||||
max-height="calc(100vh - 320px)"
|
||||
/>
|
||||
<ArchiveHistoryCreateModal v-model:open="ArchiveHistoryCreateModalShow" />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
:deep(.n-tabs .n-tabs-nav) {
|
||||
max-width: 420px;
|
||||
}
|
||||
</style>
|
||||
|
||||
22
resources/js/Plugins/naiveUI.js
Normal file
22
resources/js/Plugins/naiveUI.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {createDiscreteApi, darkTheme, lightTheme} from "naive-ui";
|
||||
|
||||
export function setupNaiveDiscreteApi(app) {
|
||||
const {message, notification, dialog, loadingBar} = createDiscreteApi(
|
||||
['message', 'dialog', 'notification', 'loadingBar'],
|
||||
{
|
||||
configProviderProps: {
|
||||
theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? darkTheme : lightTheme
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
window.$notification = notification
|
||||
window.$message = message
|
||||
window.$dialog = dialog
|
||||
window.$loadingBar = loadingBar
|
||||
|
||||
app.config.globalProperties.$notification = notification
|
||||
app.config.globalProperties.$message = message
|
||||
app.config.globalProperties.$dialog = dialog
|
||||
app.config.globalProperties.$loadingBar = loadingBar
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import '../css/app.css';
|
||||
import { createApp, h } from 'vue'
|
||||
import { createInertiaApp } from '@inertiajs/vue3'
|
||||
import {createPinia} from "pinia";
|
||||
import {setupNaiveDiscreteApi} from "./Plugins/naiveUI.js";
|
||||
|
||||
createInertiaApp({
|
||||
id: 'kartoteka',
|
||||
@@ -11,11 +12,17 @@ createInertiaApp({
|
||||
return pages[`./Pages/${name}.vue`]
|
||||
},
|
||||
setup({el, App, props, plugin}) {
|
||||
const vueApp = createApp({render: () => h(App, props)})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(pinia)
|
||||
.mount(el)
|
||||
vueApp.use(plugin)
|
||||
vueApp.use(pinia)
|
||||
|
||||
setupNaiveDiscreteApi(vueApp)
|
||||
|
||||
vueApp.mount(el)
|
||||
},
|
||||
}).then(r => {
|
||||
console.log('Инициализация прошла успешно')
|
||||
})
|
||||
|
||||
@@ -13,13 +13,25 @@ Route::prefix('si')->group(function () {
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('mis')->group(function () {
|
||||
Route::prefix('patients')->group(function () {
|
||||
Route::post('search', [\App\Http\Controllers\MisHistoryController::class, 'search']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('archive')->group(function () {
|
||||
Route::prefix('histories')->group(function () {
|
||||
Route::get('{id}', [\App\Http\Controllers\ArchiveHistoryController::class, 'show']);
|
||||
Route::prefix('move')->group(function () {
|
||||
Route::post('/', [\App\Http\Controllers\ArchiveHistoryController::class, 'move']);
|
||||
Route::post('/', [\App\Http\Controllers\ArchiveHistoryController::class, 'moveStore']);
|
||||
Route::post('{id}', [\App\Http\Controllers\ArchiveHistoryController::class, 'moveUpdate']);
|
||||
});
|
||||
Route::prefix('info')->group(function () {
|
||||
Route::post('{patientId}', [\App\Http\Controllers\ArchiveHistoryController::class, 'infoUpdate']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::post('create', [\App\Http\Controllers\ArchiveInfoController::class, 'store']);
|
||||
});
|
||||
|
||||
Route::prefix('orgs')->group(function () {
|
||||
|
||||
46
setup-fdw.md
Normal file
46
setup-fdw.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Подключение таблицы из реплики MIS
|
||||
|
||||
Подключаемся к БД от postgres
|
||||
```
|
||||
sudo su - postgres
|
||||
```
|
||||
|
||||
Выбираем рабочую БД
|
||||
```
|
||||
\c kartoteka
|
||||
```
|
||||
|
||||
Создаем расширение fdw
|
||||
```postgresql
|
||||
CREATE EXTENSION postgres_fdw;
|
||||
```
|
||||
|
||||
Указываем параметры подключения к серверу
|
||||
```postgresql
|
||||
CREATE SERVER mis_data
|
||||
FOREIGN DATA WRAPPER postgres_fdw
|
||||
OPTIONS (dbname 'database_name', host '127.0.0.1', port '5432');
|
||||
```
|
||||
|
||||
Делам маппинг пользователя для доступа к удаленному серверу
|
||||
```postgresql
|
||||
CREATE USER MAPPING FOR kartoteka
|
||||
SERVER mis_data
|
||||
OPTIONS (USER 'user_inner_db', password 'password_inner_db');
|
||||
```
|
||||
|
||||
Разрешаем пользователю ``kartoteka`` подключение к удаленному серверу
|
||||
```postgresql
|
||||
GRANT USAGE ON FOREIGN SERVER mis_data TO kartoteka;
|
||||
```
|
||||
|
||||
Подключаем таблицу ``stt_medicalhistory``
|
||||
```postgresql
|
||||
IMPORT FOREIGN SCHEMA public LIMIT TO (stt_medicalhistory)
|
||||
FROM SERVER mis_data INTO public;
|
||||
```
|
||||
|
||||
Если таблица ``stt_medicalhistory`` больше не нужна
|
||||
```postgresql
|
||||
DROP FOREIGN TABLE stt_medicalhistory;
|
||||
```
|
||||
2
storage/debugbar/.gitignore
vendored
Normal file
2
storage/debugbar/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
0
storage/logs/.gitignore
vendored
Normal file → Executable file
0
storage/logs/.gitignore
vendored
Normal file → Executable file
Reference in New Issue
Block a user