Add medical examinations and improve edit-user

This commit is contained in:
Matteo Gheza 2024-01-08 22:25:01 +01:00
parent 053eaffec1
commit 84beeda8fe
13 changed files with 317 additions and 18 deletions

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\File;
use App\Models\Document;
use App\Models\DocumentFile;
class DocumentsController extends Controller
@ -42,4 +43,54 @@ class DocumentsController extends Controller
return response()->file(storage_path('app/public/' . $document->file_path));
}
function addMedicalExamination(Request $request)
{
$request->validate([
'user' => ['required', 'integer', 'exists:users,id'],
'date' => ['required', 'date'],
'certifier' => ['required', 'string', 'max:255'],
'expiration_date' => ['required', 'date'],
'file' => [
File::types('application/pdf')
->max(50 * 1024)
]
]);
if($request->user()->id != $request->input('user') && !$request->user()->hasPermission("users-add-medical-examination")) abort(401);
if($request->user()->id == $request->input('user') && !$request->user()->hasPermission("user-add-medical-examination")) abort(401);
$document = new Document();
$document->type = 'medical_examination';
$document->doc_certifier = $request->input('doctor');
$document->user = $request->input('user');
$document->added_by = auth()->user()->id;
if($request->hasFile('file')) {
$fileName = time() . '_' . $request->file->getClientOriginalName();
$filePath = $request->file('file')->storeAs('medical_examinations', $fileName, 'public');
$documentFile = new DocumentFile();
$documentFile->uuid = Str::uuid()->toString();
$documentFile->type = 'medical_examination';
$documentFile->file_path = $filePath;
$documentFile->uploadedBy()->associate(auth()->user());
$documentFile->save();
$document->documentFile()->associate($documentFile);
}
$document->date = $request->input('date');
$document->expiration_date = $request->input('expiration_date');
$document->save();
return response()->json([
"id" => $document->id
]);
}
function serveMedicalExamination($uuid)
{
$document = DocumentFile::where('uuid', $uuid)->firstOrFail();
return response()->file(storage_path('app/public/' . $document->file_path));
}
}

View File

@ -60,7 +60,7 @@ class UserController extends Controller
User::where('id', $request->user()->id)->update(['last_access' => now()]);
$dl_tmp = Document::where('documents.added_by', $user->id)
$dl_tmp = Document::where('documents.user', $user->id)
->where('documents.type', 'driving_license')
->join('document_files', 'document_files.id', '=', 'documents.document_file_id')
->select('documents.doc_type', 'documents.doc_number', 'documents.expiration_date', 'document_files.uuid as scan_uuid')
@ -70,6 +70,26 @@ class UserController extends Controller
$user->driving_license = $dl_tmp[0];
}
$me_tmp = Document::where('documents.user', $user->id)
->where('documents.type', 'medical_examination')
->leftJoin('document_files', 'document_files.id', '=', 'documents.document_file_id')
->select('documents.doc_certifier as certifier', 'documents.date', 'documents.expiration_date', 'document_files.uuid as cert_uuid')
->get();
if($me_tmp->count() > 0) {
$user->medical_examinations = $me_tmp;
foreach($user->medical_examinations as $me) {
if(!is_null($me->cert_uuid)) {
$me->cert_url = URL::temporarySignedRoute(
'medical_examination_serve', now()->addHours(1), ['uuid' => $me->cert_uuid]
);
unset($me->cert_uuid);
}
}
} else {
$user->medical_examinations = [];
}
if(!is_null($user->driving_license) && !is_null($user->driving_license->scan_uuid)) {
$user->driving_license->scan_url = URL::temporarySignedRoute(
'driving_license_scan_serve', now()->addMinutes(2), ['uuid' => $user->driving_license->scan_uuid]
@ -143,13 +163,14 @@ class UserController extends Controller
//Check if driving license is present
if($request->has('driving_license')) {
$drivingLicense = Document::where('added_by', $user->id)
$drivingLicense = Document::where('user', $user->id)
->where('type', 'driving_license')
->first();
if(is_null($drivingLicense)) {
$drivingLicense = new Document();
$drivingLicense->added_by = $user->id;
$drivingLicense->user = $user->id;
$drivingLicense->added_by = $request->user()->id;
$drivingLicense->type = 'driving_license';
}

View File

@ -18,7 +18,8 @@ class Document extends Model
protected $fillable = [
'type',
'doc_number',
'doc_type'
'doc_type',
'doc_certifier'
];
/**
@ -27,9 +28,15 @@ class Document extends Model
* @var array<string, string>
*/
protected $casts = [
'date' => 'datetime',
'expiration_date' => 'datetime'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function addedBy(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@ -13,21 +13,21 @@ return [
'roles_structure' => [
'superadmin' => [
'users' => 'c,r,u,d,i,b,h,sc,sd',
'users' => 'c,r,u,d,i,b,h,sc,sd,ame',
'user' => 'u,h,sc,sd',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
],
'admin' => [
'users' => 'c,r,u,d,i,b,h,sc,sd',
'users' => 'c,r,u,d,i,b,h,sc,sd,ame',
'user' => 'u,h,sc,sd',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
],
'chief' => [
'users' => 'r,u,sc,sd',
'users' => 'r,u,sc,sd,ame',
'user' => 'u',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
@ -53,5 +53,6 @@ return [
'h' => 'hide',
'sc' => 'set-chief',
'sd' => 'set-driver',
'ame' => 'add-medical-examination'
]
];

View File

@ -24,8 +24,11 @@ return new class extends Migration
$table->string('type');
$table->string('doc_number')->nullable();
$table->string('doc_type')->nullable();
$table->string('doc_certifier')->nullable();
$table->foreignId('user')->constrained('users');
$table->foreignId('added_by')->constrained('users');
$table->foreignId('document_file_id')->nullable()->constrained('document_files');
$table->dateTime('date')->nullable();
$table->dateTime('expiration_date')->nullable();
$table->timestamps();
});

View File

@ -50,6 +50,7 @@ Route::middleware('auth:sanctum')->group( function () {
Route::put('/users/{user}', [UserController::class, 'update']);
Route::post('/documents/driving_license', [DocumentsController::class, 'uploadDrivingLicenseScan']);
Route::post('/documents/medical_examination', [DocumentsController::class, 'addMedicalExamination']);
Route::get('/schedules', [ScheduleSlotsController::class, 'index']);
Route::post('/schedules', [ScheduleSlotsController::class, 'store']);
@ -90,6 +91,7 @@ Route::middleware('auth:sanctum')->group( function () {
Route::middleware('signed')->group( function () {
Route::get('/documents/driving_license/{uuid}', [DocumentsController::class, 'serveDrivingLicenseScan'])->name('driving_license_scan_serve');
Route::get('/documents/medical_examination/{uuid}', [DocumentsController::class, 'serveMedicalExamination'])->name('medical_examination_serve');
});
Route::get('/owner_image', function() {

View File

@ -0,0 +1,39 @@
<div class="modal-header">
<h4 class="modal-title pull-left" translate>user_info_modal.title</h4>
<button
type="button"
class="btn-close close pull-right"
[attr.aria-label]="'close' | translate | titlecase"
(click)="bsModalRef.hide()"
>
<span aria-hidden="true" class="visually-hidden">&times;</span>
</button>
</div>
<div class="modal-body">
<form [formGroup]="form" (submit)="formSubmit()" class="row g-3">
<div class="col-md-6">
<label for="me_date" class="form-label">Data</label>
<input formControlName="date" type="text" class="form-control" [placeholder]="'press_to_select_a_date'|translate|titlecase" id="me_date" bsDatepicker [bsConfig]="{ adaptivePosition: true, dateInputFormat: 'DD/MM/YYYY' }" [maxDate]="dateMaxDate">
</div>
<div class="col-md-6">
<label for="me_expiration_date" class="form-label">Data scadenza</label>
<input formControlName="expiration_date" type="text" class="form-control" [placeholder]="'press_to_select_a_date'|translate|titlecase" id="me_expiration_date" bsDatepicker [bsConfig]="{ adaptivePosition: true, dateInputFormat: 'DD/MM/YYYY' }" [minDate]="expirationDateMinDate">
</div>
<div class="col-12">
<label for="me_certifier">Ente/Certificatore</label>
<input formControlName="certifier" type="text" class="form-control" id="me_certifier">
</div>
<div class="col-12">
<label for="dl_scan" class="form-label">Carica certificato medico (<u>OPZIONALE</u>, PDF)</label>
<input class="form-control" type="file" id="dl_scan" (change)="onMedicalExaminationCertificateSelected($event)">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="formSubmit()">
{{ "add" | translate | titlecase }}
</button>
<button type="button" class="btn btn-secondary" (click)="bsModalRef.hide()">
{{ "close" | translate }}
</button>
</div>

View File

@ -0,0 +1,105 @@
import { Component, OnInit, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { BsModalRef } from 'ngx-bootstrap/modal';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from 'src/app/_services/auth.service';
import { TranslateService } from '@ngx-translate/core';
import Swal from 'sweetalert2';
@Component({
selector: 'modal-add-medical-examination',
templateUrl: './modal-add-medical-examination.component.html',
styleUrls: ['./modal-add-medical-examination.component.scss']
})
export class ModalAddMedicalExaminationComponent implements OnInit {
userId: number = 0;
form: FormGroup = this.formBuilder.group({
date: [null, [Validators.required]],
certifier: ['', [Validators.required]],
expiration_date: [null, [Validators.required]],
file: [null]
});
dateMaxDate = new Date();
expirationDateMinDate = new Date(new Date().setDate(new Date().getDate() + 1)); //Tomorrow
allowedImageTypes = ["application/pdf"];
maxImageSize = 1024 * 1024 * 50; //50MB
submitEvents: EventEmitter<any> = new EventEmitter();
constructor(
public bsModalRef: BsModalRef,
private formBuilder: FormBuilder,
private api: ApiClientService,
public auth: AuthService,
private translateService: TranslateService
) { }
ngOnInit() { }
onMedicalExaminationCertificateSelected(event: any) {
const file: File = event.target.files[0];
if (file) {
if(!this.allowedImageTypes.includes(file.type)) {
event.target.value = null;
Swal.fire({
title: this.translateService.instant("error_title"),
text: this.translateService.instant("edit_user.image_format_not_supported"),
icon: 'error',
confirmButtonText: 'Ok'
});
return;
}
if(file.size > this.maxImageSize) {
event.target.value = null;
Swal.fire({
title: this.translateService.instant("error_title"),
text: this.translateService.instant("edit_user.file_too_big"),
icon: 'error',
confirmButtonText: 'Ok'
});
return;
}
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (_event) => {
this.form.patchValue({
file
});
this.form.get('file')?.updateValueAndValidity();
}
}
}
formSubmit() {
const formValues = this.form.value;
formValues.date = formValues.date ? new Date(formValues.date).toISOString() : null;
formValues.expiration_date = formValues.expiration_date ? new Date(formValues.expiration_date).toISOString() : null;
console.log(formValues);
const formData = new FormData();
formData.append('user', this.userId.toString());
formData.append('date', formValues.date);
formData.append('certifier', formValues.certifier);
formData.append('expiration_date', formValues.expiration_date);
if(formValues.file) formData.append('file', formValues.file, formValues.file.name);
this.api.post("documents/medical_examination", formData).then((response) => {
console.log(response);
this.bsModalRef.hide();
this.submitEvents.emit();
}).catch((err) => {
Swal.fire({
title: this.translateService.instant("error_title"),
text: err.error.message,
icon: 'error',
confirmButtonText: 'Ok'
});
console.log(err);
});
}
}

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslationModule } from '../../translation.module';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { ModalAddMedicalExaminationComponent } from './modal-add-medical-examination.component';
@NgModule({
declarations: [
ModalAddMedicalExaminationComponent
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
TranslationModule,
BsDatepickerModule.forRoot()
],
exports: [
ModalAddMedicalExaminationComponent
]
})
export class ModalAddMedicalExaminationModule { }

View File

@ -147,7 +147,7 @@
</div>
</form>
<ng-component *ngIf="false">
<ng-container *ngIf="false">
<hr>
<div class="text-center">
@ -180,31 +180,34 @@
</table>
<br>
</div>
</ng-container>
<hr>
<div class="text-center">
<h3>Visite mediche</h3>
<button *ngIf="!hideMEAddBtn" class="btn btn-sm btn-outline-success m-2" (click)="openModalAddMedicalExamination()">
<i class="fas fa-plus"></i> {{ 'add'|translate|titlecase }}
</button>
<table class="mx-auto table table-striped table-bordered table-responsive">
<thead>
<tr>
<th>DATA</th>
<th>MEDICO</th>
<th>ENTE</th>
<th>SCADENZA</th>
<th>CERT.</th>
<th *ngIf="!hideMECertCol">CERT.</th>
</tr>
</thead>
<tbody>
<tr *ngIf="false">
<td>31/12/2024</td>
<td>Medico</td>
<td>31/12/2027</td>
<td>
<a href=''><i class='fas fa-clipboard-list'></i></a>
<tr *ngFor="let row of user.medical_examinations">
<td>{{ row.date | date:'dd/MM/YYYY' }}</td>
<td>{{ row.certifier }}</td>
<td>{{ row.expiration_date | date:'dd/MM/YYYY' }}</td>
<td *ngIf="!hideMECertCol && row.cert_url">
<a [href]='row.cert_url' target="_blank"><i class='fas fa-clipboard-list'></i></a>
</td>
</tr>
</tbody>
</table>
</div>
</ng-component>

View File

@ -4,6 +4,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from 'src/app/_services/auth.service';
import { TranslateService } from '@ngx-translate/core';
import { ModalAddMedicalExaminationComponent } from 'src/app/_components/modal-add-medical-examination/modal-add-medical-examination.component';
import { BsModalService } from 'ngx-bootstrap/modal';
import Swal from 'sweetalert2';
@Component({
@ -42,6 +44,8 @@ export class EditUserComponent implements OnInit {
boot_size: ['']
});
hideMECertCol = true;
hideMEAddBtn = true;
birthdayMaxDate = new Date(new Date().setFullYear(new Date().getFullYear() - 18)); //18 years ago
dlExpirationMinDate = new Date(new Date().setDate(new Date().getDate() + 1)); //Tomorrow
allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
@ -60,7 +64,8 @@ export class EditUserComponent implements OnInit {
private formBuilder: FormBuilder,
private api: ApiClientService,
private auth: AuthService,
private translateService: TranslateService
private translateService: TranslateService,
private modalService: BsModalService
) {
this.route.paramMap.subscribe(params => {
this.id = typeof params.get('id') === 'string' ? parseInt(params.get('id') || '') : undefined;
@ -109,6 +114,13 @@ export class EditUserComponent implements OnInit {
if(this.user.driving_license && this.user.driving_license.scan_uuid) {
this.dlCurrScanUrl = this.api.apiEndpoint(this.user.driving_license.scan_url);
}
//If medical examination is present, check if at least one row has cert_url
if(this.user.medical_examinations && this.user.medical_examinations.length > 0) {
this.hideMECertCol = !this.user.medical_examinations.some((me: any) => {
return me.cert_url;
});
}
}).catch((err) => {
console.log(err);
});
@ -129,11 +141,15 @@ export class EditUserComponent implements OnInit {
let canHide = this.id == this.auth.profile.id ?
this.auth.profile.can('user-hide') :
this.auth.profile.can('users-hide');
let canAddMedicalExamination = this.id == this.auth.profile.id ?
this.auth.profile.can('user-add-medical-examination') :
this.auth.profile.can('users-add-medical-examination');
if(!canSetChief) this.profileForm.get('chief')?.disable();
if(!canSetDriver) this.profileForm.get('driver')?.disable();
if(!canBan) this.profileForm.get('banned')?.disable();
if(!canHide) this.profileForm.get('hidden')?.disable();
this.hideMEAddBtn = !canAddMedicalExamination;
}
onDrivingLicenseScanSelected(event: any) {
@ -221,4 +237,28 @@ export class EditUserComponent implements OnInit {
}
}
openModalAddMedicalExamination() {
const modalReference = this.modalService.show(ModalAddMedicalExaminationComponent, {
initialState: {
userId: this.id
}
});
modalReference.content?.submitEvents.subscribe(() => {
//Refresh user data after modal is closed
this.api.get(`users/${this.id}`).then((response) => {
this.user = response;
console.log(response);
//If medical examination is present, check if at least one row has cert_url
if(this.user.medical_examinations && this.user.medical_examinations.length > 0) {
this.hideMECertCol = !this.user.medical_examinations.some((me: any) => {
return me.cert_url;
});
}
}).catch((err) => {
console.log(err);
});
});
}
}

View File

@ -4,6 +4,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { BackBtnModule } from '../../_components/back-btn/back-btn.module';
import { ModalAddMedicalExaminationModule } from 'src/app/_components/modal-add-medical-examination/modal-add-medical-examination.module';
import { TranslationModule } from '../../translation.module';
import { EditUserRoutingModule } from './edit-user-routing.module';
@ -20,6 +21,7 @@ import { EditUserComponent } from './edit-user.component';
ReactiveFormsModule,
BsDatepickerModule.forRoot(),
BackBtnModule,
ModalAddMedicalExaminationModule,
TranslationModule
]
})