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 getReportStatusLedAttribute(): string { $color = match ($this->report_status) { ReportStatus::Unread => 'bg-white', // ReportStatus::Opened => 'bg-secondary', ReportStatus::Preliminary => 'bg-info', ReportStatus::Finalized => 'bg-primary', ReportStatus::Approved => 'bg-success', default => 'bg-light', }; // $icon = match ($this->report_status) { ReportStatus::Unread => 'spinner text-muted', ReportStatus::Preliminary => 'pen-to-square', ReportStatus::Finalized => 'badge-check', ReportStatus::Approved => '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, 'report_status_led' => $this->getReportStatusLedAttribute(), '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 => sprintf($tpl, 'danger', 'light-emergency-on'), Priority::High => sprintf($tpl, 'primary', 'bolt'), 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, ?StudyLevelStatus $status = null): void { $params = [ 'locking_physician_id' => me($user)->id, 'locked_at' => now(), ]; if ($status) { $params['study_status'] = $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; } /** * 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 setReportStatus(ReportStatus $status, User|int|null $user = null): void { $user_id = me($user)->id; $params = ['report_status' => $status->value]; switch ($status) { case ReportStatus::Finalized: $params['reading_physician_id'] = $user_id; $params['read_at'] = now(); break; case ReportStatus::Approved: 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(ReportStatus::Finalized); } public function reportStatusBefore(ReportStatus $checkpoint): bool { return $this->report_status->value < $checkpoint->value; } public function canEditMetadata(): bool { return $this->isActive() && $this->reportStatusBefore(ReportStatus::Finalized); } public function canAssignRad(): bool { if ($this->isLocked()) { return false; } return $this->isActive() && $this->reportStatusBefore(ReportStatus::Preliminary); } public function hasReports(): bool { return $this->reports->isNotEmpty(); } public function isReportReady(): bool { return ($this->report_status->value == ReportStatus::Finalized->value) || ($this->report_status->value == ReportStatus::Approved->value); } public function isStudyComplete(): bool { return $this->report_status->value >= ReportStatus::Finalized->value; } protected function casts(): array { return [ 'is_archived' => 'boolean', 'study_status' => StudyLevelStatus::class, 'report_status' => ReportStatus::class, 'priority' => Priority::class, 'received_at' => 'immutable_datetime', 'read_at' => 'immutable_datetime', 'assigned_at' => 'immutable_datetime', 'study_date' => 'immutable_datetime', 'locked_at' => 'immutable_datetime', 'patient_birthdate' => 'immutable_date', ]; } }