where(function (Builder $q) use ($userId) { $q ->where('reading_physician_id', $userId) ->orWhere('locking_physician_id', $userId) ->orWhere('approving_physician_id', $userId) ->orWhereHas('assignedPhysicians', function (Builder $subQuery) use ($userId) { $subQuery->where('user_id', $userId); }); }); } public function details(): HasOne { return $this->hasOne(StudyDetails::class); } public function attachments(): HasMany { return $this->hasMany(StudyAttachment::class); } public function reports(): HasMany { return $this->hasMany(StudyReport::class); } public function shares(): HasMany { return $this->hasMany(SharedStudy::class); } public function scopeActive(Builder $query): Builder { return $query->whereNull('archived_at'); } public function scopeArchived(Builder $query): Builder { return $query->whereNotNull('archived_at'); } public function scopeUnlocked(Builder $query): Builder { return $query->whereNotNull('locked_at'); } public function getHistoryLink(): string { $user = auth()->user(); if ($user->may(Permission::StudyHistoryEdit)) { return route('staff.history.edit', $this->hash); } if ($user->may(Permission::StudyHistoryView)) { return route('staff.history.view', $this->hash); } return '#'; } public function getMetadataLink(): string { $user = auth()->user(); if ($user->may(Permission::StudyMetadataEdit)) { return route('staff.meta.edit', $this->hash); } if ($user->may(Permission::StudyMetadataView)) { return route('staff.meta.view', $this->hash); } return '#'; } public function getAge(): ?string { $dob = $this->patient_birthdate; return $dob ? (int) Carbon::make($dob)->diffInYears() . 'Y' : null; } public function sexAge(): string { $age = $this->getAge(); if (blank($age) && blank($this->patient_sex)) { return '~'; } return sprintf('%s / %s', $age ?? '~', $this->patient_sex); } public function numInstances(): string { return "{$this->image_count} / {$this->series_count}"; } public function readingPhysician(): BelongsTo { return $this->belongsTo(User::class, 'reading_physician_id'); } public function lockingPhysician(): BelongsTo { return $this->belongsTo(User::class, 'locking_physician_id'); } public function assignedPhysicians(): BelongsToMany { return $this->belongsToMany(User::class, 'study_assignments'); } public function dicomServer(): BelongsTo { return $this->belongsTo(DicomServer::class); } public function isAssigned(User|int|null $user = null): bool { return $this->assignedPhysicians->contains(me($user)->id); } public function isUserInStudyAssignmentsOrReadingPhysician(User|int|null $user = null): bool { $count = self::buildRadiologistQuery(self::where('id', $this->id), me($user)->id)->count(); return $count > 0; } public function isActive(): bool { return $this->archived_at === null; } public function isArchived(): bool { return $this->archived_at !== null; } public function getWorkflowLevelLedAttribute(): string { // todo: implement $color = match ($this->workflow_level) { WorkflowLevel::Unassigned => 'bg-white', // ReportStatus::Opened => 'bg-secondary', WorkflowLevel::DraftAvailable => 'bg-info', WorkflowLevel::Finalized => 'bg-primary', WorkflowLevel::Published => 'bg-success', default => 'bg-light', }; // $icon = match ($this->workflow_level) { WorkflowLevel::Unassigned => 'spinner text-muted', WorkflowLevel::DraftAvailable => 'pen-to-square', WorkflowLevel::Finalized => 'badge-check', WorkflowLevel::Published => 'shield-check', default => 'spinner text-muted', }; return sprintf('', $color, $icon); } public function getArchiveLink(): ?string { if (me()->may(Permission::StudyDownload)) { return PacsUrlGen::archive($this->dicomServer, $this); } return null; } public function getStoneLink(): ?string { if (me()->may(Permission::StudyDownload)) { return PacsUrlGen::stoneViewer($this->dicomServer, $this); } return null; } public function getOhifLink(): ?string { if (me()->may(Permission::StudyDownload)) { return PacsUrlGen::ohifViewer($this->dicomServer, $this); } return null; } public function getOhifSegmentationLink(): ?string { if (me()->may(Permission::StudyDownload)) { return PacsUrlGen::ohifSegmentation($this->dicomServer, $this); } return null; } public function getOhifMprLink(): ?string { if (me()->may(Permission::StudyDownload)) { return PacsUrlGen::ohifViewerMpr($this->dicomServer, $this); } return null; } public function getWeasisLink(): ?string { if (me()->may(Permission::StudyDownload)) { return PacsUrlGen::weasisUrl($this->dicomServer, $this); } return null; } public function links(): array { return [ 'history' => $this->getHistoryLink(), 'metadata' => $this->getMetadataLink(), 'zip' => $this->getArchiveLink(), 'stone' => $this->getStoneLink(), 'ohif' => $this->getOhifLink(), 'weasis' => $this->getWeasisLink(), 'ohif.mpr' => $this->getOhifMprLink(), 'ohif.seg' => $this->getOhifSegmentationLink(), ]; } public function allowed(): array { return [ 'zip' => filled($this->getArchiveLink()), 'stone' => filled($this->getStoneLink()), 'ohif' => filled($this->getOhifLink()), 'weasis' => filled($this->getWeasisLink()), 'ohif.mpr' => filled($this->getOhifMprLink()), 'ohif.seg' => filled($this->getOhifSegmentationLink()), ]; } public function sanitizedPatientName(): string { $name = preg_replace('/[^[:alnum:][:space:]]/u', ' ', $this->patient_name); return Str::of($name) ->slug(' ') ->title(); } public function sanitizedStudyDescription(): string { $name = preg_replace('/[^[:alnum:][:space:]\/\-\&]/u', ' ', $this->study_description); $name = Str::of($name)->title(); $lut = [ 'ct' => 'CT', 'mr' => 'MR', 'mri' => 'MRI', 'hrct' => 'HRCT', 'kub' => 'KUB', 'ap' => 'AP', 'pa' => 'PA', 'with' => 'w/', ]; foreach ($lut as $search => $replace) { $name = preg_replace("/\b{$search}\b/i", $replace, $name); } return $name; } public function toArray(): array { return array_merge(parent::toArray(), [ 'disk_size_human' => human_filesize($this->disk_size), 'reader_name' => $this->readingPhysician?->display_name, // 'assigned_physician_name' => $this->assignedPhysician?->display_name, 'reader_photo' => $this->readingPhysician?->profile_photo_url, 'workflow_level_led' => $this->getWorkflowLevelLedAttribute(), 'priority_icon' => $this->getPriorityIcon(), 'sex_age' => $this->sexAge(), 'num_instances' => $this->numInstances(), 'links' => $this->links(), 'allowed' => $this->allowed(), ]); } public function getPriorityIcon(): string { $tpl = ' '; return match ($this->priority) { Priority::Stat => Blade::render('_partials._img-tooltip', [ 'src' => asset('imgs/stat.png'), 'class' => 'msg-icon', 'tip' => 'STAT', ]), Priority::High => Blade::render('_partials._img-tooltip', [ 'src' => asset('imgs/warning.png'), 'class' => 'msg-icon', 'tip' => 'High Priority', ]), Priority::Low => sprintf($tpl, 'light', 'chevrons-down'), default => '', }; } public function registerMediaConversions(?Media $media = null): void { // $media->extension $this->addMediaConversion('tn') ->width(48) ->height(48) ->sharpen(10) ->performOnCollections(Study::MEDIA_COLLECTION) ->nonQueued(); } public function isLocked(): bool { return $this->locked_at !== null; } public function isUnlocked(): bool { return $this->locked_at === null; } public function lockStudy(User|int|null $user = null, ?WorkflowLevel $status = null): void { $params = [ 'locking_physician_id' => me($user)->id, 'locked_at' => now(), ]; if ($status) { $params['workflow_level'] = $status->value; } $this->update($params); } public function unlockStudy(): void { $this->update( [ 'locking_physician_id' => null, 'locked_at' => null, ] ); } public function registerMediaCollections(): void { $this->addMediaCollection(self::MEDIA_COLLECTION) ->useFallbackUrl(asset(self::FALLBACK_IMAGE)) ->useFallbackUrl(asset(self::FALLBACK_IMAGE), 'tn') ->useFallbackPath(public_path(self::FALLBACK_IMAGE)) ->useFallbackPath(public_path(self::FALLBACK_IMAGE), 'tn'); } public function getGenderIconAttribute(): string { return match (strtolower($this->patient_sex ?? '')) { 'f' => 'fa-venus text-danger', 'm' => 'fa-mars text-primary', default => 'fa-genderless', }; } public function getGenderNameAttribute(): string { return match (strtolower($this->patient_sex ?? '')) { 'f' => 'Female', 'm' => 'Male', default => 'Other', }; } public function getPatientDemographic(): string { $parts = array_purge([ Str::limit($this->sanitizedPatientName(), 20), $this->patient_sex, $this->patient_id, ]); if (! empty($parts)) { return implode(' ^ ', $parts); } return ''; } public function isLockedBy(User|int|null $user = null): bool { return $this->locking_physician_id === me($user)->id; } public function isReadBy(User|int|null $user = null): bool { return $this->reading_physician_id === me($user)->id; } public function isBookmarkedBy(User|int|null $user = null): bool { return $this->bookmarkedByUsers()->where('user_id', me($user)->id)->exists(); } /** * Returns true if study has been locked by the given user or is unlocked. Returns false if the study was locked by another user. */ public function canObtainLock(User|int|null $user = null): bool { return $this->isUnlocked() || $this->isLockedBy($user); } public function setReportWorkflowLevel(WorkflowLevel $level, User|int|null $user = null): void { $user_id = me($user)->id; $params = ['workflow_level' => $level->value]; switch ($level) { case WorkflowLevel::Finalized: $params['reading_physician_id'] = $user_id; $params['read_at'] = now(); break; case WorkflowLevel::Published: if ($this->reading_physician_id === null) { $params['reading_physician_id'] = $user_id; $params['read_at'] = now(); } $params['approving_physician_id'] = $user_id; $params['approved_at'] = now(); break; } $this->update($params); } public function canEditReport(): bool { // todo: check if the study disallows reporting return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::Finalized); } public function reportStatusBefore(WorkflowLevel $checkpoint): bool { return $this->workflow_level->value < $checkpoint->value; } public function canEditMetadata(): bool { return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::Finalized); } public function canAssignRad(): bool { if ($this->isLocked()) { return false; } return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::DraftAvailable); } public function hasReports(): bool { return $this->reports->isNotEmpty(); } public function isReportReady(): bool { return ($this->workflow_level->value == WorkflowLevel::Finalized->value) || ($this->workflow_level->value == WorkflowLevel::Published->value); } public function isStudyComplete(): bool { return $this->workflow_level->value >= WorkflowLevel::Finalized->value; } public function bookmarkedByUsers() { return $this->belongsToMany(User::class, 'study_bookmarks'); } protected function casts(): array { return [ 'workflow_level' => WorkflowLevel::class, 'priority' => Priority::class, 'archived_at' => 'immutable_datetime', 'approved_at' => 'immutable_datetime', 'received_at' => 'immutable_datetime', 'read_at' => 'immutable_datetime', 'assigned_at' => 'immutable_datetime', 'study_date' => 'immutable_datetime', 'locked_at' => 'immutable_datetime', 'patient_birthdate' => 'immutable_date', ]; } }