diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php
index c422837..0101be8 100644
--- a/app/Http/Controllers/IndexController.php
+++ b/app/Http/Controllers/IndexController.php
@@ -13,17 +13,30 @@ class IndexController extends Controller
{
$pageSize = $request->get('page_size', 15);
$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');
- $cards = SttMedicalHistory::query();
+ $cardsQuery = SttMedicalHistory::query();
if (!empty($searchText)) {
- $cards = $cards->search($searchText);
+ $cardsQuery = $cardsQuery->search($searchText);
}
- $cards = SttMedicalHistoryResource::collection($cards->paginate($pageSize));
+ if (!empty($dateExtractFrom)) {
+ $cardsQuery = $cardsQuery->whereDate('dateextract', '>=', $dateExtractFrom);
+ if (!empty($dateExtractTo)) {
+ $cardsQuery = $cardsQuery->whereDate('dateextract', '<=', $dateExtractTo);
+ }
+ }
+
+ $cards = SttMedicalHistoryResource::collection($cardsQuery->paginate($pageSize));
return Inertia::render('Home/Index', [
'cards' => $cards,
+ 'filters' => $request->only([
+ 'search', 'date_extract_from', 'date_extract_to', 'page_size', 'page', 'view_type'
+ ]),
]);
}
}
diff --git a/app/Http/Controllers/MedicalHistoryController.php b/app/Http/Controllers/MedicalHistoryController.php
new file mode 100644
index 0000000..a23fced
--- /dev/null
+++ b/app/Http/Controllers/MedicalHistoryController.php
@@ -0,0 +1,28 @@
+get('view_type', 'si');
+ $patientId = $request->get('patient_id');
+
+ $patientInfo = null;
+ if ($viewType == 'si') {
+ $patient = SttMedicalHistory::where('id', $id)->first();
+ $archiveJournal = $patient->archiveHistory;
+
+ $patientInfo = [
+ 'info' => $patient,
+ 'journal' => $archiveJournal,
+ ];
+ }
+
+ return response()->json($patientInfo);
+ }
+}
diff --git a/app/Http/Resources/SI/SttMedicalHistoryResource.php b/app/Http/Resources/SI/SttMedicalHistoryResource.php
index d3d6b6b..d440a95 100644
--- a/app/Http/Resources/SI/SttMedicalHistoryResource.php
+++ b/app/Http/Resources/SI/SttMedicalHistoryResource.php
@@ -16,14 +16,15 @@ class SttMedicalHistoryResource extends JsonResource
public function toArray(Request $request): array
{
return [
+ 'id' => $this->id,
'fullname' => $this->getFullNameAttribute(),
- 'mpostdate' => Carbon::parse($this->mpostdate)->format('d.m.Y'),
- 'menddate' => Carbon::parse($this->menddate)->format('d.m.Y'),
+ 'daterecipient' => Carbon::parse($this->daterecipient)->format('d.m.Y'),
+ 'dateextract' => Carbon::parse($this->dateextract)->format('d.m.Y'),
'narhiv' => $this->narhiv,
'datearhiv' => Carbon::parse($this->datearhiv)->format('d.m.Y'),
'statgod' => $this->statgod,
'enp' => $this->enp,
- 'nkarta' => $this->nkarta,
+ 'medcardnum' => $this->medcardnum,
'dr' => Carbon::parse($this->dr)->format('d.m.Y'),
];
}
diff --git a/app/Models/ArchiveHistory.php b/app/Models/ArchiveHistory.php
index c7fd374..d4aa79d 100644
--- a/app/Models/ArchiveHistory.php
+++ b/app/Models/ArchiveHistory.php
@@ -11,9 +11,11 @@ class ArchiveHistory extends Model
'historyable_id',
'issue_at',
'return_at',
- 'name_org',
+ 'comment',
+ 'org_id',
'employee_name',
- 'employee_position',
+ 'employee_post',
+ 'has_lost',
];
public function historyable()
diff --git a/app/Models/Org.php b/app/Models/Org.php
new file mode 100644
index 0000000..8d03436
--- /dev/null
+++ b/app/Models/Org.php
@@ -0,0 +1,12 @@
+fam $this->im $this->ot";
+ return "$this->family $this->name $this->ot";
}
- public function getArchiveHistory()
+ public function archiveHistory()
{
return $this->morphMany(ArchiveHistory::class, 'historyable');
}
@@ -37,12 +37,12 @@ class SttMedicalHistory extends Model
{
return $query->where(function($q) use ($searchText) {
if (is_numeric($searchText)) {
- $q->where('nkarta', 'LIKE', "$searchText%");
+ $q->where('medcardnum', 'ILIKE', "$searchText%");
} else {
// Ищем по всем частям ФИО
- $q->where('fam', 'LIKE', "%$searchText%")
- ->orWhere('im', 'LIKE', "%$searchText%")
- ->orWhere('ot', 'LIKE', "%$searchText%");
+ $q->where('family', 'ILIKE', "%$searchText%")
+ ->orWhere('name', 'ILIKE', "%$searchText%")
+ ->orWhere('ot', 'ILIKE', "%$searchText%");
}
});
}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 6e19349..49c7a38 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -8,6 +8,7 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
+ api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
diff --git a/composer.json b/composer.json
index 9504a27..3a5aceb 100644
--- a/composer.json
+++ b/composer.json
@@ -9,6 +9,7 @@
"php": "^8.2",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
+ "laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index 3b3e7ff..e89e1ac 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "9cc5ea89dc281a98d5bcb5bf7755b102",
+ "content-hash": "4ecf76c8e987c4f4186a17e150248317",
"packages": [
{
"name": "brick/math",
@@ -1400,6 +1400,69 @@
},
"time": "2025-11-21T20:52:52+00:00"
},
+ {
+ "name": "laravel/sanctum",
+ "version": "v4.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/sanctum.git",
+ "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664",
+ "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "illuminate/console": "^11.0|^12.0",
+ "illuminate/contracts": "^11.0|^12.0",
+ "illuminate/database": "^11.0|^12.0",
+ "illuminate/support": "^11.0|^12.0",
+ "php": "^8.2",
+ "symfony/console": "^7.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^9.15|^10.8",
+ "phpstan/phpstan": "^1.10"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Sanctum\\SanctumServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Sanctum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
+ "keywords": [
+ "auth",
+ "laravel",
+ "sanctum"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/sanctum/issues",
+ "source": "https://github.com/laravel/sanctum"
+ },
+ "time": "2025-11-21T13:59:03+00:00"
+ },
{
"name": "laravel/serializable-closure",
"version": "v2.0.7",
@@ -9407,12 +9470,12 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
- "platform-dev": [],
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/config/sanctum.php b/config/sanctum.php
new file mode 100644
index 0000000..44527d6
--- /dev/null
+++ b/config/sanctum.php
@@ -0,0 +1,84 @@
+ explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
+ '%s%s',
+ 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
+ Sanctum::currentApplicationUrlWithPort(),
+ // Sanctum::currentRequestHost(),
+ ))),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Sanctum Guards
+ |--------------------------------------------------------------------------
+ |
+ | This array contains the authentication guards that will be checked when
+ | Sanctum is trying to authenticate a request. If none of these guards
+ | are able to authenticate the request, Sanctum will use the bearer
+ | token that's present on an incoming request for authentication.
+ |
+ */
+
+ 'guard' => ['web'],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Expiration Minutes
+ |--------------------------------------------------------------------------
+ |
+ | This value controls the number of minutes until an issued token will be
+ | considered expired. This will override any values set in the token's
+ | "expires_at" attribute, but first-party sessions are not affected.
+ |
+ */
+
+ 'expiration' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Token Prefix
+ |--------------------------------------------------------------------------
+ |
+ | Sanctum can prefix new tokens in order to take advantage of numerous
+ | security scanning initiatives maintained by open source platforms
+ | that notify developers if they commit tokens into repositories.
+ |
+ | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
+ |
+ */
+
+ 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Sanctum Middleware
+ |--------------------------------------------------------------------------
+ |
+ | When authenticating your first-party SPA with Sanctum you may need to
+ | customize some of the middleware Sanctum uses while processing the
+ | request. You may change the middleware listed below as required.
+ |
+ */
+
+ 'middleware' => [
+ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
+ 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
+ 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
+ ],
+
+];
diff --git a/database/migrations/2025_11_30_063144_create_orgs_table.php b/database/migrations/2025_11_30_063144_create_orgs_table.php
new file mode 100644
index 0000000..e5c47a2
--- /dev/null
+++ b/database/migrations/2025_11_30_063144_create_orgs_table.php
@@ -0,0 +1,28 @@
+id();
+ $table->string('name');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('orgs');
+ }
+};
diff --git a/database/migrations/2025_11_30_112144_create_archive_histories_table.php b/database/migrations/2025_11_30_112144_create_archive_histories_table.php
index 40a2d60..3ef879f 100644
--- a/database/migrations/2025_11_30_112144_create_archive_histories_table.php
+++ b/database/migrations/2025_11_30_112144_create_archive_histories_table.php
@@ -16,9 +16,11 @@ return new class extends Migration
$table->morphs('historyable');
$table->timestamp('issue_at');
$table->timestamp('return_at')->nullable();
- $table->string('name_org');
+ $table->text('comment')->nullable();
+ $table->foreignIdFor(\App\Models\Org::class, 'org_id');
$table->string('employee_name');
- $table->string('employee_position')->nullable();
+ $table->string('employee_post')->nullable();
+ $table->boolean('has_lost')->default(false);
$table->timestamps();
});
}
diff --git a/database/migrations/2025_12_02_070245_create_personal_access_tokens_table.php b/database/migrations/2025_12_02_070245_create_personal_access_tokens_table.php
new file mode 100644
index 0000000..40ff706
--- /dev/null
+++ b/database/migrations/2025_12_02_070245_create_personal_access_tokens_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->morphs('tokenable');
+ $table->text('name');
+ $table->string('token', 64)->unique();
+ $table->text('abilities')->nullable();
+ $table->timestamp('last_used_at')->nullable();
+ $table->timestamp('expires_at')->nullable()->index();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('personal_access_tokens');
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index a0de4e4..8451b1a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"@inertiajs/vue3": "^2.2.19",
"@vitejs/plugin-vue": "^6.0.2",
"@vueuse/core": "^14.1.0",
+ "date-fns": "^4.1.0",
"pinia": "^3.0.4",
"ufo": "^1.6.1",
"vue": "^3.5.25"
@@ -1655,7 +1656,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
- "dev": true,
"license": "MIT",
"funding": {
"type": "github",
diff --git a/package.json b/package.json
index f4131de..c256123 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@inertiajs/vue3": "^2.2.19",
"@vitejs/plugin-vue": "^6.0.2",
"@vueuse/core": "^14.1.0",
+ "date-fns": "^4.1.0",
"pinia": "^3.0.4",
"ufo": "^1.6.1",
"vue": "^3.5.25"
diff --git a/resources/js/Composables/useMedicalHistoryFilter.js b/resources/js/Composables/useMedicalHistoryFilter.js
new file mode 100644
index 0000000..732e57e
--- /dev/null
+++ b/resources/js/Composables/useMedicalHistoryFilter.js
@@ -0,0 +1,222 @@
+// 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";
+
+export const useMedicalHistoryFilter = (filters) => {
+ 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'
+ })
+
+ const meta = computed(() => page.props.cards?.meta || {})
+ const isLoading = ref(false)
+
+ // Форматирование даты для URL
+ const formatDateForUrl = (date) => {
+ if (!date) return null
+ if (date instanceof Date) {
+ return format(date, 'yyyy-MM-dd')
+ }
+ return date
+ }
+
+ // Навигация с фильтрами
+ const applyFilters = (updates = {}, resetPage = false) => {
+ // Обновляем фильтры
+ Object.assign(filtersRef.value, updates)
+
+ // Если сбрасываем фильтры, обнуляем страницу
+ if (resetPage) {
+ filtersRef.value.page = 1
+ }
+
+ // Подготавливаем параметры для URL
+ const params = {
+ search: filtersRef.value.search || null,
+ date_extract_from: formatDateForUrl(filtersRef.value.date_extract_from),
+ date_extract_to: formatDateForUrl(filtersRef.value.date_extract_to),
+ page: filtersRef.value.page,
+ page_size: filtersRef.value.page_size,
+ sort_by: filtersRef.value.sort_by,
+ sort_order: filtersRef.value.sort_order,
+ view_type: filtersRef.value.view_type,
+ }
+
+ // Очищаем пустые значения
+ const cleanParams = Object.fromEntries(
+ Object.entries(params).filter(([_, value]) =>
+ value !== undefined && value !== null && value !== ''
+ )
+ )
+
+ const query = stringifyQuery(cleanParams)
+
+ isLoading.value = true
+ router.visit(`/${query ? `?${query}` : ''}`, {
+ preserveState: true,
+ preserveScroll: true,
+ onFinish: () => {
+ isLoading.value = false
+ }
+ })
+ }
+
+ // Дебаунсированный поиск
+ const debouncedSearch = useDebounceFn((value) => {
+ applyFilters({ search: value }, true)
+ }, 500)
+
+ // Обработчики событий
+ const handleSearch = (value) => {
+ filtersRef.value.search = value
+ debouncedSearch(value)
+ }
+
+ // Конвертация строки даты в timestamp для NaiveUI
+ const convertToTimestamp = (dateString) => {
+ if (!dateString) return null
+
+ try {
+ // Предполагаем формат yyyy-MM-dd
+ const date = parse(dateString, 'yyyy-MM-dd', new Date())
+ return isValid(date) ? date.getTime() : null
+ } catch (error) {
+ console.error('Error converting date to timestamp:', error)
+ return null
+ }
+ }
+
+ // Конвертация timestamp в строку для URL
+ const convertTimestampToString = (timestamp) => {
+ if (!timestamp) return null
+
+ try {
+ const date = new Date(timestamp)
+ return isValid(date) ? format(date, 'yyyy-MM-dd') : null
+ } catch (error) {
+ console.error('Error converting timestamp to string:', error)
+ return null
+ }
+ }
+
+ const dateRange = ref([
+ filtersRef.value.date_extract_from ? convertToTimestamp(filtersRef.value.date_extract_from) : null,
+ filtersRef.value.date_extract_to ? convertToTimestamp(filtersRef.value.date_extract_to) : null
+ ])
+
+ const handleDateRangeChange = (timestamps) => {
+ dateRange.value = timestamps || [null, null]
+
+ const updates = {
+ date_extract_from: timestamps?.[0] ? convertTimestampToString(timestamps[0]) : null,
+ date_extract_to: timestamps?.[1] ? convertTimestampToString(timestamps[1]) : null
+ }
+
+ Object.assign(filtersRef.value, updates)
+ applyFilters(updates, true)
+ }
+
+ const handleStatusChange = (status) => {
+ applyFilters({ status }, true)
+ }
+
+ const handlePageChange = (page) => {
+ applyFilters({ page })
+ }
+
+ 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
+ })
+ }
+
+ const resetAllFilters = () => {
+ filtersRef.value = {
+ search: '',
+ date_extract_from: null,
+ date_extract_to: null,
+ page: 1,
+ page_size: 15,
+ sort_by: 'created_at',
+ sort_order: 'desc',
+ view_type: 'archive',
+ }
+ applyFilters({}, true)
+ }
+
+ // Активные фильтры для отображения
+ const activeFilters = computed(() => {
+ const active = []
+ if (filtersRef.value.search) {
+ active.push({ key: 'search', label: `Поиск: ${filtersRef.value.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}` })
+ }
+ // Добавьте другие фильтры по необходимости
+ return active
+ })
+
+ // Следим за изменениями в page.props.filters
+ watch(
+ () => page.props.filters,
+ (newFilters) => {
+ if (newFilters) {
+ // Сохраняем все фильтры, включая search!
+ filtersRef.value = {
+ ...filtersRef.value,
+ ...newFilters
+ }
+
+ // Синхронизируем dateRange с фильтрами
+ const formattedDate = [
+ newFilters.date_extract_from ? convertToTimestamp(newFilters.date_extract_from) : null,
+ newFilters.date_extract_to ? convertToTimestamp(newFilters.date_extract_to) : null
+ ]
+
+ dateRange.value = formattedDate.every(date => date === null) ? null : formattedDate
+ }
+ },
+ { deep: true, immediate: true }
+ )
+
+ return {
+ filtersRef,
+ meta,
+ isLoading,
+ activeFilters,
+ dateRange,
+ handleViewTypeChange,
+ handleSearch,
+ handleDateRangeChange,
+ handleStatusChange,
+ handlePageChange,
+ handlePageSizeChange,
+ handleSortChange,
+ resetAllFilters,
+ applyFilters
+ }
+}
diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue
index 81966dc..3aa0320 100644
--- a/resources/js/Layouts/AppLayout.vue
+++ b/resources/js/Layouts/AppLayout.vue
@@ -26,7 +26,7 @@ const themeOverrides = {
>
-
+
diff --git a/resources/js/Pages/Home/ArchiveHistoryModal/Index.vue b/resources/js/Pages/Home/ArchiveHistoryModal/Index.vue
index 4a4aff2..a924eaf 100644
--- a/resources/js/Pages/Home/ArchiveHistoryModal/Index.vue
+++ b/resources/js/Pages/Home/ArchiveHistoryModal/Index.vue
@@ -1,13 +1,70 @@
-
+
-
+ {{ patient.info?.medcardnum }} {{ patient.info?.family }} {{ patient.info?.name }} {{ patient.info?.ot }}
+
diff --git a/resources/js/Pages/Home/DataTable/Index.vue b/resources/js/Pages/Home/DataTable/Index.vue
index c1366e2..e72e8dc 100644
--- a/resources/js/Pages/Home/DataTable/Index.vue
+++ b/resources/js/Pages/Home/DataTable/Index.vue
@@ -1,17 +1,17 @@
-
+
+