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, 'reporting_physician_id');
}
public function assignedPhysician(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_physician_id');
}
public function dicomServer(): BelongsTo
{
return $this->belongsTo(DicomServer::class);
}
public function isAssigned(int $rad_id): bool
{
return $this->assigned_physician_id === $rad_id;
}
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 links(): array
{
return [
'history' => $this->getHistoryLink(),
'metadata' => $this->getMetadataLink(),
'zip' => $this->getArchiveLink(),
'stone' => $this->getStoneLink(),
'ohif' => $this->getOhifLink(),
'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()),
'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): void
{
$this->update(
[
'locking_physician_id' => me($user)->id,
'locked_at' => now(),
]
);
}
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 isAssignedTo(User|int|null $user = null): bool
{
return $this->assigned_physician_id === me($user)->id;
}
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['reporting_physician_id'] = $user_id;
$params['reported_at'] = now();
break;
case ReportStatus::Approved:
if ($this->reporting_physician_id === null) {
$params['reporting_physician_id'] = $user_id;
$params['reported_at'] = now();
}
$params['approving_physician_id'] = $user_id;
$params['approved_at'] = now();
break;
}
$this->update($params);
}
public function canEditReport(): bool
{
$this->report_status <= ReportStatus::Finalized;
}
protected function casts(): array
{
return [
'is_archived' => 'boolean',
'study_status' => StudyLevelStatus::class,
'report_status' => ReportStatus::class,
'priority' => Priority::class,
'received_at' => 'immutable_datetime',
'reported_at' => 'immutable_datetime',
'assigned_at' => 'immutable_datetime',
'study_date' => 'immutable_datetime',
'patient_birthdate' => 'immutable_date',
];
}
}