Compare commits

...

16 Commits

Author SHA1 Message Date
72b1bbc0e7 minor 2025-01-29 22:26:44 +06:00
27c3cd5d8d wip dept-org 2025-01-29 22:20:34 +06:00
7f5e916d80 datatable 2025-01-29 22:12:47 +06:00
8853528b0a remove unneeded 2025-01-29 22:08:24 +06:00
3d5cceb125 FIX - value 2025-01-29 22:08:13 +06:00
db97aef03c footer version 2025-01-29 22:08:00 +06:00
ba705bb98b removed report_status 2025-01-29 21:47:50 +06:00
9e2315b89b sync 2025-01-29 21:28:39 +06:00
bdf1ad1ca3 workflow_level 2025-01-29 21:26:37 +06:00
5292865bef report workflow_level 2025-01-29 21:25:28 +06:00
d1d6f0d7af workflow_level 2025-01-29 21:24:45 +06:00
3f2a38faa6 workflow_level 2025-01-29 21:24:41 +06:00
ef170b5357 workflow_level 2025-01-29 21:20:42 +06:00
b32ebecfaa wf 2025-01-29 21:17:26 +06:00
15d8ebf3c7 removed report_status 2025-01-29 21:17:12 +06:00
6bffd31f2f FIX - role name 2025-01-29 21:05:29 +06:00
35 changed files with 420 additions and 140 deletions

View File

@ -1,4 +1,5 @@
APP_NAME=Laravel APP_NAME="PixelBridge"
APP_VERSION="25.1/a"
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true

View File

@ -2,12 +2,13 @@
namespace App\DAL; namespace App\DAL;
use App\Domain\ACL\Role;
use App\Services\UserService; use App\Services\UserService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
final readonly class Radiologists final readonly class Radiologists
{ {
public static function worklist_stats(int $days, int $report_status) public static function worklist_stats(int $days, int $workflow_level)
{ {
$sql = <<<'SQL' $sql = <<<'SQL'
SELECT SELECT
@ -28,12 +29,12 @@ public static function worklist_stats(int $days, int $report_status)
WHERE WHERE
--st.received_at :: DATE >= NOW() - INTERVAL '3 DAY' --st.received_at :: DATE >= NOW() - INTERVAL '3 DAY'
st.received_at::DATE >= NOW() - INTERVAL '%d DAY' st.received_at::DATE >= NOW() - INTERVAL '%d DAY'
AND st.report_status < %d AND st.workflow_level < %d
AND st.read_at IS NULL AND st.read_at IS NULL
GROUP BY GROUP BY
sa.user_id) AS cte ON cte.user_id = users."id" sa.user_id) AS cte ON cte.user_id = users."id"
WHERE WHERE
roles."name" = 'radiologist' roles."name" = '%s'
AND users.is_active = TRUE AND users.is_active = TRUE
GROUP BY GROUP BY
users."id", users."id",
@ -42,7 +43,7 @@ public static function worklist_stats(int $days, int $report_status)
users.display_name users.display_name
SQL; SQL;
$rows = DB::select(sprintf($sql, $days, $report_status)); $rows = DB::select(sprintf($sql, $days, $workflow_level, Role::Radiologist->value));
foreach ($rows as $row) { foreach ($rows as $row) {
$row->last_seen = UserService::getLastSeen((int) $row->id); $row->last_seen = UserService::getLastSeen((int) $row->id);
} }

View File

@ -2,7 +2,6 @@
namespace App\DAL\Studies; namespace App\DAL\Studies;
use App\Domain\Report\ReportStatus;
use App\Domain\Study\WorkflowLevel; use App\Domain\Study\WorkflowLevel;
use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@ -13,8 +12,6 @@ public function setRadiologist(int $radiologist_id): self;
public function setWorkflowLevel(WorkflowLevel $status): self; public function setWorkflowLevel(WorkflowLevel $status): self;
public function setReportStatus(ReportStatus $status): self;
public function setPerPage(int $size): self; public function setPerPage(int $size): self;
public function setSortOrder(string $order): self; public function setSortOrder(string $order): self;

View File

@ -2,7 +2,6 @@
namespace App\DAL\Studies; namespace App\DAL\Studies;
use App\Domain\Report\ReportStatus;
use App\Domain\Study\WorkflowLevel; use App\Domain\Study\WorkflowLevel;
use App\Models\Study; use App\Models\Study;
use Carbon\Carbon; use Carbon\Carbon;
@ -21,8 +20,6 @@ abstract class WorklistBase implements IUserStudyLister
private ?WorkflowLevel $workflowLevel = null; private ?WorkflowLevel $workflowLevel = null;
private ?ReportStatus $reportStatus = null;
private ?bool $locked = null; private ?bool $locked = null;
private ?bool $archived = null; private ?bool $archived = null;
@ -41,7 +38,7 @@ abstract class WorklistBase implements IUserStudyLister
protected static function reportCompleteQuery(Builder $query): Builder protected static function reportCompleteQuery(Builder $query): Builder
{ {
return $query->where('report_status', '=', ReportStatus::Approved->value); return $query->where('workflow_level', '=', WorkflowLevel::Published->value);
} }
public function setRadiologist(int $radiologist_id): self public function setRadiologist(int $radiologist_id): self
@ -58,13 +55,6 @@ public function setWorkflowLevel(WorkflowLevel $status): self
return $this; return $this;
} }
public function setReportStatus(ReportStatus $status): self
{
$this->reportStatus = $status;
return $this;
}
public function query(?int $user_id = null): Builder public function query(?int $user_id = null): Builder
{ {
$query = $this->buildQuery($user_id); $query = $this->buildQuery($user_id);
@ -236,15 +226,6 @@ private function applyWorkflowLevel(Builder $query): Builder
return $query; return $query;
} }
private function applyReportStatus(Builder $query): Builder
{
if ($this->reportStatus != null) {
$query = $query->where('report_status', '=', $this->reportStatus->value);
}
return $query;
}
private function getPageSize(?int $user_id = null): int private function getPageSize(?int $user_id = null): int
{ {
return $this->perPage ?? user_per_page($user_id); return $this->perPage ?? user_per_page($user_id);

View File

@ -3,7 +3,7 @@
namespace App\DataTables; namespace App\DataTables;
use App\DAL\Studies\WorklistFactory; use App\DAL\Studies\WorklistFactory;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Models\Study; use App\Models\Study;
use App\Services\ACL\WorklistButton; use App\Services\ACL\WorklistButton;
use App\Services\ACL\WorklistColumn; use App\Services\ACL\WorklistColumn;
@ -47,7 +47,7 @@ public function dataTable(QueryBuilder $query): EloquentDataTable
$dataTable = new EloquentDataTable($query); $dataTable = new EloquentDataTable($query);
$rawColumns = [ $rawColumns = [
'priority_icon', 'priority_icon',
'report_status_led', 'workflow_level_led',
]; ];
foreach ($this->renderCustomColumns() as $column => $content) { foreach ($this->renderCustomColumns() as $column => $content) {
$dataTable->addColumn($column, $content); $dataTable->addColumn($column, $content);
@ -148,11 +148,11 @@ public function getColumns(): array
->title(''); ->title('');
break; break;
case WorklistColumn::ReportStatus: case WorklistColumn::WorkflowLevel:
$columns[] = Column::make($col->value) $columns[] = Column::make($col->value)
->searchable(false) ->searchable(false)
->hidden(); ->hidden();
$columns[] = Column::make('report_status_led') $columns[] = Column::make('workflow_level_led')
->searchable(false) ->searchable(false)
->orderable(false) ->orderable(false)
->addClass('text-center p-0 ps-2') ->addClass('text-center p-0 ps-2')
@ -330,10 +330,10 @@ private function filterStatus(QueryBuilder $query, ?string $status): void
switch ($status) { switch ($status) {
case 'unread': case 'unread':
$query->where('report_status', '<', ReportStatus::Finalized->value); $query->where('workflow_level', '<', WorkflowLevel::Finalized->value);
break; break;
case 'read': case 'read':
$query->where('report_status', '>=', ReportStatus::Finalized->value); $query->where('workflow_level', '>=', WorkflowLevel::Finalized->value);
break; break;
case 'progress': case 'progress':
$query->whereNotNull('locked_at'); $query->whereNotNull('locked_at');

View File

@ -1,14 +0,0 @@
<?php
namespace App\Domain\Report;
enum ReportStatus: int
{
case Unread = 0;
case Dictated = 10;
case Transcribed = 20;
case Preliminary = 30;
case Finalized = 40;
case Approved = 90;
case Cancelled = 199;
}

View File

@ -4,9 +4,36 @@
enum WorkflowLevel: int enum WorkflowLevel: int
{ {
case Pending = 0; case Received = 10;
case Unassigned = 10; case Unassigned = 20;
case Assigned = 20; case Assigned = 30;
case ReadInProgress = 30; case Dictated = 40;
case ReadCompleted = 40; case Transcribed = 50;
case Repetition = 60;
case ReadInProgress = 70;
case DraftAvailable = 80;
case Finalized = 90;
case UnderReview = 100;
case Published = 110;
case Archived = 200;
case Cancelled = 240;
public function description(): string
{
return match ($this) {
self::Received => 'Study Received',
self::Unassigned => 'Unassigned',
self::Assigned => 'Assigned',
self::Dictated => 'Dictated',
self::Transcribed => 'Transcribed',
self::Repetition => 'Repetition',
self::ReadInProgress => 'Read In Progress',
self::DraftAvailable => 'Draft interpretation available',
self::Finalized => 'Repoort finalized',
self::UnderReview => 'Under Review',
self::Published => 'Report published',
self::Archived => 'Study archived',
self::Cancelled => 'Cancelled',
};
}
} }

View File

@ -0,0 +1,93 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\DepartmentResource\Pages;
use App\Models\Department;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Ramsey\Uuid\Uuid;
class DepartmentResource extends Resource
{
protected static ?string $model = Department::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('guid')
->required()
->maxLength(40)
->default(sprintf('FAC-%s', Uuid::uuid4())),
Toggle::make('is_active')
->required(),
Select::make('organization_id')
->relationship('organization', 'name')
->required(),
TextInput::make('name')
->required()
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('guid')
->searchable(),
IconColumn::make('is_active')
->boolean(),
TextColumn::make('organization.name')
->numeric()
->sortable(),
TextColumn::make('name')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDepartments::route('/'),
'create' => Pages\CreateDepartment::route('/create'),
'edit' => Pages\EditDepartment::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\DepartmentResource\Pages;
use App\Filament\Resources\DepartmentResource;
use Filament\Resources\Pages\CreateRecord;
class CreateDepartment extends CreateRecord
{
protected static string $resource = DepartmentResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DepartmentResource\Pages;
use App\Filament\Resources\DepartmentResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditDepartment extends EditRecord
{
protected static string $resource = DepartmentResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DepartmentResource\Pages;
use App\Filament\Resources\DepartmentResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListDepartments extends ListRecords
{
protected static string $resource = DepartmentResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\OrganizationResource\Pages;
use App\Models\Organization;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Ramsey\Uuid\Uuid;
class OrganizationResource extends Resource
{
protected static ?string $model = Organization::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('guid')
->required()
->maxLength(40)
->default(sprintf('INS-%s', Uuid::uuid4())),
TextInput::make('name')
->required()
->maxLength(255),
Toggle::make('is_active')
->required(),
TextInput::make('address')
->maxLength(255),
TextInput::make('logo_path')
->maxLength(255),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('guid')
->searchable(),
TextColumn::make('name')
->searchable(),
IconColumn::make('is_active')
->boolean(),
TextColumn::make('address')
->searchable(),
TextColumn::make('logo_path')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
EditAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListOrganizations::route('/'),
'create' => Pages\CreateOrganization::route('/create'),
'edit' => Pages\EditOrganization::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateOrganization extends CreateRecord
{
protected static string $resource = OrganizationResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditOrganization extends EditRecord
{
protected static string $resource = OrganizationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\OrganizationResource\Pages;
use App\Filament\Resources\OrganizationResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListOrganizations extends ListRecords
{
protected static string $resource = OrganizationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -5,7 +5,7 @@
use App\DAL\Radiologists; use App\DAL\Radiologists;
use App\Domain\ACL\Permission; use App\Domain\ACL\Permission;
use App\Domain\ACL\Role; use App\Domain\ACL\Role;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Http\Controllers\HashedStudyControllerBase; use App\Http\Controllers\HashedStudyControllerBase;
use App\Http\Requests\AssignPhysicianRequest; use App\Http\Requests\AssignPhysicianRequest;
use App\Models\User; use App\Models\User;
@ -22,7 +22,7 @@ public function show()
->get(['id', 'display_name', 'profile_photo_path', 'first_name', 'last_name', 'created_at']) ->get(['id', 'display_name', 'profile_photo_path', 'first_name', 'last_name', 'created_at'])
->each(fn ($rad) => $rad->info = ['workload' => '', 'last_seen' => '']); ->each(fn ($rad) => $rad->info = ['workload' => '', 'last_seen' => '']);
$stats = Radiologists::worklist_stats(3, ReportStatus::Finalized->value); $stats = Radiologists::worklist_stats(3, WorkflowLevel::Finalized->value);
foreach ($stats as $rad) { foreach ($stats as $rad) {
$found = $rads->where('id', $rad->id)->first(); $found = $rads->where('id', $rad->id)->first();
if ($found) { if ($found) {

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers\Staff; namespace App\Http\Controllers\Staff;
use App\Domain\ACL\Permission; use App\Domain\ACL\Permission;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Http\Controllers\HashedStudyControllerBase; use App\Http\Controllers\HashedStudyControllerBase;
use App\Http\Requests\StudyMetadataUpdateRequest; use App\Http\Requests\StudyMetadataUpdateRequest;
use App\Models\Study; use App\Models\Study;
@ -32,7 +32,7 @@ public function edit()
public function save(StudyMetadataUpdateRequest $request) public function save(StudyMetadataUpdateRequest $request)
{ {
abort_unless(auth()->user()->may(Permission::StudyMetadataEdit), 403); abort_unless(may(Permission::StudyMetadataEdit), 403);
$study = $this->getStudy(); $study = $this->getStudy();
if ($study->isReportReady()) { if ($study->isReportReady()) {
return $this->lockedNotice(); return $this->lockedNotice();
@ -43,7 +43,7 @@ public function save(StudyMetadataUpdateRequest $request)
$payload['patient_sex'] = strtoupper($payload['patient_sex']); $payload['patient_sex'] = strtoupper($payload['patient_sex']);
if ($request->has('cancel_read')) { if ($request->has('cancel_read')) {
// lock the study if report is not needed // lock the study if report is not needed
$payload['report_status'] = ReportStatus::Cancelled->value; $payload['workflow_level'] = WorkflowLevel::Cancelled->value;
$payload['locked_at'] = now(); $payload['locked_at'] = now();
unset($payload['cancel_read']); unset($payload['cancel_read']);
} }

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers\Staff; namespace App\Http\Controllers\Staff;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Http\Controllers\HashidControllerBase; use App\Http\Controllers\HashidControllerBase;
use App\Http\Requests\StoreReportRequest; use App\Http\Requests\StoreReportRequest;
use App\Models\Study; use App\Models\Study;
@ -39,10 +39,10 @@ public function save(StoreReportRequest $request)
ReportManager::ensureEditAccess(); ReportManager::ensureEditAccess();
$this->decodeKeys(); $this->decodeKeys();
$manager = ReportManager::make($this->key); $manager = ReportManager::make($this->key);
$reportStatus = ReportStatus::from($request->integer('report_status')); $workflow_level = WorkflowLevel::from($request->integer('report_status'));
$report = $manager->createReport(request('content'), $reportStatus); $report = $manager->createReport(request('content'), $workflow_level);
if ($reportStatus->value === ReportStatus::Finalized->value) { if ($workflow_level->value === WorkflowLevel::Finalized->value) {
$manager->finalizeReport($report); $manager->finalizeReport($report);
} }

View File

@ -2,7 +2,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Models\Study; use App\Models\Study;
use App\Rules\ExistsByHash; use App\Rules\ExistsByHash;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
@ -22,7 +22,7 @@ public function rules(): array
'content' => 'required', 'content' => 'required',
'report_status' => [ 'report_status' => [
'required', 'required',
Rule::enum(ReportStatus::class)->only([ReportStatus::Preliminary, ReportStatus::Finalized, ReportStatus::Approved]), Rule::enum(WorkflowLevel::class)->only([WorkflowLevel::DraftAvailable, WorkflowLevel::Finalized, WorkflowLevel::Published]),
], ],
]; ];
} }

View File

@ -175,22 +175,23 @@ public function isArchived(): bool
return $this->archived_at !== null; return $this->archived_at !== null;
} }
public function getReportStatusLedAttribute(): string public function getWorkflowLevelLedAttribute(): string
{ {
$color = match ($this->report_status) { // todo: implement
ReportStatus::Unread => 'bg-white', $color = match ($this->workflow_level) {
WorkflowLevel::Unassigned => 'bg-white',
// ReportStatus::Opened => 'bg-secondary', // ReportStatus::Opened => 'bg-secondary',
ReportStatus::Preliminary => 'bg-info', WorkflowLevel::DraftAvailable => 'bg-info',
ReportStatus::Finalized => 'bg-primary', WorkflowLevel::Finalized => 'bg-primary',
ReportStatus::Approved => 'bg-success', WorkflowLevel::Published => 'bg-success',
default => 'bg-light', default => 'bg-light',
}; };
// <i class="fa-solid fa-spinner"></i> // <i class="fa-solid fa-spinner"></i>
$icon = match ($this->report_status) { $icon = match ($this->workflow_level) {
ReportStatus::Unread => 'spinner text-muted', WorkflowLevel::Unassigned => 'spinner text-muted',
ReportStatus::Preliminary => 'pen-to-square', WorkflowLevel::DraftAvailable => 'pen-to-square',
ReportStatus::Finalized => 'badge-check', WorkflowLevel::Finalized => 'badge-check',
ReportStatus::Approved => 'shield-check', WorkflowLevel::Published => 'shield-check',
default => 'spinner text-muted', default => 'spinner text-muted',
}; };
@ -315,7 +316,7 @@ public function toArray(): array
'reader_name' => $this->readingPhysician?->display_name, 'reader_name' => $this->readingPhysician?->display_name,
// 'assigned_physician_name' => $this->assignedPhysician?->display_name, // 'assigned_physician_name' => $this->assignedPhysician?->display_name,
'reader_photo' => $this->readingPhysician?->profile_photo_url, 'reader_photo' => $this->readingPhysician?->profile_photo_url,
'report_status_led' => $this->getReportStatusLedAttribute(), 'workflow_level_led' => $this->getWorkflowLevelLedAttribute(),
'priority_icon' => $this->getPriorityIcon(), 'priority_icon' => $this->getPriorityIcon(),
'sex_age' => $this->sexAge(), 'sex_age' => $this->sexAge(),
'num_instances' => $this->numInstances(), 'num_instances' => $this->numInstances(),
@ -456,18 +457,18 @@ public function canObtainLock(User|int|null $user = null): bool
return $this->isUnlocked() || $this->isLockedBy($user); return $this->isUnlocked() || $this->isLockedBy($user);
} }
public function setReportStatus(ReportStatus $status, User|int|null $user = null): void public function setReportWorkflowLevel(WorkflowLevel $level, User|int|null $user = null): void
{ {
$user_id = me($user)->id; $user_id = me($user)->id;
$params = ['report_status' => $status->value]; $params = ['workflow_level' => $level->value];
switch ($status) { switch ($level) {
case ReportStatus::Finalized: case WorkflowLevel::Finalized:
$params['reading_physician_id'] = $user_id; $params['reading_physician_id'] = $user_id;
$params['read_at'] = now(); $params['read_at'] = now();
break; break;
case ReportStatus::Approved: case WorkflowLevel::Published:
if ($this->reading_physician_id === null) { if ($this->reading_physician_id === null) {
$params['reading_physician_id'] = $user_id; $params['reading_physician_id'] = $user_id;
$params['read_at'] = now(); $params['read_at'] = now();
@ -483,18 +484,18 @@ public function setReportStatus(ReportStatus $status, User|int|null $user = null
public function canEditReport(): bool public function canEditReport(): bool
{ {
// todo: check if the study disallows reporting // todo: check if the study disallows reporting
return $this->isActive() && $this->reportStatusBefore(ReportStatus::Finalized); return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::Finalized);
} }
public function reportStatusBefore(ReportStatus $checkpoint): bool public function reportStatusBefore(WorkflowLevel $checkpoint): bool
{ {
return $this->report_status->value < $checkpoint->value; return $this->workflow_level->value < $checkpoint->value;
} }
public function canEditMetadata(): bool public function canEditMetadata(): bool
{ {
return $this->isActive() && $this->reportStatusBefore(ReportStatus::Finalized); return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::Finalized);
} }
public function canAssignRad(): bool public function canAssignRad(): bool
@ -503,7 +504,7 @@ public function canAssignRad(): bool
return false; return false;
} }
return $this->isActive() && $this->reportStatusBefore(ReportStatus::Preliminary); return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::DraftAvailable);
} }
public function hasReports(): bool public function hasReports(): bool
@ -513,13 +514,13 @@ public function hasReports(): bool
public function isReportReady(): bool public function isReportReady(): bool
{ {
return ($this->report_status->value == ReportStatus::Finalized->value) || return ($this->workflow_level->value == WorkflowLevel::Finalized->value) ||
($this->report_status->value == ReportStatus::Approved->value); ($this->workflow_level->value == WorkflowLevel::Published->value);
} }
public function isStudyComplete(): bool public function isStudyComplete(): bool
{ {
return $this->report_status->value >= ReportStatus::Finalized->value; return $this->workflow_level->value >= WorkflowLevel::Finalized->value;
} }
public function bookmarkedByUsers() public function bookmarkedByUsers()
@ -531,7 +532,6 @@ protected function casts(): array
{ {
return [ return [
'workflow_level' => WorkflowLevel::class, 'workflow_level' => WorkflowLevel::class,
'report_status' => ReportStatus::class,
'priority' => Priority::class, 'priority' => Priority::class,
'archived_at' => 'immutable_datetime', 'archived_at' => 'immutable_datetime',
'approved_at' => 'immutable_datetime', 'approved_at' => 'immutable_datetime',

View File

@ -3,7 +3,7 @@
namespace App\Models; namespace App\Models;
use App\Domain\Report\ExportFormat; use App\Domain\Report\ExportFormat;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Services\Report\ReportStorage; use App\Services\Report\ReportStorage;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasTimestamps; use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
@ -43,19 +43,19 @@ public function scopeForStudy(Builder $query, Study|int $study): Builder
return $query; return $query;
} }
public function setStatus(ReportStatus $status, User|int|null $user = null): void public function setWorkflowLevel(WorkflowLevel $level, User|int|null $user = null): void
{ {
$user_id = me($user)->id; $user_id = me($user)->id;
$params = ['report_status' => $status->value]; $params = ['workflow_level' => $level->value];
switch ($status) { switch ($level) {
case ReportStatus::Dictated: case WorkflowLevel::Dictated:
$params['dictated_by_id'] = $user_id; $params['dictated_by_id'] = $user_id;
break; break;
case ReportStatus::Preliminary: case WorkflowLevel::DraftAvailable:
case ReportStatus::Finalized: case WorkflowLevel::Finalized:
$params['read_by_id'] = $user_id; $params['read_by_id'] = $user_id;
break; break;
case ReportStatus::Approved: case WorkflowLevel::Published:
$params['approved_by_id'] = $user_id; $params['approved_by_id'] = $user_id;
break; break;
} }
@ -96,12 +96,12 @@ public function getContent(): ?string
public function eligibleForExport(): bool public function eligibleForExport(): bool
{ {
return $this->report_status->value >= ReportStatus::Finalized->value; return $this->workflow_level->value >= WorkflowLevel::Finalized->value;
} }
public function canRemove(User|int|null $user = null): bool public function canRemove(User|int|null $user = null): bool
{ {
if ($this->report_status->value < ReportStatus::Finalized->value) { if ($this->workflow_level->value < WorkflowLevel::Finalized->value) {
if ($this->read_by_id === me($user)->id) { if ($this->read_by_id === me($user)->id) {
return true; return true;
} }
@ -117,7 +117,7 @@ public function canRemove(User|int|null $user = null): bool
public function canEdit(User|int|null $user = null): bool public function canEdit(User|int|null $user = null): bool
{ {
if ($this->report_status->value < ReportStatus::Finalized->value && if ($this->workflow_level->value < WorkflowLevel::Finalized->value &&
$this->study->canEditReport() && $this->study->canEditReport() &&
$this->read_by_id === me($user)->id) { $this->read_by_id === me($user)->id) {
return true; return true;
@ -128,7 +128,7 @@ public function canEdit(User|int|null $user = null): bool
public function isFinalized(): bool public function isFinalized(): bool
{ {
return $this->report_status->value >= ReportStatus::Finalized->value; return $this->workflow_level->value >= WorkflowLevel::Finalized->value;
} }
/** /**
@ -137,7 +137,7 @@ public function isFinalized(): bool
protected function casts(): array protected function casts(): array
{ {
return [ return [
'report_status' => ReportStatus::class, 'workflow_level' => WorkflowLevel::class,
]; ];
} }
} }

View File

@ -38,7 +38,7 @@ class User extends Authenticatable implements FilamentUser
use HasProfilePhoto; use HasProfilePhoto;
use HasRoles; use HasRoles;
use Notifiable; use Notifiable;
use TwoFactorAuthenticatable; // use TwoFactorAuthenticatable;
public array $info = []; public array $info = [];

View File

@ -9,13 +9,6 @@ final class RoleService
{ {
private static array $roles = []; private static array $roles = [];
private static function initCache(): void
{
if (empty(self::$roles)) {
self::$roles = SpatieRole::pluck('id', 'name')->toArray();
}
}
public static function select(): array public static function select(): array
{ {
// self::initCache(); // self::initCache();
@ -25,4 +18,11 @@ public static function select(): array
->mapWithKeys(fn (Role $r) => [$r->value => $r->name]) ->mapWithKeys(fn (Role $r) => [$r->value => $r->name])
->toArray(); ->toArray();
} }
private static function initCache(): void
{
if (empty(self::$roles)) {
self::$roles = SpatieRole::pluck('id', 'name')->toArray();
}
}
} }

View File

@ -5,7 +5,6 @@
enum WorklistColumn: string enum WorklistColumn: string
{ {
case WorkflowLevel = 'workflow_level'; case WorkflowLevel = 'workflow_level';
case ReportStatus = 'report_status';
case StudyHash = 'hash'; case StudyHash = 'hash';
case PatientName = 'patient_name'; case PatientName = 'patient_name';
case PatientId = 'patient_id'; case PatientId = 'patient_id';

View File

@ -14,7 +14,7 @@ public static function worklistColumns(User|int|null $usr = null): Collection
$user = me($usr); $user = me($usr);
$columns = collect([ $columns = collect([
WorklistColumn::Priority, WorklistColumn::Priority,
WorklistColumn::ReportStatus, WorklistColumn::WorkflowLevel,
WorklistColumn::ActionButtons, WorklistColumn::ActionButtons,
// WorklistColumn::AssignedPhysician, // WorklistColumn::AssignedPhysician,
WorklistColumn::PatientId, WorklistColumn::PatientId,

View File

@ -180,7 +180,7 @@ public function transformData(mixed $orthanc_src): array
$study['workflow_level'] = $stable_study $study['workflow_level'] = $stable_study
? WorkflowLevel::Unassigned->value ? WorkflowLevel::Unassigned->value
: WorkflowLevel::Pending->value; : WorkflowLevel::Received->value;
$study['patient_birthdate'] = null; $study['patient_birthdate'] = null;
$dob = data_get($orthanc_src, 'PatientMainDicomTags.PatientBirthDate'); $dob = data_get($orthanc_src, 'PatientMainDicomTags.PatientBirthDate');
if (filled($dob)) { if (filled($dob)) {

View File

@ -3,7 +3,7 @@
namespace App\Services\Report; namespace App\Services\Report;
use App\Domain\ACL\Permission; use App\Domain\ACL\Permission;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Models\Study; use App\Models\Study;
use App\Models\StudyReport; use App\Models\StudyReport;
use App\Services\AuditTrail\Activity; use App\Services\AuditTrail\Activity;
@ -41,13 +41,13 @@ public function getReports(): Collection
return $this->study->reports->sortByDesc('created_at'); return $this->study->reports->sortByDesc('created_at');
} }
public function createReport(string $content, ReportStatus $status): StudyReport public function createReport(string $content, WorkflowLevel $level): StudyReport
{ {
$report = StudyReport::make([ $report = StudyReport::make([
'study_id' => $this->study->id, 'study_id' => $this->study->id,
'organization_id' => $this->study->organization_id, 'organization_id' => $this->study->organization_id,
'department_id' => $this->study->department_id, 'department_id' => $this->study->department_id,
'report_status' => $status->value, 'workflow_level' => $level->value,
'read_by_id' => me()->id, 'read_by_id' => me()->id,
]); ]);
$report->saveContent($content); $report->saveContent($content);
@ -65,14 +65,14 @@ public function createReport(string $content, ReportStatus $status): StudyReport
public function finalizeReport(StudyReport $report): void public function finalizeReport(StudyReport $report): void
{ {
$report->setStatus(ReportStatus::Finalized); $report->setWorkflowLevel(WorkflowLevel::Finalized);
audit() audit()
->on($this->study) ->on($this->study)
->did(Activity::Report_Finalize) ->did(Activity::Report_Finalize)
->notes($report->accession_number) ->notes($report->accession_number)
->log(); ->log();
$this->study->setReportStatus(ReportStatus::Finalized); $this->study->setReportWorkflowLevel(WorkflowLevel::Finalized);
audit() audit()
->on($this->study) ->on($this->study)
@ -123,7 +123,7 @@ public function lockStudyIfRequired(): void
public function latestReport(): ?StudyReport public function latestReport(): ?StudyReport
{ {
return StudyReport::forStudy($this->study) return StudyReport::forStudy($this->study)
->where('report_status', ReportStatus::Preliminary->value) ->where('workflow_level', WorkflowLevel::DraftAvailable->value)
->select(['id', 'accession_number', 'file_path']) ->select(['id', 'accession_number', 'file_path'])
->latest() ->latest()
->first(); ->first();

View File

@ -3,7 +3,7 @@
namespace App\Services\Workflow; namespace App\Services\Workflow;
use App\Domain\ACL\Permission; use App\Domain\ACL\Permission;
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Models\Study; use App\Models\Study;
use App\Models\User; use App\Models\User;
use Closure; use Closure;
@ -42,28 +42,28 @@ public function can(Permission $permission): bool
public function canAssignPhysician(): bool public function canAssignPhysician(): bool
{ {
return $this->activeStudy(fn () => $this->study->report_status <= ReportStatus::Preliminary); return $this->activeStudy(fn () => $this->study->workflow_level <= WorkflowLevel::DraftAvailable);
} }
public function canStudyHistoryEdit(): bool public function canStudyHistoryEdit(): bool
{ {
return $this->activeStudy(fn () => $this->study->report_status <= ReportStatus::Preliminary); return $this->activeStudy(fn () => $this->study->workflow_level <= WorkflowLevel::DraftAvailable);
} }
public function canAttachmentUpload(): bool public function canAttachmentUpload(): bool
{ {
return $this->activeStudy(fn () => $this->study->report_status <= ReportStatus::Preliminary); return $this->activeStudy(fn () => $this->study->workflow_level <= WorkflowLevel::DraftAvailable);
} }
public function canReportDownload(): bool public function canReportDownload(): bool
{ {
return $this->activeStudy(fn () => $this->study->report_status >= ReportStatus::Finalized); return $this->activeStudy(fn () => $this->study->workflow_level >= WorkflowLevel::Finalized);
} }
public function canReportEdit(): bool public function canReportEdit(): bool
{ {
return $this->activeStudy(fn () => $this->study->isUnlocked() && return $this->activeStudy(fn () => $this->study->isUnlocked() &&
$this->study->report_status <= ReportStatus::Preliminary && $this->study->workflow_level <= WorkflowLevel::DraftAvailable &&
$this->study->isAssignedTo($this->user) $this->study->isAssignedTo($this->user)
); );
} }

View File

@ -13,7 +13,9 @@
| |
*/ */
'name' => env('APP_NAME', 'Laravel'), 'name' => env('APP_NAME', 'PixelBridge'),
'version' => env('APP_VERSION', '0.1.0'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -1,6 +1,5 @@
<?php <?php
use App\Domain\Report\ReportStatus;
use App\Domain\Study\Priority; use App\Domain\Study\Priority;
use App\Domain\Study\WorkflowLevel; use App\Domain\Study\WorkflowLevel;
use App\Models\Department; use App\Models\Department;
@ -21,8 +20,7 @@ public function up(): void
$table->foreignIdFor(Department::class)->nullable()->index(); $table->foreignIdFor(Department::class)->nullable()->index();
$table->unsignedTinyInteger('priority')->default(Priority::Routine->value); $table->unsignedTinyInteger('priority')->default(Priority::Routine->value);
$table->unsignedTinyInteger('workflow_level')->default(WorkflowLevel::Pending->value); $table->unsignedTinyInteger('workflow_level')->default(WorkflowLevel::Received->value);
$table->unsignedTinyInteger('report_status')->default(ReportStatus::Unread->value);
$table->boolean('requires_approval')->default(false); $table->boolean('requires_approval')->default(false);
$table->string('orthanc_uuid')->index(); $table->string('orthanc_uuid')->index();
@ -73,7 +71,7 @@ public function up(): void
$table->index(['department_id', 'assigned_at']); $table->index(['department_id', 'assigned_at']);
$table->index(['department_id', 'locked_at']); $table->index(['department_id', 'locked_at']);
$table->index(['requires_approval', 'report_status']); $table->index(['requires_approval', 'workflow_level']);
}); });
} }

View File

@ -1,6 +1,6 @@
<?php <?php
use App\Domain\Report\ReportStatus; use App\Domain\Study\WorkflowLevel;
use App\Models\Department; use App\Models\Department;
use App\Models\Organization; use App\Models\Organization;
use App\Models\Study; use App\Models\Study;
@ -15,7 +15,7 @@ public function up(): void
{ {
Schema::create('study_reports', function (Blueprint $table) { Schema::create('study_reports', function (Blueprint $table) {
$table->id(); $table->id();
$table->unsignedTinyInteger('report_status')->default(ReportStatus::Unread->value); $table->unsignedTinyInteger('workflow_level')->default(WorkflowLevel::Received->value);
$table->string('accession_number', 64)->unique()->index()->default(DB::raw("concat('REP-', gen_random_uuid())")); $table->string('accession_number', 64)->unique()->index()->default(DB::raw("concat('REP-', gen_random_uuid())"));
$table->foreignIdFor(Organization::class)->index()->nullable()->constrained()->nullOnDelete(); $table->foreignIdFor(Organization::class)->index()->nullable()->constrained()->nullOnDelete();

View File

@ -15,9 +15,9 @@ public function run(): void
// User::factory(10)->create(); // User::factory(10)->create();
$usr = User::factory()->create([ $usr = User::factory()->create([
'first_name' => 'PACS Sync', 'first_name' => 'PACS Agent',
'display_name' => 'PACS Sync Agent', 'display_name' => 'PACS Agent',
'username' => '$$_pacs_sync_$$', 'username' => '$$_pacs_agent_$$',
'password' => bcrypt(fake()->password(20)), 'password' => bcrypt(fake()->password(20)),
'is_active' => false, 'is_active' => false,
]); ]);
@ -30,6 +30,7 @@ public function run(): void
'email' => 'admin@example.com', 'email' => 'admin@example.com',
'email_verified_at' => now(), 'email_verified_at' => now(),
'phone' => '+8801733938582', 'phone' => '+8801733938582',
'is_active' => true,
]); ]);
$usr->assignRole(Role::Admin); $usr->assignRole(Role::Admin);

View File

@ -7,7 +7,7 @@
<div class="{{ $containerFooter }}"> <div class="{{ $containerFooter }}">
<div class="footer-container d-flex align-items-center justify-content-between py-4 flex-md-row flex-column"> <div class="footer-container d-flex align-items-center justify-content-between py-4 flex-md-row flex-column">
<div class="text-body mb-2 mb-md-0 fw-light"> <div class="text-body mb-2 mb-md-0 fw-light">
© 2024-<script>document.write(new Date().getFullYear())</script> {{ config('app.name') }}. All rights reserved. © 2024-<script>document.write(new Date().getFullYear())</script> {{ config('app.name') }} v{{ config('app.version') }}. All rights reserved.
</div> </div>
<div class="text-body d-none d-lg-inline-block"> <div class="text-body d-none d-lg-inline-block">
Made with <span class="text-danger"><i class="tf-icons ri-heart-fill"></i></span> by Made with <span class="text-danger"><i class="tf-icons ri-heart-fill"></i></span> by

View File

@ -52,7 +52,7 @@ class="ck-editor editor-container editor-container_classic-editor fixed-containe
<div class="form-check form-check-success custom-option custom-option-basic"> <div class="form-check form-check-success custom-option custom-option-basic">
<label class="form-check-label custom-option-content" for="radio_prelim"> <label class="form-check-label custom-option-content" for="radio_prelim">
<input name="report_status" class="form-check-input" type="radio" <input name="report_status" class="form-check-input" type="radio"
value="{{ \App\Domain\Report\ReportStatus::Preliminary->value }}" value="{{ \App\Domain\Study\WorkflowLevel::DraftAvailable->value }}"
id="radio_prelim" checked/> id="radio_prelim" checked/>
<span class="custom-option-body"> <span class="custom-option-body">
@ -68,7 +68,7 @@ class="ck-editor editor-container editor-container_classic-editor fixed-containe
<div class="form-check form-check-success custom-option custom-option-basic"> <div class="form-check form-check-success custom-option custom-option-basic">
<label class="form-check-label custom-option-content" for="radio_final"> <label class="form-check-label custom-option-content" for="radio_final">
<input name="report_status" class="form-check-input" type="radio" <input name="report_status" class="form-check-input" type="radio"
value="{{ \App\Domain\Report\ReportStatus::Finalized->value }}" value="{{ \App\Domain\Study\WorkflowLevel::Finalized->value }}"
id="radio_final"/> id="radio_final"/>
<span class="custom-option-body"> <span class="custom-option-body">

View File

@ -4,7 +4,7 @@
<td class="bg-gray-100">{{ $report->created_at }}</td> <td class="bg-gray-100">{{ $report->created_at }}</td>
<td> <td>
<a target="_blank" href="{{ $report->viewUrl() }}"> <a target="_blank" href="{{ $report->viewUrl() }}">
{{ $report->report_status->name }} {{ $report->workflow_level->name }}
<i class="fa-regular fa-square-arrow-up-right ms-1"></i> <i class="fa-regular fa-square-arrow-up-right ms-1"></i>
</a> </a>
</td> </td>