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 @@ 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 @@