621 lines
22 KiB
PHP
621 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\DataTables;
|
|
|
|
use App\DAL\Studies\WorklistFactory;
|
|
use App\Domain\Report\ReportStatus;
|
|
use App\Models\Study;
|
|
use App\Services\ACL\WorklistButton;
|
|
use App\Services\ACL\WorklistColumn;
|
|
use App\Services\ACL\WorklistGuard;
|
|
use Carbon\Carbon;
|
|
use Carbon\CarbonImmutable;
|
|
use Closure;
|
|
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Str;
|
|
use Yajra\DataTables\EloquentDataTable;
|
|
use Yajra\DataTables\Html\Builder as HtmlBuilder;
|
|
use Yajra\DataTables\Html\Column;
|
|
use Yajra\DataTables\Html\SearchPane;
|
|
use Yajra\DataTables\QueryDataTable;
|
|
use Yajra\DataTables\Services\DataTable;
|
|
|
|
class WorklistDataTable extends DataTable
|
|
{
|
|
const DATE_FORMAT_SHORT = 'd.m.Y H:i';
|
|
const DATE_FORMAT_LONG = 'h:i A, d-M-Y (D)';
|
|
|
|
private static function renderDateColumn(Carbon|CarbonImmutable|null $dt): ?string
|
|
{
|
|
if ($dt === null) {
|
|
return null;
|
|
}
|
|
|
|
return Blade::render(
|
|
'staff.worklist.partials._multi-value-cell',
|
|
[
|
|
'title' => $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;
|
|
|
|
default:
|
|
// dd(Str::slug($col->value, '-'));
|
|
$columns[] = Column::make($col->value)->title($this->columTitle($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;
|
|
}
|
|
}
|
|
|
|
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 columTitle(string $str): string
|
|
{
|
|
return Str::of($str)->slug()->replace('-', ' ')->title()->toString();
|
|
}
|
|
|
|
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<string, Closure>
|
|
*/
|
|
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 '<a target="_blank" href="' . route('staff.meta.edit', $study->hash) . '">' . $study->patient_id . '</a>';
|
|
};
|
|
break;
|
|
case WorklistColumn::PatientName:
|
|
$columns[$col->value] = static function (Study $study) {
|
|
if (me()->isRadiologist() || ! $study->canEditMetadata()) {
|
|
return $study->sanitizedPatientName();
|
|
}
|
|
|
|
return '<a target="_blank" href="' . route('staff.history.edit', $study->hash) . '">' . $study->sanitizedPatientName() . '</a>';
|
|
};
|
|
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 '<img src="' . asset('imgs/assigned.png') . '" data-bs-toggle="tooltip" data-bs-placement="right" title="' . $study->assigned_at->format(self::DATE_FORMAT_LONG) . '">';
|
|
}
|
|
|
|
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::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('
|
|
<a href="#" data-id="%s" class="btn btn-sm btn-outline-light show-attach" data-bs-toggle="tooltip" data-bs-placement="right" title="xxx">
|
|
<i class="fa-light fa-file-prescription %s"></i>
|
|
</a>
|
|
',
|
|
$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 = $hasReports ? 'report.png' : '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, '', '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', '', 'OHIF Viewer', route('viewer.ohif', $study->hash), true
|
|
);
|
|
$btns[] = $this->renderImageLink(
|
|
$study->hash, 'weasis.png', '', 'WEASIS Viewer', $study->links()['weasis'], false
|
|
);
|
|
$btns[] = $this->renderImageLink(
|
|
$study->hash, 'live.png', '', 'STONE Viewer', route('viewer.stone', $study->hash), true
|
|
);
|
|
|
|
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', 'showStudy', '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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
);
|
|
}
|
|
}
|