From d4f077cdaf126dc490f990b143e5b51aa8523508 Mon Sep 17 00:00:00 2001 From: brusnitsyn Date: Sun, 11 Jan 2026 23:37:18 +0900 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=BE=D0=BB=D0=B8,=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BB=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D0=B5=D1=82=D0=B0,=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=20=D0=B3?= =?UTF-8?q?=D0=BB=D0=B0=D0=B2=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/ReportController.php | 151 +++++++++++ app/Http/Controllers/Api/RoleController.php | 33 +++ app/Http/Controllers/Web/IndexController.php | 4 +- app/Http/Controllers/Web/ReportController.php | 24 ++ .../Controllers/Web/StatisticController.php | 62 +++++ app/Http/Middleware/HandleInertiaRequests.php | 1 + .../Mis/FormattedPatientResource.php | 27 ++ app/Models/Department.php | 24 +- app/Models/DepartmentMetrikaDefault.php | 22 ++ app/Models/DepartmentType.php | 16 ++ app/Models/Lpu.php | 23 -- app/Models/MetrikaGroup.php | 18 +- app/Models/MetrikaResult.php | 19 +- app/Models/MetrikaResultValue.php | 1 + app/Models/MisMedicalHistory.php | 21 ++ app/Models/ObservationPatient.php | 18 ++ app/Models/Report.php | 5 + app/Models/Role.php | 37 +++ app/Models/User.php | 58 +++-- app/Models/UserRole.php | 30 +++ .../0001_01_01_000000_create_users_table.php | 15 ++ ...2025_12_27_055719_create_reports_table.php | 2 +- ...27_060129_create_metrika_results_table.php | 8 +- ...302_create_metrika_result_values_table.php | 3 + ...9_154100_create_department_types_table.php | 28 +++ ..._01_09_154110_create_departments_table.php | 30 +++ ...eate_department_metrika_defaults_table.php | 29 +++ ...0432_create_observation_patients_table.php | 30 +++ database/seeders/DatabaseSeeder.php | 2 +- database/seeders/TestDepartmentDataSeeder.php | 145 +++++++++++ database/seeders/TestLpuDataSeeder.php | 46 ---- database/seeders/TestMetrikaSeeder.php | 28 +++ database/seeders/TestUserSeeder.php | 33 +++ docker/app.conf | 114 --------- docker/nginx.conf | 28 --- package-lock.json | 235 ++++++++++++++++++ package.json | 2 + resources/css/app.css | 53 ++-- resources/js/Layouts/AppLayout.vue | 2 +- resources/js/Layouts/Components/AppHeader.vue | 17 +- .../js/Layouts/Components/AppHeaderRole.vue | 35 +++ .../js/Layouts/Components/AppUserButton.vue | 15 +- resources/js/Pages/Index.vue | 9 +- resources/js/Pages/Path/Patient.vue | 116 +++++++++ .../js/Pages/Report/Components/ReportForm.vue | 42 ++++ .../Report/Components/ReportFormInput.vue | 25 ++ .../Pages/Report/Components/ReportHeader.vue | 88 +++++++ .../Pages/Report/Components/ReportSection.vue | 90 +++++++ .../Report/Components/ReportSectionHeader.vue | 55 ++++ .../Report/Components/ReportSectionItem.vue | 166 +++++++++++++ resources/js/Pages/Report/Index.vue | 17 +- resources/js/Pages/Statistic/Index.vue | 167 +++++++++---- resources/js/Pages/Statistic/IndexOld.vue | 58 +++++ resources/js/Stores/auth.js | 4 +- resources/js/Stores/report.js | 87 ++++++- resources/js/app.js | 5 +- resources/views/app.blade.php | 13 +- routes/api.php | 21 ++ routes/web.php | 8 + 59 files changed, 2099 insertions(+), 366 deletions(-) create mode 100644 app/Http/Controllers/Api/ReportController.php create mode 100644 app/Http/Controllers/Api/RoleController.php create mode 100644 app/Http/Controllers/Web/ReportController.php create mode 100644 app/Http/Resources/Mis/FormattedPatientResource.php create mode 100644 app/Models/DepartmentMetrikaDefault.php create mode 100644 app/Models/DepartmentType.php delete mode 100644 app/Models/Lpu.php create mode 100644 app/Models/MisMedicalHistory.php create mode 100644 app/Models/ObservationPatient.php create mode 100644 app/Models/Role.php create mode 100644 app/Models/UserRole.php create mode 100644 database/migrations/2026_01_09_154100_create_department_types_table.php create mode 100644 database/migrations/2026_01_09_154110_create_departments_table.php create mode 100644 database/migrations/2026_01_09_220419_create_department_metrika_defaults_table.php create mode 100644 database/migrations/2026_01_11_130432_create_observation_patients_table.php create mode 100644 database/seeders/TestDepartmentDataSeeder.php delete mode 100644 database/seeders/TestLpuDataSeeder.php create mode 100644 resources/js/Layouts/Components/AppHeaderRole.vue create mode 100644 resources/js/Pages/Path/Patient.vue create mode 100644 resources/js/Pages/Report/Components/ReportForm.vue create mode 100644 resources/js/Pages/Report/Components/ReportFormInput.vue create mode 100644 resources/js/Pages/Report/Components/ReportHeader.vue create mode 100644 resources/js/Pages/Report/Components/ReportSection.vue create mode 100644 resources/js/Pages/Report/Components/ReportSectionHeader.vue create mode 100644 resources/js/Pages/Report/Components/ReportSectionItem.vue create mode 100644 resources/js/Pages/Statistic/IndexOld.vue diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php new file mode 100644 index 0000000..bd7e554 --- /dev/null +++ b/app/Http/Controllers/Api/ReportController.php @@ -0,0 +1,151 @@ +user(); + $department = $user->department; + + $beds = (int)$department->metrikaDefault()->where('rf_metrika_item_id', 1)->first()->value; + $occupiedBeds = optional(Report::where('rf_department_id', $user->rf_department_id) + ->join('metrika_results', 'reports.report_id', '=', 'metrika_results.rf_report_id') + ->where('metrika_results.rf_metrika_item_id', 8) + ->orderBy('sent_at', 'desc')->first())->value ?? 0; + + $percentLoadedBeds = intval($occupiedBeds) * 100 / $beds; + + $metrikaGroup = MetrikaGroup::whereMetrikaGroupId(2)->first(); + $metrikaItems = $metrikaGroup->metrikaItems; + + return response()->json([ + 'department' => [ + 'beds' => $beds, + 'percentLoadedBeds' => $percentLoadedBeds, + ], + 'metrikaItems' => $metrikaItems + ]); + } + + public function store(Request $request) + { + $data = $request->validate([ + 'metrics' => 'required', + 'observationPatients' => 'nullable', + 'departmentId' => 'required|integer', + ]); + + $metrics = $data['metrics']; + $observationPatients = $data['observationPatients']; + + $metriks = []; + foreach ($metrics as $key => $value) { + $metrika = new MetrikaResult; + $metrikaId = (int)Str::replace('metrika_item_', '', $key); + $metrika->rf_metrika_item_id = $metrikaId; + $metrika->value = $value; + + $metriks[] = $metrika; + } + + \DB::beginTransaction(); + + $report = Report::create([ + 'rf_department_id' => $data['departmentId'], + 'rf_user_id' => Auth::user()->id, + 'created_at' => now(), + 'sent_at' => now() + ]); + + foreach ($metriks as $metrika) { + $metrika->rf_report_id = $report->report_id; + $metrika->save(); + } + + foreach ($observationPatients as $observationPatient) { + ObservationPatient::create([ + 'rf_department_id' => $data['departmentId'], + 'rf_report_id' => $report->report_id, + 'rf_medicalhistory_id' => $observationPatient['id'], + 'rf_mkab_id' => null + ]); + } + + \DB::commit(); + + return response()->json([ + 'message' => 'success' + ]); + } + + public function getPatients(Request $request) + { + $data = $request->validate([ + 'status' => 'required|string', // plan emergency + ]); + + $status = $data['status']; + + $model = new MisMedicalHistory(); + if ($status === 'plan') { + $patients = MisMedicalHistory::select( + [ + ...$model->getFillable(), + DB::raw('ROW_NUMBER() OVER (ORDER BY "DateRecipient" DESC) as num') + ]) + ->where('rf_EmerSignID', 1) + ->orderBy('DateRecipient', 'DESC') + ->get(); + } else if ($status === 'emergency') { + $patients = MisMedicalHistory::select( + [ + ...$model->getFillable(), + DB::raw('ROW_NUMBER() OVER (ORDER BY "DateRecipient" DESC) as num') + ]) + ->where('rf_EmerSignID', 2) + ->orderBy('DateRecipient', 'DESC') + ->get(); + } + + return response()->json(FormattedPatientResource::collection($patients)); + } + + public function getPatientsCount(Request $request) + { + $data = $request->validate([ + 'status' => 'required|string', // plan emergency + ]); + + $status = $data['status']; + + $model = new MisMedicalHistory(); + if ($status === 'plan') { + $count = MisMedicalHistory::select($model->getFillable()) + ->where('rf_EmerSignID', 1) + ->orderBy('DateRecipient', 'DESC') + ->count(); + } else if ($status === 'emergency') { + $count = MisMedicalHistory::select($model->getFillable()) + ->where('rf_EmerSignID', 2) + ->orderBy('DateRecipient', 'DESC') + ->count(); + } + + return response()->json($count); + } +} diff --git a/app/Http/Controllers/Api/RoleController.php b/app/Http/Controllers/Api/RoleController.php new file mode 100644 index 0000000..769c305 --- /dev/null +++ b/app/Http/Controllers/Api/RoleController.php @@ -0,0 +1,33 @@ +user()->roles; + + return response()->json( + $roles + ); + } + + public function setUserRole(Request $request) + { + $data = $request->validate([ + 'role_id' => 'required|integer|exists:roles,id' + ]); + } + + public function getRoles() + { + $roles = Role::all(); + + return response()->json($roles); + } +} diff --git a/app/Http/Controllers/Web/IndexController.php b/app/Http/Controllers/Web/IndexController.php index e51e6e3..74cabd4 100644 --- a/app/Http/Controllers/Web/IndexController.php +++ b/app/Http/Controllers/Web/IndexController.php @@ -20,9 +20,7 @@ class IndexController extends Controller $fillableModel = - $departments = Department::with(['lpu'])->whereHas('lpu', function ($query) { - $query->where('mainlpuid', 1); - })->get(); + $departments = Department::all(); return Inertia::render('Report/Index', [ 'depatments' => $departments, diff --git a/app/Http/Controllers/Web/ReportController.php b/app/Http/Controllers/Web/ReportController.php new file mode 100644 index 0000000..aef8976 --- /dev/null +++ b/app/Http/Controllers/Web/ReportController.php @@ -0,0 +1,24 @@ +user(); + $department = $user->department; + + $beds = $department->metrikaDefault()->where('rf_metrika_item_id', 1)->first()->value; + + return Inertia::render('Report/Index', [ + 'department' => [ + 'beds' => $beds + ], + ]); + } +} diff --git a/app/Http/Controllers/Web/StatisticController.php b/app/Http/Controllers/Web/StatisticController.php index 5bc56e7..8234a8b 100644 --- a/app/Http/Controllers/Web/StatisticController.php +++ b/app/Http/Controllers/Web/StatisticController.php @@ -3,9 +3,13 @@ namespace App\Http\Controllers\Web; use App\Http\Controllers\Controller; +use App\Models\Department; use App\Models\MetrikaForm; use App\Models\MetrikaGroup; use App\Models\MetrikaItem; +use App\Models\MetrikaResult; +use App\Models\Report; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -15,6 +19,64 @@ use Inertia\Inertia; class StatisticController extends Controller { public function index(Request $request) + { + $user = $request->user(); + + $userDepartment = $user->department; + + $data = []; + + $departments = Department::select('department_id', 'name_short')->get(); + + foreach ($departments as $department) { + $allCount = MetrikaResult::whereHas('report', function ($query) use ($userDepartment, $department) { + $query->where('rf_department_id', $department->department_id); + })->where('rf_metrika_item_id', 3) + ->sum(DB::raw('value::integer')); + + $leaveCount = MetrikaResult::whereHas('report', function ($query) use ($userDepartment, $department) { + $query->where('rf_department_id', $department->department_id); + })->where('rf_metrika_item_id', 7) + ->sum(DB::raw('value::integer')); + + $consistCount = optional(MetrikaResult::where('rf_metrika_item_id', 8) + ->whereHas('report', function (Builder $query) use ($department) { + $query->where('rf_department_id', $department->department_id); + })->join('reports', 'metrika_results.rf_report_id', '=', 'reports.report_id') + ->select('metrika_results.value') + ->orderBy('reports.sent_at', 'desc') + )->value('value') ?? 0; + + $beds = (int)optional($department->metrikaDefault() + ->where('rf_metrika_item_id', 1) + ->first())->value ?? 0; + + $occupiedBeds = (int)optional(Report::where('rf_department_id', $department->department_id) + ->join('metrika_results', 'reports.report_id', '=', 'metrika_results.rf_report_id') + ->where('metrika_results.rf_metrika_item_id', 8) + ->orderBy('reports.sent_at', 'desc') + ->first())->value ?? 0; + + $percentLoadedBeds = $beds > 0 ? $occupiedBeds * 100 / $beds : 0; + + $data[] = [ + 'department' => $department->name_short, + 'beds' => $beds, + 'all' => $allCount, + 'plan' => '0', + 'emergency' => '0', + 'leave' => $leaveCount, + 'consist' => $consistCount, + 'percentLoadedBeds' => $percentLoadedBeds, + ]; + } + + return Inertia::render('Statistic/Index', [ + 'data' => $data + ]); + } + + public function indexOld(Request $request) { $user = Auth::user(); diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index cc417e6..56f8aff 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -44,6 +44,7 @@ class HandleInertiaRequests extends Middleware 'name' => $user->name, 'token' => Session::get('token'), 'permissions' => $user->permissions(), + 'role' => $user->currentRole(), 'available_departments' => $user->availableDepartments(), 'current_department' => $user->department ] : null, diff --git a/app/Http/Resources/Mis/FormattedPatientResource.php b/app/Http/Resources/Mis/FormattedPatientResource.php new file mode 100644 index 0000000..58ffa8d --- /dev/null +++ b/app/Http/Resources/Mis/FormattedPatientResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->MedicalHistoryID, + 'num' => $this->num, + 'fullname' => Str::ucwords(Str::lower("$this->FAMILY $this->Name $this->OT")), + 'age' => Carbon::parse($this->BD)->diff(Carbon::now())->format('%y'), + 'birth_date' => Carbon::parse($this->BD)->format('d.m.Y'), + ]; + } +} diff --git a/app/Models/Department.php b/app/Models/Department.php index 471b41d..46e4cb1 100644 --- a/app/Models/Department.php +++ b/app/Models/Department.php @@ -6,17 +6,29 @@ use Illuminate\Database\Eloquent\Model; class Department extends Model { - protected $table = 'oms_department'; - protected $primaryKey = 'departmentid'; public $timestamps = false; + protected $primaryKey = 'department_id'; + protected $fillable = [ - 'departmentname', - 'rf_lpuid' + 'name_full', + 'name_short', + 'rf_mis_department_id', + 'rf_department_type' ]; - public function lpu(): \Illuminate\Database\Eloquent\Relations\HasOne + public function metrikaDefault() { - return $this->hasOne(Lpu::class, 'lpuid', 'rf_lpuid'); + return $this->hasMany(DepartmentMetrikaDefault::class, 'rf_department_id', 'department_id'); + } + + public function observationPatients(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ObservationPatient::class, 'rf_department_id', 'department_id'); + } + + public function reports(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Report::class, 'rf_department_id', 'department_id'); } } diff --git a/app/Models/DepartmentMetrikaDefault.php b/app/Models/DepartmentMetrikaDefault.php new file mode 100644 index 0000000..1a66fe7 --- /dev/null +++ b/app/Models/DepartmentMetrikaDefault.php @@ -0,0 +1,22 @@ +belongsTo(Department::class, 'rf_department_id'); + } +} diff --git a/app/Models/DepartmentType.php b/app/Models/DepartmentType.php new file mode 100644 index 0000000..337b8bc --- /dev/null +++ b/app/Models/DepartmentType.php @@ -0,0 +1,16 @@ +hasMany(Department::class, 'lpuid', 'rf_lpuid'); - } -} diff --git a/app/Models/MetrikaGroup.php b/app/Models/MetrikaGroup.php index be815c2..7b3ca30 100644 --- a/app/Models/MetrikaGroup.php +++ b/app/Models/MetrikaGroup.php @@ -10,8 +10,24 @@ class MetrikaGroup extends Model public $timestamps = false; protected $fillable = [ - 'metrika_group_id', 'name', 'description', ]; + + public function groupItems() + { + return $this->hasMany(MetrikaGroupItem::class, 'rf_metrika_group_id', 'metrika_group_id'); + } + + public function metrikaItems() + { + return $this->hasManyThrough( + MetrikaItem::class, + MetrikaGroupItem::class, + 'rf_metrika_group_id', + 'metrika_item_id', + 'metrika_group_id', + 'rf_metrika_item_id', + ); + } } diff --git a/app/Models/MetrikaResult.php b/app/Models/MetrikaResult.php index c68a233..768248e 100644 --- a/app/Models/MetrikaResult.php +++ b/app/Models/MetrikaResult.php @@ -11,8 +11,9 @@ class MetrikaResult extends Model public $timestamps = false; protected $fillable = [ + 'rf_metrika_item_id', 'rf_report_id', - 'rf_metrika_group_id', + 'value' ]; public function report(): \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -20,13 +21,13 @@ class MetrikaResult extends Model return $this->belongsTo(Report::class, 'rf_report_id', 'report_id'); } - public function group(): \Illuminate\Database\Eloquent\Relations\BelongsTo - { - return $this->belongsTo(MetrikaGroup::class, 'rf_metrika_group_id', 'metrika_group_id'); - } +// public function group(): \Illuminate\Database\Eloquent\Relations\BelongsTo +// { +// return $this->belongsTo(MetrikaGroup::class, 'rf_metrika_group_id', 'metrika_group_id'); +// } - public function values(): HasMany - { - return $this->hasMany(MetrikaResultValue::class, 'rf_metrika_result_id', 'metrika_result_id'); - } +// public function values(): HasMany +// { +// return $this->hasMany(MetrikaResultValue::class, 'rf_metrika_result_id', 'metrika_result_id'); +// } } diff --git a/app/Models/MetrikaResultValue.php b/app/Models/MetrikaResultValue.php index cc46e40..3709bcf 100644 --- a/app/Models/MetrikaResultValue.php +++ b/app/Models/MetrikaResultValue.php @@ -12,6 +12,7 @@ class MetrikaResultValue extends Model protected $fillable = [ 'rf_metrika_result_id', 'rf_metrika_item_id', + 'rf_report_id', 'value', ]; diff --git a/app/Models/MisMedicalHistory.php b/app/Models/MisMedicalHistory.php new file mode 100644 index 0000000..b4f2720 --- /dev/null +++ b/app/Models/MisMedicalHistory.php @@ -0,0 +1,21 @@ +hasMany(MetrikaResult::class, 'rf_report_id', 'report_id'); } + + public function observationPatients(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ObservationPatient::class, 'rf_report_id', 'report_id'); + } } diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 0000000..b91cf7c --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,37 @@ +hasMany(UserRole::class, 'rf_role_id', 'role_id'); + } + + public function users(): HasManyThrough + { + return $this->hasManyThrough( + User::class, + UserRole::class, + 'rf_role_id', + 'id', + 'role_id', + 'rf_user_id' + ); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3d42a04..870e690 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,9 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -55,49 +58,60 @@ class User extends Authenticatable return $this->belongsTo(Department::class, 'rf_department_id'); } + public function userRoles(): HasMany + { + return $this->hasMany(UserRole::class, 'rf_user_id', 'id'); + } + + public function roles(): HasManyThrough + { + return $this->hasManyThrough( + Role::class, + UserRole::class, + 'rf_user_id', + 'role_id', + 'id', + 'rf_role_id' + ); + } + + public function currentRole() + { + $defaultRoleId = $this->roles()->where('is_default', true)->first()->role_id; + $roleId = session('currentRoleId', $defaultRoleId); + + $role = Role::where('role_id', $roleId)->first(); + + return $role; + } + // Методы для проверки ролей public function isAdmin() { - return $this->role === 'admin'; + return $this->currentRole()->slug === 'admin'; } public function isDoctor() { - return $this->role === 'doctor'; - } - - public function isNurse() - { - return $this->role === 'nurse'; + return $this->currentRole()->slug === 'doctor'; } public function isHeadOfDepartment() { - return $this->role === 'head_of_department'; + return $this->currentRole()->slug === 'head_of_department'; } public function isStatistician() { - return $this->role === 'statistician'; + return $this->currentRole()->slug === 'statistician'; } // Получение доступных отделений public function availableDepartments() { - $departments = [ - 'Гематология', - 'Хирургия', - 'Терапия', - 'Реанимация', - 'Онкология', - 'Кардиология', - 'Неврология', - 'Урология', - 'Гинекология', - 'Лаборатория' - ]; + $departments = Department::all(); - if ($this->isAdmin() || $this->isHeadOfDepartment()) { + if ($this->isAdmin()) { return $departments; } diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php new file mode 100644 index 0000000..a74675b --- /dev/null +++ b/app/Models/UserRole.php @@ -0,0 +1,30 @@ +belongsTo(User::class, 'rf_user_id', 'id'); + } + + public function role(): BelongsTo + { + return $this->belongsTo(Role::class, 'rf_role_id', 'role_id'); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 8a12ca4..27ba876 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -11,6 +11,13 @@ return new class extends Migration */ public function up(): void { + Schema::create('roles', function (Blueprint $table) { + $table->id('role_id'); + $table->string('name'); + $table->string('slug'); + $table->boolean('is_active')->default(true); + }); + Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); @@ -23,6 +30,14 @@ return new class extends Migration $table->timestamps(); }); + Schema::create('user_roles', function (Blueprint $table) { + $table->id('user_role_id'); + $table->foreignIdFor(\App\Models\User::class, 'rf_user_id'); + $table->foreignIdFor(\App\Models\Role::class, 'rf_role_id'); + $table->boolean('is_active')->default(true); + $table->boolean('is_default')->default(false); + }); + Schema::create('password_reset_tokens', function (Blueprint $table) { $table->string('login')->primary(); $table->string('token'); diff --git a/database/migrations/2025_12_27_055719_create_reports_table.php b/database/migrations/2025_12_27_055719_create_reports_table.php index 272570d..bbf806e 100644 --- a/database/migrations/2025_12_27_055719_create_reports_table.php +++ b/database/migrations/2025_12_27_055719_create_reports_table.php @@ -14,7 +14,7 @@ return new class extends Migration Schema::create('reports', function (Blueprint $table) { $table->id('report_id'); $table->date('created_at'); - $table->date('sent_at')->nullable(); + $table->dateTime('sent_at')->nullable(); $table->foreignIdFor(\App\Models\Department::class, 'rf_department_id')->default(1); $table->foreignIdFor(\App\Models\User::class, 'rf_user_id')->nullable(); }); diff --git a/database/migrations/2025_12_27_060129_create_metrika_results_table.php b/database/migrations/2025_12_27_060129_create_metrika_results_table.php index 6a7d5b7..f1e8195 100644 --- a/database/migrations/2025_12_27_060129_create_metrika_results_table.php +++ b/database/migrations/2025_12_27_060129_create_metrika_results_table.php @@ -14,13 +14,13 @@ return new class extends Migration Schema::create('metrika_results', function (Blueprint $table) { $table->id('metrika_result_id') ->comment('Идентификатор результата метрики'); + $table->foreignIdFor(\App\Models\MetrikaItem::class, 'rf_metrika_item_id') + ->comment('Идентификатор метрики') + ->constrained(); $table->foreignIdFor(\App\Models\Report::class, 'rf_report_id') ->comment('Идентификатор отчета') - ->constrained() - ->cascadeOnDelete(); - $table->foreignIdFor(\App\Models\MetrikaGroup::class, 'rf_metrika_group_id') - ->comment('Идентификатор группы метрики') ->constrained(); + $table->text('value'); }); } diff --git a/database/migrations/2025_12_27_060302_create_metrika_result_values_table.php b/database/migrations/2025_12_27_060302_create_metrika_result_values_table.php index 4ed4f40..83427be 100644 --- a/database/migrations/2025_12_27_060302_create_metrika_result_values_table.php +++ b/database/migrations/2025_12_27_060302_create_metrika_result_values_table.php @@ -21,6 +21,9 @@ return new class extends Migration $table->foreignIdFor(\App\Models\MetrikaItem::class, 'rf_metrika_item_id') ->comment('Идентификатор метрики') ->constrained(); + $table->foreignIdFor(\App\Models\Report::class, 'rf_report_id') + ->comment('Идентификатор отчета') + ->constrained(); $table->text('value'); $table->timestamps(); }); diff --git a/database/migrations/2026_01_09_154100_create_department_types_table.php b/database/migrations/2026_01_09_154100_create_department_types_table.php new file mode 100644 index 0000000..a144b56 --- /dev/null +++ b/database/migrations/2026_01_09_154100_create_department_types_table.php @@ -0,0 +1,28 @@ +id('department_type_id'); + $table->string('name_full'); + $table->string('name_short')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('department_types'); + } +}; diff --git a/database/migrations/2026_01_09_154110_create_departments_table.php b/database/migrations/2026_01_09_154110_create_departments_table.php new file mode 100644 index 0000000..4f0e8b2 --- /dev/null +++ b/database/migrations/2026_01_09_154110_create_departments_table.php @@ -0,0 +1,30 @@ +id('department_id'); + $table->string('name_full'); + $table->string('name_short')->nullable(); + $table->unsignedBigInteger('rf_mis_department_id')->nullable(); + $table->foreignIdFor(\App\Models\DepartmentType::class, 'rf_department_type')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('departments'); + } +}; diff --git a/database/migrations/2026_01_09_220419_create_department_metrika_defaults_table.php b/database/migrations/2026_01_09_220419_create_department_metrika_defaults_table.php new file mode 100644 index 0000000..e5e2433 --- /dev/null +++ b/database/migrations/2026_01_09_220419_create_department_metrika_defaults_table.php @@ -0,0 +1,29 @@ +id('department_metrika_default_id'); + $table->foreignIdFor(\App\Models\Department::class, 'rf_department_id'); + $table->foreignIdFor(\App\Models\MetrikaItem::class, 'rf_metrika_item_id'); + $table->string('value'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('department_metrika_defaults'); + } +}; diff --git a/database/migrations/2026_01_11_130432_create_observation_patients_table.php b/database/migrations/2026_01_11_130432_create_observation_patients_table.php new file mode 100644 index 0000000..3277873 --- /dev/null +++ b/database/migrations/2026_01_11_130432_create_observation_patients_table.php @@ -0,0 +1,30 @@ +id('observation_patient_id'); + $table->unsignedBigInteger('rf_medicalhistory_id')->nullable(); + $table->unsignedBigInteger('rf_mkab_id')->nullable(); + $table->foreignIdFor(\App\Models\Department::class, 'rf_department_id'); + $table->foreignIdFor(\App\Models\Report::class, 'rf_report_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('observation_patients'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d92ff65..2600ec0 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,7 +16,7 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->call([ - TestLpuDataSeeder::class, + TestDepartmentDataSeeder::class, TestMetrikaSeeder::class, TestUserSeeder::class, ]); diff --git a/database/seeders/TestDepartmentDataSeeder.php b/database/seeders/TestDepartmentDataSeeder.php new file mode 100644 index 0000000..30f9be7 --- /dev/null +++ b/database/seeders/TestDepartmentDataSeeder.php @@ -0,0 +1,145 @@ + 'Хирургические', + 'name_full' => 'Хирургические отделения' + ]); + DepartmentType::create([ + 'name_short' => 'Терапевтические', + 'name_full' => 'Терапевтические отделения' + ]); + DepartmentType::create([ + 'name_short' => 'Перинатальный', + 'name_full' => 'Перинатальный центр' + ]); + + Department::create([ + 'name_full' => 'Гинекологическое отделение', + 'name_short' => 'Гинекологическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Нейрохирургическое отделение', + 'name_short' => 'Нейрохирургическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Отделение термических поражений', + 'name_short' => 'Термических поражений', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Отоларингологическое отделение', + 'name_short' => 'Отоларингологическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Проктологическое отделение', + 'name_short' => 'Проктологическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Отделение сосудистой хирургии', + 'name_short' => 'Сосудистой хирургии', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Отделение торакальной хирургии', + 'name_short' => 'Торакальной хирургии', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Травматологическое отделение', + 'name_short' => 'Травматологическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Урологическое отделение', + 'name_short' => 'Урологическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Хирургическое отделение', + 'name_short' => 'Хирургическое', + 'rf_department_type' => 1 + ]); + Department::create([ + 'name_full' => 'Отделение ЧЛХ', + 'name_short' => 'ЧЛХ', + 'rf_department_type' => 1 + ]); + + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 1, + 'rf_metrika_item_id' => 1, + 'value' => '50' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 2, + 'rf_metrika_item_id' => 1, + 'value' => '45' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 3, + 'rf_metrika_item_id' => 1, + 'value' => '39' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 4, + 'rf_metrika_item_id' => 1, + 'value' => '30' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 5, + 'rf_metrika_item_id' => 1, + 'value' => '34' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 6, + 'rf_metrika_item_id' => 1, + 'value' => '40' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 7, + 'rf_metrika_item_id' => 1, + 'value' => '25' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 8, + 'rf_metrika_item_id' => 1, + 'value' => '50' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 9, + 'rf_metrika_item_id' => 1, + 'value' => '50' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 10, + 'rf_metrika_item_id' => 1, + 'value' => '55' + ]); + DepartmentMetrikaDefault::create([ + 'rf_department_id' => 11, + 'rf_metrika_item_id' => 1, + 'value' => '25' + ]); + } +} diff --git a/database/seeders/TestLpuDataSeeder.php b/database/seeders/TestLpuDataSeeder.php deleted file mode 100644 index 575003e..0000000 --- a/database/seeders/TestLpuDataSeeder.php +++ /dev/null @@ -1,46 +0,0 @@ -id('lpuid'); - $table->string('mname_s'); - $table->foreignId('mainlpuid')->nullable()->constrained('oms_lpu', 'lpuid'); - }); - Schema::create('oms_department', function (Blueprint $table) { - $table->id('depatmentid'); - $table->string('departmentname'); - $table->foreignId('rf_lpuid')->constrained('oms_lpu', 'lpuid'); - }); - - \DB::table('oms_lpu')->insert([ - 'lpuid' => 1, - 'mname_s' => 'ГАУЗ АО АОКБ', - 'mainlpuid' => null - ]); - \DB::table('oms_lpu')->insert([ - 'lpuid' => 2, - 'mname_s' => 'Приемное отделение', - 'mainlpuid' => 1 - ]); - - - \DB::table('oms_department')->insert([ - 'departmentname' => 'Тест', - 'rf_lpuid' => 2, - 'depatmentid' => 1 - ]); - } -} diff --git a/database/seeders/TestMetrikaSeeder.php b/database/seeders/TestMetrikaSeeder.php index 02f2099..110414a 100644 --- a/database/seeders/TestMetrikaSeeder.php +++ b/database/seeders/TestMetrikaSeeder.php @@ -50,5 +50,33 @@ class TestMetrikaSeeder extends Seeder 'rf_metrika_item_id' => $item->metrika_item_id ]); } + + MetrikaGroup::create([ + 'name' => 'Поступления', + ]); + + MetrikaItem::create([ + 'name' => 'Выбыло', + 'data_type' => 'integer', + 'default_value' => 0, + ]); + MetrikaItem::create([ + 'name' => 'Состоит', + 'data_type' => 'integer', + 'default_value' => 0, + ]); + + MetrikaGroupItem::create([ + 'rf_metrika_group_id' => 2, + 'rf_metrika_item_id' => 3 + ]); + MetrikaGroupItem::create([ + 'rf_metrika_group_id' => 2, + 'rf_metrika_item_id' => 7 + ]); + MetrikaGroupItem::create([ + 'rf_metrika_group_id' => 2, + 'rf_metrika_item_id' => 8 + ]); } } diff --git a/database/seeders/TestUserSeeder.php b/database/seeders/TestUserSeeder.php index f68873c..0d272c2 100644 --- a/database/seeders/TestUserSeeder.php +++ b/database/seeders/TestUserSeeder.php @@ -2,7 +2,9 @@ namespace Database\Seeders; +use App\Models\Role; use App\Models\User; +use App\Models\UserRole; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -13,6 +15,23 @@ class TestUserSeeder extends Seeder */ public function run(): void { + Role::create([ + 'name' => 'Администратор', + 'slug' => 'admin', + ]); + Role::create([ + 'name' => 'Заведующий отделением', + 'slug' => 'head_of_department', + ]); + Role::create([ + 'name' => 'Врач', + 'slug' => 'doctor', + ]); + Role::create([ + 'name' => 'Статистик', + 'slug' => 'statistician', + ]); + User::create([ 'name' => 'Test User', 'login' => 'test', @@ -20,5 +39,19 @@ class TestUserSeeder extends Seeder 'rf_department_id' => 1, 'rf_lpudoctor_id' => null ]); + + UserRole::create([ + 'rf_user_id' => 1, + 'rf_role_id' => 1, + 'is_default' => true + ]); + UserRole::create([ + 'rf_user_id' => 1, + 'rf_role_id' => 2, + ]); + UserRole::create([ + 'rf_user_id' => 1, + 'rf_role_id' => 3, + ]); } } diff --git a/docker/app.conf b/docker/app.conf index cf60464..1d1e914 100644 --- a/docker/app.conf +++ b/docker/app.conf @@ -4,26 +4,6 @@ map $remote_addr $blocked_ip { 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 _; @@ -46,85 +26,11 @@ server { 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$ { @@ -157,32 +63,12 @@ server { # ========== ОСНОВНОЙ 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 ~ /\. { diff --git a/docker/nginx.conf b/docker/nginx.conf index f9a1903..73b5689 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -13,27 +13,8 @@ events { 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; @@ -62,15 +43,6 @@ http { 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; diff --git a/package-lock.json b/package-lock.json index d5ab1a0..c44ce52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "@arco-design/color": "^0.4.0", "@inertiajs/vue3": "^2.3.4", "@vitejs/plugin-vue": "^6.0.2", + "@vue-flow/core": "^1.48.1", "@vueuse/core": "^14.1.0", "date-fns": "^4.1.0", "pinia": "^3.0.4", "ufo": "^1.6.1", "vue": "^3.5.25", + "vue-draggable-next": "^2.3.0", "vue-icons-plus": "^0.1.9" }, "devDependencies": { @@ -1387,6 +1389,116 @@ "dev": true, "license": "MIT" }, + "node_modules/@vue-flow/core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.1.tgz", + "integrity": "sha512-3IxaMBLvWRbznZ4CuK0kVhp4Y4lCDQx9nhi48Swp6PwPw29KNhmiKd2kaBogYeWjGLb/tLjlE9V0s3jEmKCYWw==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "^10.5.0", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vue-flow/core/node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.26", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", @@ -1924,6 +2036,112 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -3154,6 +3372,13 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "license": "MIT", + "peer": true + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3740,6 +3965,16 @@ } } }, + "node_modules/vue-draggable-next": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vue-draggable-next/-/vue-draggable-next-2.3.0.tgz", + "integrity": "sha512-ymbY0UIwfSdg0iDN/iyNNwUrTqZ/6KbPryzsvTNXBLuDCuOBdNijSK8yynNtmiSj6RapTPQfjLGQdJrZkzBd2w==", + "license": "MIT", + "peerDependencies": { + "sortablejs": "^1.14.0", + "vue": "^3.5.17" + } + }, "node_modules/vue-icons-plus": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/vue-icons-plus/-/vue-icons-plus-0.1.9.tgz", diff --git a/package.json b/package.json index 336760a..51f4aae 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "@arco-design/color": "^0.4.0", "@inertiajs/vue3": "^2.3.4", "@vitejs/plugin-vue": "^6.0.2", + "@vue-flow/core": "^1.48.1", "@vueuse/core": "^14.1.0", "date-fns": "^4.1.0", "pinia": "^3.0.4", "ufo": "^1.6.1", "vue": "^3.5.25", + "vue-draggable-next": "^2.3.0", "vue-icons-plus": "^0.1.9" } } diff --git a/resources/css/app.css b/resources/css/app.css index 2698ccd..3281ba9 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,41 +1,46 @@ @import 'tailwindcss'; +@import "@vue-flow/core/dist/style.css"; +@import "@vue-flow/core/dist/theme-default.css"; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; @source '../**/*.blade.php'; @source '../**/*.js'; -@font-face { - font-family: "v-sans"; - font-weight: 400; - src: url("/fonts/Golos-Text_Regular.woff2"); -} +@layer base { + @font-face { + font-family: "v-sans"; + font-weight: 400; + src: url("/fonts/Golos-Text_Regular.woff2"); + } -@font-face { - font-family: "v-sans"; - font-weight: 500; - src: url("/fonts/Golos-Text_Medium.woff2"); -} + @font-face { + font-family: "v-sans"; + font-weight: 500; + src: url("/fonts/Golos-Text_Medium.woff2"); + } -@font-face { - font-family: "v-sans"; - font-weight: 600; - src: url("/fonts/Golos-Text_DemiBold.woff2"); -} + @font-face { + font-family: "v-sans"; + font-weight: 600; + src: url("/fonts/Golos-Text_DemiBold.woff2"); + } -@font-face { - font-family: "v-sans"; - font-weight: 700; - src: url("/fonts/Golos-Text_Bold.woff2"); -} + @font-face { + font-family: "v-sans"; + font-weight: 700; + src: url("/fonts/Golos-Text_Bold.woff2"); + } -@font-face { - font-family: "v-sans"; - font-weight: 800; - src: url("/fonts/Golos-Text_Black.woff2"); + @font-face { + font-family: "v-sans"; + font-weight: 800; + src: url("/fonts/Golos-Text_Black.woff2"); + } } @theme { --font-sans: 'v-sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } + diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index 4943230..e74128d 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -34,7 +34,7 @@ const themeOverrides = { - +
diff --git a/resources/js/Layouts/Components/AppHeader.vue b/resources/js/Layouts/Components/AppHeader.vue index 71fe10d..84e8660 100644 --- a/resources/js/Layouts/Components/AppHeader.vue +++ b/resources/js/Layouts/Components/AppHeader.vue @@ -3,24 +3,21 @@ import { NFlex, NSpace, NDivider, NButton } from 'naive-ui' import ReportSelectDate from "../../Components/ReportSelectDate.vue"; import AppUserButton from "./AppUserButton.vue"; import {Link} from "@inertiajs/vue3"; +import AppHeaderRole from "./AppHeaderRole.vue"; diff --git a/resources/js/Layouts/Components/AppHeaderRole.vue b/resources/js/Layouts/Components/AppHeaderRole.vue new file mode 100644 index 0000000..850f0a7 --- /dev/null +++ b/resources/js/Layouts/Components/AppHeaderRole.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/resources/js/Layouts/Components/AppUserButton.vue b/resources/js/Layouts/Components/AppUserButton.vue index 5c7fb10..10e4cdf 100644 --- a/resources/js/Layouts/Components/AppUserButton.vue +++ b/resources/js/Layouts/Components/AppUserButton.vue @@ -1,6 +1,6 @@ diff --git a/resources/js/Pages/Report/Components/ReportForm.vue b/resources/js/Pages/Report/Components/ReportForm.vue new file mode 100644 index 0000000..26c51be --- /dev/null +++ b/resources/js/Pages/Report/Components/ReportForm.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/resources/js/Pages/Report/Components/ReportFormInput.vue b/resources/js/Pages/Report/Components/ReportFormInput.vue new file mode 100644 index 0000000..3e930f9 --- /dev/null +++ b/resources/js/Pages/Report/Components/ReportFormInput.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/resources/js/Pages/Report/Components/ReportHeader.vue b/resources/js/Pages/Report/Components/ReportHeader.vue new file mode 100644 index 0000000..6d4b02e --- /dev/null +++ b/resources/js/Pages/Report/Components/ReportHeader.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/resources/js/Pages/Report/Components/ReportSection.vue b/resources/js/Pages/Report/Components/ReportSection.vue new file mode 100644 index 0000000..2b1d14b --- /dev/null +++ b/resources/js/Pages/Report/Components/ReportSection.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/resources/js/Pages/Report/Components/ReportSectionHeader.vue b/resources/js/Pages/Report/Components/ReportSectionHeader.vue new file mode 100644 index 0000000..27ff59a --- /dev/null +++ b/resources/js/Pages/Report/Components/ReportSectionHeader.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/resources/js/Pages/Report/Components/ReportSectionItem.vue b/resources/js/Pages/Report/Components/ReportSectionItem.vue new file mode 100644 index 0000000..0d56af7 --- /dev/null +++ b/resources/js/Pages/Report/Components/ReportSectionItem.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/resources/js/Pages/Report/Index.vue b/resources/js/Pages/Report/Index.vue index d0c3d46..378926c 100644 --- a/resources/js/Pages/Report/Index.vue +++ b/resources/js/Pages/Report/Index.vue @@ -1,19 +1,18 @@ diff --git a/resources/js/Pages/Statistic/Index.vue b/resources/js/Pages/Statistic/Index.vue index 1a02f4f..691e6a2 100644 --- a/resources/js/Pages/Statistic/Index.vue +++ b/resources/js/Pages/Statistic/Index.vue @@ -1,58 +1,135 @@ diff --git a/resources/js/Pages/Statistic/IndexOld.vue b/resources/js/Pages/Statistic/IndexOld.vue new file mode 100644 index 0000000..1a02f4f --- /dev/null +++ b/resources/js/Pages/Statistic/IndexOld.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/resources/js/Stores/auth.js b/resources/js/Stores/auth.js index 53bccb0..0099a1f 100644 --- a/resources/js/Stores/auth.js +++ b/resources/js/Stores/auth.js @@ -7,7 +7,7 @@ export const useAuthStore = defineStore('authStore', () => { const user = usePage().props.user const token = user?.token const permissions = user?.permissions - const availableDepartments = ref([]) + const availableDepartments = ref(user?.available_departments) // Инициализация axios с токеном if (token?.value) { @@ -21,7 +21,7 @@ export const useAuthStore = defineStore('authStore', () => { const isNurse = computed(() => user.role === 'nurse') const isHeadOfDepartment = computed(() => user.role === 'head_of_department') const isStatistician = computed(() => user.role === 'statistician') - const userDepartment = computed(() => user.department || '') + const userDepartment = computed(() => user.current_department || '') const clearAuthData = () => { user.value = null diff --git a/resources/js/Stores/report.js b/resources/js/Stores/report.js index f04f37f..2b52075 100644 --- a/resources/js/Stores/report.js +++ b/resources/js/Stores/report.js @@ -1,6 +1,7 @@ import {defineStore} from "pinia"; import {useTimestamp} from "@vueuse/core"; import {computed, ref} from "vue"; +import {router} from "@inertiajs/vue3"; export const useReportStore = defineStore('reportStore', () => { const timestampNow = useTimestamp() @@ -22,6 +23,82 @@ export const useReportStore = defineStore('reportStore', () => { const dataOnReport = ref(null) + const reportInfo = ref(null) + + const patientColumns = [ + { + title: '№', + width: '80', + key: 'num' + }, + { + title: 'ФИО', + width: '320', + key: 'fullname' + }, + { + title: 'Возраст', + key: 'age' + }, + { + title: 'Дата рождения', + key: 'birth_date' + }, + { + title: 'Диагноз', + key: 'ds' + } + ] + + const patientsData = ref({ + plan: [], + emergency: [], + observation: [], + deceased: [] + }) + + const reportForm = ref({}) + + const getColumnsByKey = (keys) => { + const result = [] + for (const key of keys) { + const column = patientColumns.find(item => item.key === key) + result.push(column) + } + + return result + } + + const sendReportForm = (assignForm) => { + const form = { + metrics: reportForm.value, + observationPatients: patientsData.value['observation'], + ...assignForm + } + + axios.post('/api/report', form) + .then(r => { + window.$message.success('Отчет сохранен') + resetReportForm() + router.visit('/') + }) + } + + const resetReportForm = () => { + reportForm.value = {} + patientsData.value.observation = [] + } + + const $reset = () => { + + } + + const getReportInfo = async () => { + await axios.get('/api/report').then((res) => { + reportInfo.value = res.data + }) + } + const getDataOnReportDate = async () => { await axios.get(`/api/metric-forms/1/report-by-date?sent_at=${timestampCurrentRange.value}`) .then(res => { @@ -38,7 +115,15 @@ export const useReportStore = defineStore('reportStore', () => { timestampCurrent, timestampCurrentRange, dataOnReport, + patientColumns, + patientsData, + reportInfo, + reportForm, - getDataOnReportDate + getColumnsByKey, + getDataOnReportDate, + getReportInfo, + sendReportForm, + resetReportForm, } }) diff --git a/resources/js/app.js b/resources/js/app.js index 4d36859..1d0a943 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,9 +1,10 @@ -import './bootstrap'; -import '../css/app.css'; import { createApp, h } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' import {createPinia} from "pinia"; import {setupNaiveDiscreteApi} from "./Plugins/NaiveUI.js"; +import './bootstrap'; +import '../css/app.css'; + createInertiaApp({ id: 'onboard', diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 7ddb6f6..e145c42 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -1,10 +1,15 @@ - + - - @vite('resources/js/app.js') - @inertiaHead + + {{ config('app.name', 'Laravel') }} + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/js/app.js']) + @inertiaHead + @endif @inertia('onboard') diff --git a/routes/api.php b/routes/api.php index 3fd7af3..2ff4ce9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,8 @@ use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\MetrikaFormController; +use App\Http\Controllers\Api\ReportController; +use App\Http\Controllers\Api\RoleController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -36,5 +38,24 @@ Route::middleware(['auth:sanctum'])->group(function () { Route::get('/{group}/calendar-with-reports', [MetrikaFormController::class, 'getCalendarWithReports']) ->name('metric-forms.calendar-with-reports'); }); + + Route::prefix('mis')->group(function () { + Route::post('/patients', [ReportController::class, 'getPatients']); + Route::post('/patients/count', [ReportController::class, 'getPatientsCount']); + }); + + Route::prefix('report')->group(function () { + Route::get('/', [ReportController::class, 'index']); + Route::post('/', [ReportController::class, 'store']); + }); + + Route::prefix('app')->group(function () { + Route::prefix('user')->group(function () { + Route::prefix('roles')->group(function () { + Route::get('/', [RoleController::class, 'getUserRoles']); + Route::post('/', [RoleController::class, 'setUserRole']); + }); + }); + }); }); diff --git a/routes/web.php b/routes/web.php index 52632e8..07df975 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,9 +21,17 @@ Route::prefix('api')->group(function () { Route::get('/dashboard', [\App\Http\Controllers\Web\IndexController::class, 'index']) ->middleware(['auth']) ->name('dashboard'); +Route::get('/report', [\App\Http\Controllers\Web\ReportController::class, 'index']) + ->middleware(['auth']) + ->name('report'); Route::get('/statistic', [\App\Http\Controllers\Web\StatisticController::class, 'index']) ->middleware(['auth']) ->name('statistic'); +Route::get('/path/patient', function () { + return \Inertia\Inertia::render('Path/Patient'); +}) + ->middleware(['auth']) + ->name('path.patient'); Route::get('/', [\App\Http\Controllers\Web\IndexController::class, 'start']) ->middleware(['auth'])