$dt->format(self::DATE_FORMAT_SHORT), 'subtitle' => $dt->diffForHumans(), ] ); } public function dataTable(QueryBuilder $query): EloquentDataTable { $dataTable = new EloquentDataTable($query); $rawColumns = [ 'priority_icon', 'report_status_led', ]; foreach ($this->renderCustomColumns() as $column => $content) { $dataTable->addColumn($column, $content); $rawColumns[] = $column; } $this->applyFilters($dataTable); $orderColumns = []; $sorted = $this->getSortColumns(); $columns = $this->getColumns(); foreach ($sorted as $item) { $orderColumns[] = $columns[(int) $item['column']]['data']; /* switch ((int) $item['column']) { case 0: $orderColumns[] = WorklistColumn::Priority->value; break; case 8: $orderColumns[] = WorklistColumn::StudyDate->value; break; case 9: $orderColumns[] = WorklistColumn::ReceiveDate->value; break; case 10: $orderColumns[] = WorklistColumn::StudyDescription->value; break; } */ } $dataTable = $dataTable ->rawColumns($rawColumns) ->orderColumns($orderColumns, ':column $1') ->setRowId('id'); return $dataTable; } /** * Get the query source of dataTable. */ public function query(Study $model): QueryBuilder { return WorklistFactory::getLister()->query(); // return $model->newQuery(); } /** * Optional method if you want to use the html builder. */ public function html(): HtmlBuilder { $order = $this->getSortColumns(); return $this ->builder() ->setTableId('worklist-table') ->columns($this->getColumns()) ->minifiedAjax() ->searchPanes(SearchPane::make(['show' => true, 'hideCount' => true])) ->parameters( [ // 'dom' => 'Pfrtip', // 'dom' => 'Bfrtip', 'buttons' => [ 'searchPanes', // 'excel', // 'csv', // 'pdf', // 'print', // 'reset', // 'reload', ], 'order' => $order, ]) ->pageLength(15) ->lengthMenu([15, 25, 50, 100, 250]); } /** * Get the dataTable columns definition. */ public function getColumns(): array { $columns = []; foreach (WorklistGuard::worklistColumns() as $col) { switch ($col) { case WorklistColumn::Priority: $columns[] = Column::make($col->value) ->searchable(false) ->hidden(); $columns[] = Column::make('priority_icon') ->searchable(false) ->orderable(false) ->addClass('text-center p-0 ps-1') ->width('18px') ->title(''); break; case WorklistColumn::ReportStatus: $columns[] = Column::make($col->value) ->searchable(false) ->hidden(); $columns[] = Column::make('report_status_led') ->searchable(false) ->orderable(false) ->addClass('text-center p-0 ps-2') ->width('18px') ->title(''); break; case WorklistColumn::History: $columns[] = Column::make($col->value) ->searchable(false) ->orderable(false) ->addClass('text-center') ->width('16px') ->title(''); break; case WorklistColumn::PatientSexAge: $columns[] = Column::make($col->value) ->searchable(false) ->orderable(false) ->addClass('text-center') ->title('Age'); break; case WorklistColumn::StudyDate: $columns[] = Column::make($col->value) ->searchable(false) ->title('Scanned'); break; case WorklistColumn::ReceiveDate: $columns[] = Column::make($col->value) ->searchable(false) ->title('Received'); break; case WorklistColumn::ReportDate: $columns[] = Column::make($col->value) ->searchable(false) ->title('Read At'); break; case WorklistColumn::AssignedPhysician: case WorklistColumn::ReadingPhysician: $columns[] = Column::make($col->value) ->orderable(false) ->searchable(false) ->title('Rad'); break; case WorklistColumn::ActionButtons: case WorklistColumn::ViewerButtons: case WorklistColumn::ReportButtons: $columns[] = Column::computed($col->value) ->searchable(false) ->orderable(false) ->exportable(false) ->printable(false) // ->width(60) // ->addClass('p-0 ps-2') ->title(''); break; case WorklistColumn::PatientId: $columns[] = Column::make($col->value)->title('ID'); break; case WorklistColumn::PatientName: $columns[] = Column::make($col->value)->title('Patient'); break; case WorklistColumn::StudyDescription: $columns[] = Column::make($col->value)->title('Study'); break; case WorklistColumn::Series: $columns[] = Column::make($col->value)->orderable(false); break; case WorklistColumn::Modality: $columns[] = Column::make($col->value) ->orderable(true) ->title('Mod'); break; case WorklistColumn::Organization: case WorklistColumn::Department: $columns[] = Column::make($col->value) ->searchable(true) ->orderable(true) ->title(formatTitle($col->value)); break; default: // dd(Str::slug($col->value, '-')); $columns[] = Column::make($col->value)->title(formatTitle($col->value)); break; } } return $columns; } /** * Get the filename for export. */ protected function filename(): string { $parts = [ config('app.name'), 'worklist', date('YmdHi'), ]; return Str::slug(implode(' ', $parts), '_'); } private function getSortColumns(): array { $order = $this->request()->query('order', []); if (empty($order)) { $order = [ [0, 'desc'], [8, 'desc'], ]; } return $order; } private function applyFilters(QueryDataTable $dataTable) { $status = strtolower($this->request()->get('status')); $studyFrom = $this->request()->get('study_from'); $studyTo = $this->request()->get('study_to'); $receiveFrom = $this->request()->get('receive_from'); $receiveTo = $this->request()->get('receive_to'); $assignFrom = $this->request()->get('assign_from'); $assignTo = $this->request()->get('assign_to'); $readFrom = $this->request()->get('read_from'); $readTo = $this->request()->get('read_to'); $readBy = $this->request()->get('read_by'); $modality = $this->request()->get('modality'); $search = $this->request()->get('search', ['value' => null])['value']; $dataTable->filter( function (QueryBuilder $query) use ($status, $studyFrom, $studyTo, $receiveFrom, $receiveTo, $modality, $assignFrom, $assignTo, $readFrom, $readTo, $readBy, $search) { $this->filterStatus($query, $status); $this->filterDateRange($query, 'study_date', $studyFrom, $studyTo); $this->filterDateRange($query, 'received_at', $receiveFrom, $receiveTo); $this->filterDateRange($query, 'assigned_at', $assignFrom, $assignTo); $this->filterDateRange($query, 'read_at', $readFrom, $readTo); $this->filterModalities($query, $modality); $this->filterReadBy($query, $readBy); $this->filterSearchTerm($query, $search); } ); } private function filterStatus(QueryBuilder $query, ?string $status): void { if (blank($status)) { return; } switch ($status) { case 'unread': $query->where('report_status', '<', ReportStatus::Finalized->value); break; case 'read': $query->where('report_status', '>=', ReportStatus::Finalized->value); break; case 'progress': $query->whereNotNull('locked_at'); break; case 'assigned': // allow only assigned and locked studies $query ->whereNotNull('assigned_at') // ->whereNull('locked_at') ->whereNull('read_at'); break; case 'orphan': $query->whereNull('assigned_at'); break; case 'bookmark': $query->whereHas('bookmarkedByUsers', function ($q) { $q->where('user_id', auth()->id()); }); break; } } private function filterModalities(QueryBuilder $query, ?string $request): void { if (blank($request)) { return; } $modalities = collect(explode(',', $request)) ->filter() ->each(fn ($m) => strtoupper($m)) ->unique() ->toArray(); $query->whereIn('modality', $modalities); } private function filterDateRange(QueryBuilder $query, string $column, ?string $from, ?string $to): void { if (blank($from)) { return; } $start = Carbon::parse($from); $end = filled($to) ? Carbon::parse($to) : null; if (is_null($end) || $start->eq($end)) { $query->whereDate($column, '=', $start); } else { $query ->whereDate($column, '>=', $start) ->whereDate($column, '<=', $end); } } private function physicianColumn(Study $study): ?string { $user = $study->readingPhysician; if ($user === null) { $html = ''; if ($study->assigned_at !== null) { $html .= Blade::render('_partials._img-tooltip', [ 'src' => asset('imgs/assigned.png'), 'class' => 'msg-icon', 'tip' => 'Assigned at ' . $study->assigned_at->format(self::DATE_FORMAT_LONG), ]); } if ($study->isLocked()) { $html .= Blade::render('_partials._img-tooltip', [ 'src' => asset('imgs/lock.png'), 'class' => 'ms-1 msg-icon', 'tip' => sprintf("Locked by %s\n at %s", $study->lockingPhysician?->display_name ?? '', $study->locked_at->format(self::DATE_FORMAT_LONG)), ]); } return $html; } $dt = $study->read_at; return Blade::render('staff.worklist.partials._radiologist-listing', [ 'avatar_url' => $user->avatar(), 'name' => $user->display_name, 'time' => $dt?->format(self::DATE_FORMAT_SHORT) ?? '~', 'human_time' => $dt?->diffForHumans() ?? '', ] ); } /** * @return array */ private function renderCustomColumns(): array { $columns = []; foreach (WorklistGuard::worklistColumns() as $col) { switch ($col) { case WorklistColumn::PatientId: $columns[$col->value] = static function (Study $study) { if (me()->isRadiologist() || ! $study->canEditMetadata()) { return $study->patient_id; } return '' . $study->patient_id . ''; }; break; case WorklistColumn::PatientName: $columns[$col->value] = static function (Study $study) { if (me()->isRadiologist() || ! $study->canEditMetadata()) { return $study->sanitizedPatientName(); } return '' . $study->sanitizedPatientName() . ''; }; break; case WorklistColumn::StudyDescription: $columns[$col->value] = static fn (Study $study) => $study->sanitizedStudyDescription(); break; case WorklistColumn::AssignedPhysician: $columns[$col->value] = static function (Study $study) { if ($study->assigned_at !== null) { return ''; } return null; }; break; case WorklistColumn::ReadingPhysician: $columns[$col->value] = fn (Study $study) => $this->physicianColumn($study); break; case WorklistColumn::Series: $columns[$col->value] = fn (Study $study) => Blade::render( 'staff.worklist.partials._multi-value-cell', [ 'title' => $study->numInstances(), 'subtitle' => human_filesize($study->disk_size), ] ); break; case WorklistColumn::Organization: case WorklistColumn::Department: $columns[$col->value] = fn (Study $study) => $study->{$col->value}?->name; break; case WorklistColumn::StudyDate: case WorklistColumn::ReceiveDate: case WorklistColumn::ReportDate: case WorklistColumn::AssignDate: case WorklistColumn::AuthorizeDate: case WorklistColumn::ArchiveDate: $columns[$col->value] = fn (Study $study) => self::renderDateColumn($study->{$col->value}); break; case WorklistColumn::History: $columns[$col->value] = fn (Study $study) => sprintf(' ', $study->hash, ($study->body_part_examined) ? 'text-muted' : 'text-primary'); break; case WorklistColumn::ActionButtons: $columns[$col->value] = fn (Study $study) => $this->generateActionButtons($study); break; case WorklistColumn::ViewerButtons: $columns[$col->value] = fn (Study $study) => $this->generateViewerButtons($study); break; case WorklistColumn::ReportButtons: $columns[$col->value] = fn (Study $study) => $this->generateReportingButtons($study); break; } } return $columns; } private function generateReportingButtons(Study $study): string { $hasReports = $study->hasReports(); $img = match (true) { $hasReports => 'report.png', me()->isRadiologist() => 'report-write.png', default => 'inbox.png', }; if (me()->isRadiologist()) { if ($study->isLocked() && ! $study->isLockedBy()) { // study was locked by someone else return Blade::render('_partials._img-tooltip', [ 'src' => asset('imgs/lock.png'), 'class' => 'msg-icon', 'tip' => 'Study has been locked', ] ); } if (! $study->canEditReport() && ! $study->isReadBy()) { // already reported by someone else, disallow access to the reports return Blade::render('_partials._img-tooltip', [ 'src' => asset('imgs/stop.png'), 'class' => 'msg-icon', 'tip' => 'This study has already been interpreted. Further reporting is not permitted.', ] ); } if (! $hasReports) { // fresh untouched study, go directly to the edit page return $this->renderImageLink( $study->hash, $img, '', 'Write Report', route('staff.report.create', $study->hash), true ); } } // fallback: show popup with report history return $this->renderImageLink( $study->hash, $img, 'show-reports', 'Report' ); } private function renderButton(string $data_id, string $fa_icon, string $data_class, string $text, string $url = '#', bool $blank = false): string { return Blade::render('staff.worklist.partials._column-button', [ 'data_id' => $data_id, 'url' => $url, 'fa_icon' => $fa_icon, 'data_class' => $data_class, 'text' => $text, 'blank' => $blank, ] ); } private function renderImageLink(string $data_id, string $img_src, string $data_class, string $text, string $url = '#', bool $blank = false, int $width = 18): string { return Blade::render('staff.worklist.partials._image-link', [ 'data_id' => $data_id, 'url' => $url, 'img_src' => asset('imgs/' . $img_src), 'data_class' => $data_class, 'text' => $text, 'blank' => $blank, 'width' => $width, ] ); } private function generateViewerButtons(Study $study): string { $btns = []; $btns[] = $this->renderImageLink( $study->hash, 'eye.png', '', 'Cornerstone Viewer', route('viewer.ohif', $study->hash), true ); $btns[] = $this->renderImageLink( $study->hash, 'live.png', '', 'Simple Viewer', route('viewer.stone', $study->hash), true ); $btns[] = $this->renderImageLink( $study->hash, 'weasis.png', '', 'Open Study in Weasis', $study->links()['weasis'], false ); return implode("\r", $btns); } private function generateActionButtons(Study $study): string { $btns = []; foreach (WorklistGuard::worklistButtons($study) as $button) { switch ($button) { case WorklistButton::StudyMetadata: $btns[] = $this->renderImageLink($study->hash, 'info.png', 'show-study', 'Info'); break; case WorklistButton::Assign: $btns[] = $this->renderImageLink($study->hash, 'assign.png', 'show-assign', 'Assign'); break; case WorklistButton::Notes: $btns[] = $this->renderImageLink($study->hash, 'chat.png', 'show-notes', 'Chat'); break; case WorklistButton::Audit: $btns[] = $this->renderImageLink($study->hash, 'audit.png', 'show-audit', 'Audit Trail'); break; case WorklistButton::Bookmark: if ($study->isBookmarkedBy()) { $btns[] = Blade::render('staff.worklist.partials.bookmarks._delete', [ 'study_id' => $study->id, 'user_id' => me()->id, 'url' => route('staff.bookmark.delete'), 'img' => asset('imgs/bookmark-delete.png'), 'tip' => 'Remove bookmark', ]); } else { $btns[] = Blade::render('staff.worklist.partials.bookmarks._create', [ 'study_id' => $study->id, 'user_id' => me()->id, 'url' => route('staff.bookmark.create'), 'img' => asset('imgs/bookmark.png'), 'tip' => 'Bookmark study', ]); } break; } } return implode("\r", $btns); } private function filterReadBy(QueryBuilder $query, ?int $readBy): void { if (is_null($readBy) || $readBy <= 0) { return; } $query->where('reading_physician_id', $readBy); } private function filterSearchTerm(QueryBuilder $query, ?string $search) { if (blank($search)) { return; } $term = '%' . strtoupper(trim($search)) . '%'; $query->where( static function ($q) use ($term) { $q ->where('patient_name', 'ILIKE', $term) ->orWhere('patient_id', 'ILIKE', $term) ->orWhere('study_description', 'ILIKE', $term); } ); } }