Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e3798b32b | |||
7f5d594a24 | |||
05b615fc75 | |||
6a1dd35da5 | |||
ef2dd84cfb | |||
8565d35fe9 | |||
7e1d141338 | |||
fcfe1278f7 | |||
3ad9e8633e | |||
10c96a54c4 | |||
0da8e2bc34 | |||
049d59d39c | |||
1f7764481c | |||
4462bb1cfb | |||
ef908dd4bd | |||
19a501c2e2 | |||
49b4341227 | |||
a7228b8a61 | |||
fac10cdc25 | |||
d4c6e58e30 | |||
9e969df68b | |||
a02adfc685 | |||
ce28bb1422 | |||
0fce30bd18 | |||
7a28b892f3 | |||
60f32a6468 | |||
4e2c1837ee | |||
ea341ca8f4 | |||
9f60cfb32e | |||
97481e8473 | |||
802f13c702 | |||
3a775c4f21 | |||
927c51e16d | |||
45d692968e | |||
72b1bbc0e7 | |||
27c3cd5d8d | |||
7f5e916d80 | |||
8853528b0a | |||
3d5cceb125 | |||
db97aef03c | |||
ba705bb98b | |||
9e2315b89b | |||
bdf1ad1ca3 | |||
5292865bef | |||
d1d6f0d7af | |||
3f2a38faa6 | |||
ef170b5357 | |||
b32ebecfaa | |||
15d8ebf3c7 | |||
6bffd31f2f | |||
e9ada6f2b3 | |||
a0834c511e | |||
93842ba548 | |||
dc2d48cdb0 | |||
a2304d0245 | |||
9ceb1c3a34 | |||
da16e247c4 | |||
d343d105fc | |||
0bed01ff68 | |||
db4a6901a4 | |||
3ea8245f85 | |||
96d7d4048e | |||
ed6e462f2c | |||
235bc0b405 | |||
2a2370b3a5 | |||
70b82cd410 | |||
103df6436d | |||
90dce16aea | |||
7455709731 | |||
658adbb6be | |||
e20f7aa703 | |||
d08cdd6997 | |||
3b9f677804 | |||
9c6dddb7a0 | |||
44acb63d3a | |||
![]() |
4a79935e4c | ||
1b5faca946 | |||
d6432ac054 | |||
cd5a95e53c | |||
fcb0dc17fd | |||
7e150c972b | |||
57b24f5876 | |||
8648d1ae1e | |||
9d13f0570c | |||
b2726911e7 | |||
ce9669b556 | |||
21b24fe542 | |||
![]() |
1e0c62ce92 |
@ -1,4 +1,5 @@
|
||||
APP_NAME=Laravel
|
||||
APP_NAME="PixelBridge"
|
||||
APP_VERSION="25.01-0.1.0"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
|
@ -1,8 +1,8 @@
|
||||
# RadFusion PACS
|
||||
# PixelBridge RIS
|
||||
|
||||
## Introduction
|
||||
|
||||
RadFusion PACS is a comprehensive Picture Archiving and Communication System (PACS) designed to streamline the
|
||||
PixelBridge RIS is a comprehensive RIS (Radiology Information System) designed to streamline the
|
||||
management and viewing of medical imaging studies.
|
||||
|
||||
## Requirements
|
||||
@ -17,8 +17,8 @@ ## Installation
|
||||
1. **Clone the repository:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/masroore/radfusion-pacs.git
|
||||
cd radfusion-pacs
|
||||
git clone https://github.com/masroore/pixelbridge-pacs.git
|
||||
cd pixelbridge-pacs
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
|
@ -2,12 +2,13 @@
|
||||
|
||||
namespace App\DAL;
|
||||
|
||||
use App\Domain\ACL\Role;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
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'
|
||||
SELECT
|
||||
@ -28,12 +29,12 @@ public static function worklist_stats(int $days, int $report_status)
|
||||
WHERE
|
||||
--st.received_at :: DATE >= NOW() - INTERVAL '3 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
|
||||
GROUP BY
|
||||
sa.user_id) AS cte ON cte.user_id = users."id"
|
||||
WHERE
|
||||
roles."name" = 'radiologist'
|
||||
roles."name" = '%s'
|
||||
AND users.is_active = TRUE
|
||||
GROUP BY
|
||||
users."id",
|
||||
@ -42,11 +43,26 @@ public static function worklist_stats(int $days, int $report_status)
|
||||
users.display_name
|
||||
SQL;
|
||||
|
||||
$rows = DB::select(sprintf($sql, $days, $report_status));
|
||||
$rows = DB::select(sprintf($sql, $days, $workflow_level, Role::Radiologist->value));
|
||||
foreach ($rows as $row) {
|
||||
$row->last_seen = UserService::getLastSeen((int) $row->id);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function activeRads(): array
|
||||
{
|
||||
return cache()
|
||||
->remember('active_rads',
|
||||
now()->addMinutes(5),
|
||||
fn () => DB::table('users')
|
||||
->join('model_has_roles', 'users.id', '=', 'model_has_roles.model_id')
|
||||
->join('roles', 'model_has_roles.role_id', '=', 'roles.id')
|
||||
->where('roles.name', Role::Radiologist->value)
|
||||
->where('users.is_active', true)
|
||||
->orderBy('users.display_name')
|
||||
->pluck('users.display_name', 'users.id')
|
||||
->toArray());
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\DAL\Studies;
|
||||
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
@ -11,9 +10,7 @@ interface IUserStudyLister
|
||||
{
|
||||
public function setRadiologist(int $radiologist_id): self;
|
||||
|
||||
public function setWorkflowLevel(WorkflowLevel $status): self;
|
||||
|
||||
public function setReportStatus(ReportStatus $status): self;
|
||||
public function setWorkflowLevel(WorkflowLevel $level): self;
|
||||
|
||||
public function setPerPage(int $size): self;
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\DAL\Studies;
|
||||
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Study;
|
||||
use Carbon\Carbon;
|
||||
@ -21,8 +20,6 @@ abstract class WorklistBase implements IUserStudyLister
|
||||
|
||||
private ?WorkflowLevel $workflowLevel = null;
|
||||
|
||||
private ?ReportStatus $reportStatus = null;
|
||||
|
||||
private ?bool $locked = null;
|
||||
|
||||
private ?bool $archived = null;
|
||||
@ -41,7 +38,7 @@ abstract class WorklistBase implements IUserStudyLister
|
||||
|
||||
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
|
||||
@ -51,16 +48,9 @@ public function setRadiologist(int $radiologist_id): self
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setWorkflowLevel(WorkflowLevel $status): self
|
||||
public function setWorkflowLevel(WorkflowLevel $level): self
|
||||
{
|
||||
$this->workflowLevel = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setReportStatus(ReportStatus $status): self
|
||||
{
|
||||
$this->reportStatus = $status;
|
||||
$this->workflowLevel = $level;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -81,7 +71,6 @@ public function get(?int $user_id = null): LengthAwarePaginator
|
||||
$query = $this->applySearch($query);
|
||||
$query = $this->applyRadiologist($query);
|
||||
$query = $this->applyWorkflowLevel($query);
|
||||
$query = $this->applyReportStatus($query);
|
||||
$query = $this->applyArchived($query);
|
||||
$query = $this->applyLocked($query);
|
||||
$query = $this->applyDateFilters($query);
|
||||
@ -236,15 +225,6 @@ private function applyWorkflowLevel(Builder $query): Builder
|
||||
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
|
||||
{
|
||||
return $this->perPage ?? user_per_page($user_id);
|
||||
|
@ -3,11 +3,12 @@
|
||||
namespace App\DataTables;
|
||||
|
||||
use App\DAL\Studies\WorklistFactory;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Study;
|
||||
use App\Services\ACL\WorklistButton;
|
||||
use App\Services\ACL\WorklistColumn;
|
||||
use App\Services\ACL\WorklistGuard;
|
||||
use App\Services\GeoLocation;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Closure;
|
||||
@ -46,7 +47,7 @@ public function dataTable(QueryBuilder $query): EloquentDataTable
|
||||
$dataTable = new EloquentDataTable($query);
|
||||
$rawColumns = [
|
||||
'priority_icon',
|
||||
'report_status_led',
|
||||
'workflow_level_led',
|
||||
];
|
||||
foreach ($this->renderCustomColumns() as $column => $content) {
|
||||
$dataTable->addColumn($column, $content);
|
||||
@ -147,11 +148,11 @@ public function getColumns(): array
|
||||
->title('');
|
||||
break;
|
||||
|
||||
case WorklistColumn::ReportStatus:
|
||||
case WorklistColumn::WorkflowLevel:
|
||||
$columns[] = Column::make($col->value)
|
||||
->searchable(false)
|
||||
->hidden();
|
||||
$columns[] = Column::make('report_status_led')
|
||||
$columns[] = Column::make('workflow_level_led')
|
||||
->searchable(false)
|
||||
->orderable(false)
|
||||
->addClass('text-center p-0 ps-2')
|
||||
@ -329,10 +330,10 @@ private function filterStatus(QueryBuilder $query, ?string $status): void
|
||||
|
||||
switch ($status) {
|
||||
case 'unread':
|
||||
$query->where('report_status', '<', ReportStatus::Finalized->value);
|
||||
$query->where('workflow_level', '<', WorkflowLevel::Finalized->value);
|
||||
break;
|
||||
case 'read':
|
||||
$query->where('report_status', '>=', ReportStatus::Finalized->value);
|
||||
$query->where('workflow_level', '>=', WorkflowLevel::Finalized->value);
|
||||
break;
|
||||
case 'progress':
|
||||
$query->whereNotNull('locked_at');
|
||||
@ -514,8 +515,12 @@ private function renderCustomColumns(): array
|
||||
break;
|
||||
case WorklistColumn::DicomServer:
|
||||
$columns[$col->value] = function (Study $study) {
|
||||
return sprintf('<span class="fi fi-%s msg-icon me-1"></span>%s',
|
||||
$country = sprintf('%s (%s)', GeoLocation::name($study->dicomServer->geo_code), $study->dicomServer->geo_code);
|
||||
|
||||
return sprintf('<span class="fi fi-%s msg-icon me-1" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="%s" title="%s"></span>%s',
|
||||
strtolower($study->dicomServer->geo_code),
|
||||
$country,
|
||||
$country,
|
||||
$study->dicomServer->server_name);
|
||||
};
|
||||
break;
|
||||
@ -622,7 +627,7 @@ private function generateActionButtons(Study $study): string
|
||||
foreach (WorklistGuard::worklistButtons($study) as $button) {
|
||||
switch ($button) {
|
||||
case WorklistButton::StudyMetadata:
|
||||
$btns[] = $this->renderImageLink($study->hash, 'info.png', 'show-study', 'Info');
|
||||
$btns[] = $this->renderImageLink($study->hash, $study->hasHistory() ? 'info-green.png' : 'info.png', 'show-study', 'Info');
|
||||
break;
|
||||
case WorklistButton::Assign:
|
||||
$btns[] = $this->renderImageLink($study->hash, 'assign.png', 'show-assign', 'Assign');
|
||||
|
@ -6,13 +6,14 @@ enum Role: string
|
||||
{
|
||||
case Guest = 'guest';
|
||||
case Patient = 'patient';
|
||||
case Clinician = 'clinician';
|
||||
case Technician = 'technician';
|
||||
case Radiologist = 'radiologist';
|
||||
case Associate = 'associate';
|
||||
case SystemAgent = 'system_agent';
|
||||
case SiteAdmin = 'site_admin';
|
||||
case Clinician = 'clinic';
|
||||
case Technician = 'tech';
|
||||
case Radiologist = 'rad';
|
||||
case Reviewer = 'review';
|
||||
case Associate = 'assoc';
|
||||
case SystemAgent = 'sys';
|
||||
case SiteAdmin = 'site-admin';
|
||||
case Clerk = 'clerk';
|
||||
case Transcriptionist = 'transcriptionist';
|
||||
case Transcriptionist = 'trans';
|
||||
case Admin = 'admin';
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -4,9 +4,36 @@
|
||||
|
||||
enum WorkflowLevel: int
|
||||
{
|
||||
case Pending = 0;
|
||||
case Unassigned = 10;
|
||||
case Assigned = 20;
|
||||
case ReadInProgress = 30;
|
||||
case ReadCompleted = 40;
|
||||
case Received = 10;
|
||||
case Unassigned = 20;
|
||||
case Assigned = 30;
|
||||
case Dictated = 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 describe(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Received => 'Study Received',
|
||||
self::Unassigned => 'Unassigned study',
|
||||
self::Assigned => 'Pending read',
|
||||
self::Dictated => 'Dictated',
|
||||
self::Transcribed => 'Transcribed',
|
||||
self::Repetition => 'Repeat interpretation',
|
||||
self::ReadInProgress => 'Read In Progress',
|
||||
self::DraftAvailable => 'Draft available',
|
||||
self::Finalized => 'Report finalized',
|
||||
self::UnderReview => 'Under Review',
|
||||
self::Published => 'Report published',
|
||||
self::Archived => 'Study archived',
|
||||
self::Cancelled => 'Cancelled',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
93
app/Filament/Resources/DepartmentResource.php
Normal file
93
app/Filament/Resources/DepartmentResource.php
Normal 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 Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class DepartmentResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Department::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-building-storefront';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('guid')
|
||||
->label('Unique ID')
|
||||
->default(sprintf('DEP-%s', Str::of(Uuid::uuid4())->lower()))
|
||||
->disabled()
|
||||
->dehydrated()
|
||||
->required()
|
||||
->maxLength(40)
|
||||
->unique(ignoreRecord: true),
|
||||
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([
|
||||
IconColumn::make('is_active')->boolean(),
|
||||
TextColumn::make('name')->searchable(),
|
||||
TextColumn::make('organization.name')->sortable(),
|
||||
TextColumn::make('guid')->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'),
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
126
app/Filament/Resources/DicomRoutingRuleResource.php
Normal file
126
app/Filament/Resources/DicomRoutingRuleResource.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\DAL\Radiologists;
|
||||
use App\Filament\Resources\DicomRoutingRuleResource\Pages;
|
||||
use App\Models\DicomRoutingRule;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
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;
|
||||
|
||||
class DicomRoutingRuleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DicomRoutingRule::class;
|
||||
protected static ?string $modelLabel = 'Routing Rules';
|
||||
protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Toggle::make('is_active')
|
||||
->required(),
|
||||
Select::make('organization_id')
|
||||
->label('Organization')
|
||||
->relationship('organization', 'name')
|
||||
->required(),
|
||||
Select::make('department_id')
|
||||
->label('Department')
|
||||
->relationship('department', 'name'),
|
||||
Select::make('user_id')
|
||||
->label('Radiologist')
|
||||
->relationship('radiologist', 'display_name')
|
||||
->options(Radiologists::activeRads()),
|
||||
Select::make('assignment_panel_id')
|
||||
->label('Panel')
|
||||
->relationship('panel', 'name'),
|
||||
TextInput::make('name')
|
||||
->maxLength(255),
|
||||
Textarea::make('condition')
|
||||
->columnSpanFull()
|
||||
->required(),
|
||||
TextInput::make('priority')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(0),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
IconColumn::make('is_active')
|
||||
->label('')
|
||||
->boolean(),
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('condition')
|
||||
->label('Rule')
|
||||
->limit(20)
|
||||
->searchable(),
|
||||
TextColumn::make('priority')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('organization.name')
|
||||
->label('Org')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('department.name')
|
||||
->label('Dept')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('user_id')
|
||||
->label('Rad')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('assignment_panel_id')
|
||||
->label('Panel')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
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\ListDicomRoutingRules::route('/'),
|
||||
'create' => Pages\CreateDicomRoutingRule::route('/create'),
|
||||
'edit' => Pages\EditDicomRoutingRule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomRoutingRuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomRoutingRuleResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateDicomRoutingRule extends CreateRecord
|
||||
{
|
||||
protected static string $resource = DicomRoutingRuleResource::class;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomRoutingRuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomRoutingRuleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditDicomRoutingRule extends EditRecord
|
||||
{
|
||||
protected static string $resource = DicomRoutingRuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomRoutingRuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomRoutingRuleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDicomRoutingRules extends ListRecords
|
||||
{
|
||||
protected static string $resource = DicomRoutingRuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
150
app/Filament/Resources/DicomServerResource.php
Normal file
150
app/Filament/Resources/DicomServerResource.php
Normal file
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\DicomServerResource\Pages;
|
||||
use App\Models\Department;
|
||||
use App\Models\DicomServer;
|
||||
use App\Services\GeoLocation;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class DicomServerResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DicomServer::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-server-stack';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Toggle::make('is_active')
|
||||
->label('Active?')
|
||||
->required(),
|
||||
TextInput::make('server_name')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
Select::make('geo_code')
|
||||
->required()
|
||||
->label('GEO Location')
|
||||
->options(GeoLocation::select()),
|
||||
TextInput::make('host')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('http_port')
|
||||
->required()
|
||||
->numeric(),
|
||||
TextInput::make('dicom_port')
|
||||
->required()
|
||||
->numeric(),
|
||||
TextInput::make('rest_api_endpoint')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('ae_title')
|
||||
->maxLength(24),
|
||||
TextInput::make('username')
|
||||
->maxLength(40),
|
||||
TextInput::make('password')
|
||||
->maxLength(40),
|
||||
TextInput::make('wado_path')
|
||||
->maxLength(255),
|
||||
TextInput::make('stone_viewer_path')
|
||||
->maxLength(255),
|
||||
TextInput::make('ohif_viewer_path')
|
||||
->maxLength(255),
|
||||
TextInput::make('meddream_viewer_path')
|
||||
->maxLength(255),
|
||||
Select::make('organization_id')
|
||||
->label('Organization')
|
||||
->live()
|
||||
->relationship('organization', 'name'),
|
||||
Select::make('department_id')
|
||||
->label('Department')
|
||||
->options(function (Get $get) {
|
||||
$organization_id = $get('organization_id');
|
||||
$result = [];
|
||||
if ($organization_id != null) {
|
||||
$result = Department::active()->organization($organization_id)->pluck('name', 'id')->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
IconColumn::make('is_active')
|
||||
->label('')
|
||||
->boolean(),
|
||||
TextColumn::make('server_name')
|
||||
->label('Name')
|
||||
->searchable(),
|
||||
TextColumn::make('geo_code')
|
||||
->label('GEO')
|
||||
->searchable(),
|
||||
TextColumn::make('host')
|
||||
->searchable(),
|
||||
TextColumn::make('http_port')
|
||||
->label('WADO'),
|
||||
TextColumn::make('dicom_port')
|
||||
->label('DICOM'),
|
||||
TextColumn::make('ae_title')
|
||||
->label('AET')
|
||||
->searchable(),
|
||||
TextColumn::make('rest_api_endpoint')
|
||||
->label('REST')
|
||||
->url(fn (DicomServer $srv) => $srv->rest_api_endpoint, shouldOpenInNewTab: true)
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
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\ListDicomServers::route('/'),
|
||||
'create' => Pages\CreateDicomServer::route('/create'),
|
||||
'view' => Pages\ViewDicomServer::route('/{record}'),
|
||||
'edit' => Pages\EditDicomServer::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomServerResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateDicomServer extends CreateRecord
|
||||
{
|
||||
protected static string $resource = DicomServerResource::class;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomServerResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditDicomServer extends EditRecord
|
||||
{
|
||||
protected static string $resource = DicomServerResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomServerResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDicomServers extends ListRecords
|
||||
{
|
||||
protected static string $resource = DicomServerResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DicomServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DicomServerResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewDicomServer extends ViewRecord
|
||||
{
|
||||
protected static string $resource = DicomServerResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
96
app/Filament/Resources/OrganizationResource.php
Normal file
96
app/Filament/Resources/OrganizationResource.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\OrganizationResource\Pages;
|
||||
use App\Models\Organization;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
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 Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class OrganizationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Organization::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-building-office-2';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('guid')
|
||||
->label('Unique ID')
|
||||
->default(sprintf('ORG-%s', Str::of(Uuid::uuid4())->lower()))
|
||||
->disabled()
|
||||
->dehydrated()
|
||||
->required()
|
||||
->maxLength(40)
|
||||
->unique(ignoreRecord: true),
|
||||
Toggle::make('is_active')
|
||||
->required(),
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
TextArea::make('address')
|
||||
->maxLength(255),
|
||||
TextInput::make('logo_path')
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
IconColumn::make('is_active')->boolean(),
|
||||
TextColumn::make('name')->searchable(),
|
||||
TextColumn::make('guid')->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'),
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
195
app/Filament/Resources/UserResource.php
Normal file
195
app/Filament/Resources/UserResource.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Models\Department;
|
||||
use App\Models\User;
|
||||
use App\Services\ACL\RoleService;
|
||||
use App\Services\TimezoneList;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-users';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$isCreate = $form->getOperation() === 'create';
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('guid')
|
||||
->label('Unique ID')
|
||||
->default(sprintf('USR-%s', Str::of(Uuid::uuid4())->lower()))
|
||||
->disabled()
|
||||
->dehydrated()
|
||||
->required()
|
||||
->maxLength(40)
|
||||
->unique(ignoreRecord: true),
|
||||
Toggle::make('is_active')
|
||||
->required(),
|
||||
TextInput::make('prefix')
|
||||
->maxLength(80),
|
||||
TextInput::make('first_name')
|
||||
->required()
|
||||
->maxLength(120),
|
||||
TextInput::make('last_name')
|
||||
->maxLength(120),
|
||||
TextInput::make('display_name')
|
||||
->required()
|
||||
->maxLength(160),
|
||||
TextInput::make('username')
|
||||
->unique(ignoreRecord: true)
|
||||
->required()
|
||||
->maxLength(24),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->revealable()
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->maxLength(32),
|
||||
TextInput::make('phone')
|
||||
->unique(ignoreRecord: true)
|
||||
->tel()
|
||||
->telRegex('/^\+?[1-9]\d{8,14}$/')
|
||||
->maxLength(80),
|
||||
TextInput::make('email')
|
||||
->unique(ignoreRecord: true)
|
||||
->email()
|
||||
->maxLength(80),
|
||||
DateTimePicker::make('email_verified_at'),
|
||||
TextInput::make('profile_photo_path')
|
||||
->maxLength(255),
|
||||
FileUpload::make('signature_image_path')
|
||||
->disk('public')
|
||||
->visibility('public')
|
||||
->directory('signatures')
|
||||
->preserveFilenames()
|
||||
->getUploadedFileNameForStorageUsing(
|
||||
function (TemporaryUploadedFile $file) {
|
||||
return Str::of(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME))
|
||||
->slug()
|
||||
->prepend(now()->timestamp . '_')
|
||||
->append('.' . $file->getClientOriginalExtension());
|
||||
}
|
||||
)
|
||||
->image(),
|
||||
Textarea::make('signature_text')
|
||||
->columnSpanFull(),
|
||||
Select::make('organization_id')
|
||||
->label('Organization')
|
||||
->live()
|
||||
->relationship('organization', 'name'),
|
||||
Select::make('department_id')
|
||||
->label('Department')
|
||||
->options(function (Get $get) {
|
||||
$organization_id = $get('organization_id');
|
||||
$result = [];
|
||||
if ($organization_id != null) {
|
||||
$result = Department::active()->organization($organization_id)->pluck('name', 'id')->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}),
|
||||
Select::make('roles')
|
||||
->relationship('roles', 'name')
|
||||
->multiple()
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->searchable()
|
||||
// ->options(RoleService::select())
|
||||
->afterStateUpdated(function ($state, User $user) {
|
||||
$user->assignRole($state);
|
||||
}),
|
||||
Select::make('timezone')
|
||||
->options(
|
||||
(new TimezoneList)->splitGroup(true)->toArray(false)
|
||||
)
|
||||
->required()
|
||||
->default('Asia/Dhaka'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->label('Active?')
|
||||
->boolean(),
|
||||
TextColumn::make('display_name')
|
||||
->label('Name')
|
||||
->searchable(),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->searchable(),
|
||||
// ImageColumn::make('signature_image_path'),
|
||||
TextColumn::make('organization.name')
|
||||
->label('Org')
|
||||
->badge()
|
||||
->sortable(),
|
||||
TextColumn::make('department.name')
|
||||
->label('Dept')
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('guid')
|
||||
->limit(16)
|
||||
->searchable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'create' => Pages\CreateUser::route('/create'),
|
||||
'view' => Pages\ViewUser::route('/{record}'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
11
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
11
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateUser extends CreateRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
}
|
20
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
20
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
19
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
19
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
19
app/Filament/Resources/UserResource/Pages/ViewUser.php
Normal file
19
app/Filament/Resources/UserResource/Pages/ViewUser.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewUser extends ViewRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
@ -5,12 +5,11 @@
|
||||
use App\DAL\Radiologists;
|
||||
use App\Domain\ACL\Permission;
|
||||
use App\Domain\ACL\Role;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Http\Controllers\HashedStudyControllerBase;
|
||||
use App\Http\Requests\AssignPhysicianRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\AuditTrail\Activity;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AssignmentController extends HashedStudyControllerBase
|
||||
{
|
||||
@ -18,13 +17,17 @@ public function show()
|
||||
{
|
||||
abort_unless(me()->may(Permission::AssignRadiologist), 403);
|
||||
$study = $this->getStudy('assignedPhysicians');
|
||||
$rads = User::active()->role(Role::Radiologist)->get(['id', 'display_name', 'profile_photo_path', 'first_name', 'last_name', 'created_at']);
|
||||
$stats = Radiologists::worklist_stats(3, ReportStatus::Finalized->value);
|
||||
$rads = User::active()
|
||||
->role(Role::Radiologist)
|
||||
->get(['id', 'display_name', 'profile_photo_path', 'first_name', 'last_name', 'created_at'])
|
||||
->each(fn ($rad) => $rad->info = ['workload' => '', 'last_seen' => '']);
|
||||
|
||||
$stats = Radiologists::worklist_stats(3, WorkflowLevel::Finalized->value);
|
||||
foreach ($stats as $rad) {
|
||||
$found = $rads->where('id', $rad->id)->first();
|
||||
if ($found) {
|
||||
$found->info['workload'] = $rad->workload;
|
||||
$found->info['last_seen'] = ($rad->last_seen ?? Carbon::now()->addHours(-random_int(1, 36)))->diffForHumans();
|
||||
$found->info['last_seen'] = $rad->last_seen?->diffForHumans() ?? '-';
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,16 +38,21 @@ public function remove(AssignPhysicianRequest $request)
|
||||
{
|
||||
abort_unless(me()->may(Permission::AssignRadiologist), 403);
|
||||
$study = $this->getStudy('assignedPhysicians');
|
||||
$user = User::active()->findOrFail($request->input('rad_id'));
|
||||
$rad = User::active()->findOrFail($request->input('rad_id'));
|
||||
|
||||
if ($study->isAssigned($user)) {
|
||||
$study->assignedPhysicians()->detach($user->id);
|
||||
if ($study->assignedPhysicians->count() === 0) {
|
||||
$study->update(['assigned_at' => null]);
|
||||
if ($study->isAssigned($rad)) {
|
||||
$study->assignedPhysicians()->detach($rad->id);
|
||||
|
||||
if ($study->assignedPhysicians()->count() === 0) {
|
||||
$study->update([
|
||||
'workflow_level' => WorkflowLevel::Unassigned->value,
|
||||
'assigned_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
audit()
|
||||
->did(Activity::Unassign_Physician)
|
||||
->notes($user->display_name)
|
||||
->notes($rad->display_name)
|
||||
->on($study)
|
||||
->log();
|
||||
}
|
||||
@ -59,7 +67,10 @@ public function save(AssignPhysicianRequest $request)
|
||||
$rad = User::active()->findOrFail($request->input('rad_id'));
|
||||
|
||||
$study->assignedPhysicians()->attach($rad->id);
|
||||
$study->update(['assigned_at' => now()]);
|
||||
$study->update([
|
||||
'workflow_level' => WorkflowLevel::Assigned->value,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
audit()
|
||||
->did(Activity::Assign_Physician)
|
||||
->on($study)
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Staff;
|
||||
|
||||
use App\Http\Controllers\HashidControllerBase;
|
||||
use App\Http\Controllers\HashedStudyControllerBase;
|
||||
use App\Models\Study;
|
||||
use App\Services\AuditTrail\Activity;
|
||||
use Closure;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DicomViewerController extends HashidControllerBase
|
||||
class DicomViewerController extends HashedStudyControllerBase
|
||||
{
|
||||
public function stone()
|
||||
{
|
||||
@ -22,8 +22,7 @@ public function ohif()
|
||||
|
||||
private function loadViewer(Closure $callback)
|
||||
{
|
||||
$this->decodeKeys();
|
||||
$study = Study::findOrFail($this->key);
|
||||
$study = $this->getStudy();
|
||||
$url = $callback($study);
|
||||
abort_if(blank($url), 404);
|
||||
$title = Str::limit($study->getPatientDemographic(), 40);
|
||||
|
@ -9,12 +9,13 @@
|
||||
use App\Models\StudyDetails;
|
||||
use App\Services\AuditTrail\Activity;
|
||||
use App\Services\SessionHelper;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class HistoryController extends HashidControllerBase
|
||||
{
|
||||
public function view()
|
||||
{
|
||||
abort_unless(auth()->user()->may(Permission::StudyHistoryView), 403);
|
||||
abort_unless(may(Permission::StudyHistoryView), 403);
|
||||
$this->decodeKeys();
|
||||
$details = StudyDetails::historyOnly($this->key);
|
||||
$study = Study::findOrFail($this->key);
|
||||
@ -25,7 +26,7 @@ public function view()
|
||||
public function edit()
|
||||
{
|
||||
// SessionHelper::setIntendedUrl();
|
||||
abort_unless(auth()->user()->may(Permission::StudyHistoryEdit), 403);
|
||||
abort_unless(may(Permission::StudyHistoryEdit), 403);
|
||||
$this->decodeKeys();
|
||||
$details = StudyDetails::historyOnly($this->key);
|
||||
$study = Study::findOrFail($this->key);
|
||||
@ -35,12 +36,13 @@ public function edit()
|
||||
|
||||
public function save(StudyHistoryRequest $request)
|
||||
{
|
||||
abort_unless(auth()->user()->may(Permission::StudyHistoryEdit), 403);
|
||||
abort_unless(may(Permission::StudyHistoryEdit), 403);
|
||||
$this->decodeKeys();
|
||||
$details = StudyDetails::historyOnly($this->key);
|
||||
$payload = array_trim_strings($request->validated());
|
||||
unset($payload['study_id']);
|
||||
$details->update($payload);
|
||||
Cache::forget("study.has_history.{$this->key}");
|
||||
|
||||
audit()
|
||||
->did(Activity::Study_History_Edit)
|
||||
|
@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Staff;
|
||||
|
||||
use App\Domain\ACL\Permission;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Http\Controllers\HashedStudyControllerBase;
|
||||
use App\Http\Requests\StudyMetadataUpdateRequest;
|
||||
use App\Models\Study;
|
||||
@ -32,7 +32,7 @@ public function edit()
|
||||
|
||||
public function save(StudyMetadataUpdateRequest $request)
|
||||
{
|
||||
abort_unless(auth()->user()->may(Permission::StudyMetadataEdit), 403);
|
||||
abort_unless(may(Permission::StudyMetadataEdit), 403);
|
||||
$study = $this->getStudy();
|
||||
if ($study->isReportReady()) {
|
||||
return $this->lockedNotice();
|
||||
@ -43,7 +43,7 @@ public function save(StudyMetadataUpdateRequest $request)
|
||||
$payload['patient_sex'] = strtoupper($payload['patient_sex']);
|
||||
if ($request->has('cancel_read')) {
|
||||
// lock the study if report is not needed
|
||||
$payload['report_status'] = ReportStatus::Cancelled->value;
|
||||
$payload['workflow_level'] = WorkflowLevel::Cancelled->value;
|
||||
$payload['locked_at'] = now();
|
||||
unset($payload['cancel_read']);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Staff;
|
||||
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Http\Controllers\HashidControllerBase;
|
||||
use App\Http\Requests\StoreReportRequest;
|
||||
use App\Models\Study;
|
||||
@ -39,10 +39,10 @@ public function save(StoreReportRequest $request)
|
||||
ReportManager::ensureEditAccess();
|
||||
$this->decodeKeys();
|
||||
$manager = ReportManager::make($this->key);
|
||||
$reportStatus = ReportStatus::from($request->integer('report_status'));
|
||||
$report = $manager->createReport(request('content'), $reportStatus);
|
||||
$workflow_level = WorkflowLevel::from($request->integer('report_status'));
|
||||
$report = $manager->createReport(request('content'), $workflow_level);
|
||||
|
||||
if ($reportStatus->value === ReportStatus::Finalized->value) {
|
||||
if ($workflow_level->value === WorkflowLevel::Finalized->value) {
|
||||
$manager->finalizeReport($report);
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,8 @@ class SyncOrthancController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
foreach (DicomServer::active()->get() as $host) {
|
||||
(new StudiesSync($host))->execute();
|
||||
foreach (DicomServer::active()->get() as $pacs) {
|
||||
(new StudiesSync($pacs))->execute();
|
||||
}
|
||||
|
||||
return redirect()->route('staff.worklist.index');
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Study;
|
||||
use App\Rules\ExistsByHash;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@ -22,7 +22,7 @@ public function rules(): array
|
||||
'content' => 'required',
|
||||
'report_status' => [
|
||||
'required',
|
||||
Rule::enum(ReportStatus::class)->only([ReportStatus::Preliminary, ReportStatus::Finalized, ReportStatus::Approved]),
|
||||
Rule::enum(WorkflowLevel::class)->only([WorkflowLevel::DraftAvailable, WorkflowLevel::Finalized, WorkflowLevel::Published]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ public function rules(): array
|
||||
'referring_physician_name' => ['nullable'],
|
||||
'institution_name' => ['nullable'],
|
||||
'priority' => ['required', 'integer'],
|
||||
'cancel_read' => ['nullable', 'boolean'],
|
||||
'cancel_read' => ['accepted'],
|
||||
|
||||
/*
|
||||
'referring_physician_id' => ['nullable', 'exists:users,id'],
|
||||
|
@ -3,17 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Traits\Active;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\Traits\HasOrganization;
|
||||
|
||||
class Department extends Model
|
||||
class Department extends BaseModel
|
||||
{
|
||||
use Active;
|
||||
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
use HasOrganization;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
|
@ -2,21 +2,13 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\Rule\MatchCondition;
|
||||
use App\Models\Traits\Active;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class DicomRoutingRule extends BaseModel
|
||||
{
|
||||
use Active;
|
||||
|
||||
public function conditions(): HasMany
|
||||
{
|
||||
return $this->hasMany(DicomRuleCondition::class);
|
||||
}
|
||||
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
@ -27,21 +19,20 @@ public function department(): BelongsTo
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function panel(): HasOne
|
||||
public function panel(): BelongsTo
|
||||
{
|
||||
return $this->hasOne(AssignmentPanel::class);
|
||||
return $this->belongsTo(AssignmentPanel::class, 'assignment_panel_id');
|
||||
}
|
||||
|
||||
public function radiologist(): HasOne
|
||||
public function radiologist(): BelongsTo
|
||||
{
|
||||
return $this->hasOne(User::class);
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'match_condition' => MatchCondition::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\Rule\MatchMode;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DicomRuleCondition extends BaseModel
|
||||
{
|
||||
public function rule(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DicomRoutingRule::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'case_sensitive' => 'boolean',
|
||||
'match_mode' => MatchMode::class,
|
||||
];
|
||||
}
|
||||
}
|
@ -10,12 +10,12 @@ class DicomServer extends BaseModel
|
||||
{
|
||||
use Active;
|
||||
|
||||
public function institute(): BelongsTo
|
||||
public function organization(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function facility(): BelongsTo
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\ACL\Permission;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\Priority;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Traits\HasDepartment;
|
||||
@ -12,12 +11,13 @@
|
||||
use App\Services\Pacs\PacsUrlGen;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
@ -28,7 +28,6 @@ class Study extends BaseModel implements HasMedia
|
||||
use HasDepartment;
|
||||
use HashableId;
|
||||
use HasOrganization;
|
||||
use HasTimestamps;
|
||||
use InteractsWithMedia;
|
||||
|
||||
public const string MEDIA_COLLECTION = 'attachments';
|
||||
@ -175,26 +174,28 @@ public function isArchived(): bool
|
||||
return $this->archived_at !== null;
|
||||
}
|
||||
|
||||
public function getReportStatusLedAttribute(): string
|
||||
public function getWorkflowLevelLedAttribute(): 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',
|
||||
};
|
||||
// <i class="fa-solid fa-spinner"></i>
|
||||
$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',
|
||||
$icon = match ($this->workflow_level) {
|
||||
WorkflowLevel::Received => 'download-2-line',
|
||||
WorkflowLevel::Unassigned => 'checkbox-indeterminate-line',
|
||||
WorkflowLevel::Assigned => 'hourglass-2-line',
|
||||
WorkflowLevel::Dictated => 'speak-line',
|
||||
WorkflowLevel::Transcribed => 'message-2-line',
|
||||
WorkflowLevel::Repetition => 'repeat-line',
|
||||
WorkflowLevel::ReadInProgress => 'battery-charge-line',
|
||||
WorkflowLevel::DraftAvailable => 'draft-line',
|
||||
WorkflowLevel::Finalized => 'folder-chart-line',
|
||||
WorkflowLevel::UnderReview => 'eye-line',
|
||||
WorkflowLevel::Published => 'graduation-cap-line',
|
||||
WorkflowLevel::Archived => 'archive-line',
|
||||
WorkflowLevel::Cancelled => 'close-circle-line',
|
||||
default => 'cog-6-tooth',
|
||||
};
|
||||
|
||||
return sprintf('<span class="badge badge-center rounded-pill %s"><i class="fa-solid fa-%s"></i></span>', $color, $icon);
|
||||
$descr = $this->workflow_level->describe();
|
||||
|
||||
return sprintf('<span class="text-gray" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="%s" title="%s"><i class="ri-%s"></i></span>', $descr, $descr, $icon);
|
||||
}
|
||||
|
||||
public function getArchiveLink(): ?string
|
||||
@ -315,7 +316,7 @@ public function toArray(): array
|
||||
'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(),
|
||||
'workflow_level_led' => $this->getWorkflowLevelLedAttribute(),
|
||||
'priority_icon' => $this->getPriorityIcon(),
|
||||
'sex_age' => $this->sexAge(),
|
||||
'num_instances' => $this->numInstances(),
|
||||
@ -369,14 +370,14 @@ public function isUnlocked(): bool
|
||||
return $this->locked_at === null;
|
||||
}
|
||||
|
||||
public function lockStudy(User|int|null $user = null, ?WorkflowLevel $status = null): void
|
||||
public function lockStudy(User|int|null $user = null, ?WorkflowLevel $level = null): void
|
||||
{
|
||||
$params = [
|
||||
'locking_physician_id' => me($user)->id,
|
||||
'locked_at' => now(),
|
||||
];
|
||||
if ($status) {
|
||||
$params['workflow_level'] = $status->value;
|
||||
if ($level !== null) {
|
||||
$params['workflow_level'] = $level->value;
|
||||
}
|
||||
|
||||
$this->update($params);
|
||||
@ -456,18 +457,18 @@ 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
|
||||
public function setReportWorkflowLevel(WorkflowLevel $level, User|int|null $user = null): void
|
||||
{
|
||||
$user_id = me($user)->id;
|
||||
$params = ['report_status' => $status->value];
|
||||
$params = ['workflow_level' => $level->value];
|
||||
|
||||
switch ($status) {
|
||||
case ReportStatus::Finalized:
|
||||
switch ($level) {
|
||||
case WorkflowLevel::Finalized:
|
||||
$params['reading_physician_id'] = $user_id;
|
||||
$params['read_at'] = now();
|
||||
break;
|
||||
|
||||
case ReportStatus::Approved:
|
||||
case WorkflowLevel::Published:
|
||||
if ($this->reading_physician_id === null) {
|
||||
$params['reading_physician_id'] = $user_id;
|
||||
$params['read_at'] = now();
|
||||
@ -483,18 +484,18 @@ public function setReportStatus(ReportStatus $status, User|int|null $user = null
|
||||
public function canEditReport(): bool
|
||||
{
|
||||
// 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
|
||||
{
|
||||
return $this->isActive() && $this->reportStatusBefore(ReportStatus::Finalized);
|
||||
return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::Finalized);
|
||||
}
|
||||
|
||||
public function canAssignRad(): bool
|
||||
@ -503,7 +504,7 @@ public function canAssignRad(): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isActive() && $this->reportStatusBefore(ReportStatus::Preliminary);
|
||||
return $this->isActive() && $this->reportStatusBefore(WorkflowLevel::DraftAvailable);
|
||||
}
|
||||
|
||||
public function hasReports(): bool
|
||||
@ -513,13 +514,13 @@ public function hasReports(): bool
|
||||
|
||||
public function isReportReady(): bool
|
||||
{
|
||||
return ($this->report_status->value == ReportStatus::Finalized->value) ||
|
||||
($this->report_status->value == ReportStatus::Approved->value);
|
||||
return ($this->workflow_level->value == WorkflowLevel::Finalized->value) ||
|
||||
($this->workflow_level->value == WorkflowLevel::Published->value);
|
||||
}
|
||||
|
||||
public function isStudyComplete(): bool
|
||||
{
|
||||
return $this->report_status->value >= ReportStatus::Finalized->value;
|
||||
return $this->workflow_level->value >= WorkflowLevel::Finalized->value;
|
||||
}
|
||||
|
||||
public function bookmarkedByUsers()
|
||||
@ -531,7 +532,6 @@ protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'workflow_level' => WorkflowLevel::class,
|
||||
'report_status' => ReportStatus::class,
|
||||
'priority' => Priority::class,
|
||||
'archived_at' => 'immutable_datetime',
|
||||
'approved_at' => 'immutable_datetime',
|
||||
@ -543,4 +543,16 @@ protected function casts(): array
|
||||
'patient_birthdate' => 'immutable_date',
|
||||
];
|
||||
}
|
||||
|
||||
public function hasHistory(): bool
|
||||
{
|
||||
return Cache::remember("study.has_history.{$this->id}",
|
||||
now()->addMinutes(15),
|
||||
function () {
|
||||
return DB::table('study_details')
|
||||
->where('study_id', $this->id)
|
||||
->whereNotNull('clinical_history')
|
||||
->exists();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Domain\Report\ExportFormat;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Services\Report\ReportStorage;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
|
||||
@ -43,19 +43,19 @@ public function scopeForStudy(Builder $query, Study|int $study): Builder
|
||||
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;
|
||||
$params = ['report_status' => $status->value];
|
||||
switch ($status) {
|
||||
case ReportStatus::Dictated:
|
||||
$params = ['workflow_level' => $level->value];
|
||||
switch ($level) {
|
||||
case WorkflowLevel::Dictated:
|
||||
$params['dictated_by_id'] = $user_id;
|
||||
break;
|
||||
case ReportStatus::Preliminary:
|
||||
case ReportStatus::Finalized:
|
||||
case WorkflowLevel::DraftAvailable:
|
||||
case WorkflowLevel::Finalized:
|
||||
$params['read_by_id'] = $user_id;
|
||||
break;
|
||||
case ReportStatus::Approved:
|
||||
case WorkflowLevel::Published:
|
||||
$params['approved_by_id'] = $user_id;
|
||||
break;
|
||||
}
|
||||
@ -96,12 +96,12 @@ public function getContent(): ?string
|
||||
|
||||
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
|
||||
{
|
||||
if ($this->report_status->value < ReportStatus::Finalized->value) {
|
||||
if ($this->workflow_level->value < WorkflowLevel::Finalized->value) {
|
||||
if ($this->read_by_id === me($user)->id) {
|
||||
return true;
|
||||
}
|
||||
@ -117,7 +117,7 @@ public function canRemove(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->read_by_id === me($user)->id) {
|
||||
return true;
|
||||
@ -128,7 +128,7 @@ public function canEdit(User|int|null $user = null): 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
|
||||
{
|
||||
return [
|
||||
'report_status' => ReportStatus::class,
|
||||
'workflow_level' => WorkflowLevel::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,15 @@
|
||||
use App\Domain\ACL\Permission;
|
||||
use App\Domain\ACL\Role;
|
||||
use App\Models\Traits\Active;
|
||||
use App\Models\Traits\HasDepartment;
|
||||
use App\Models\Traits\HashableId;
|
||||
use App\Models\Traits\HasOrganization;
|
||||
use App\Services\UserService;
|
||||
use Carbon\Carbon;
|
||||
use Database\Factories\UserFactory;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
@ -30,36 +31,18 @@ class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
use Active;
|
||||
use HasApiTokens;
|
||||
use HasDepartment, HasOrganization;
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory;
|
||||
use HashableId;
|
||||
use HasProfilePhoto;
|
||||
use HasRoles;
|
||||
use Notifiable;
|
||||
use TwoFactorAuthenticatable;
|
||||
// use TwoFactorAuthenticatable;
|
||||
|
||||
public array $info = [];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'is_active',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'display_name',
|
||||
'username',
|
||||
'email',
|
||||
'phone',
|
||||
'site_id',
|
||||
'facility_id',
|
||||
'profile_photo_path',
|
||||
'timezone',
|
||||
'last_seen_at',
|
||||
'password',
|
||||
];
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
@ -159,19 +142,9 @@ public function radiologistProfile(): HasOne
|
||||
return $this->hasOne(RadiologistProfile::class);
|
||||
}
|
||||
|
||||
public function institute(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function facility(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return $this->isAdmin();
|
||||
return $this->isAdmin() && $this->is_active;
|
||||
}
|
||||
|
||||
public function panels(): HasManyThrough
|
||||
|
28
app/Services/ACL/RoleService.php
Normal file
28
app/Services/ACL/RoleService.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ACL;
|
||||
|
||||
use App\Domain\ACL\Role;
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
|
||||
final class RoleService
|
||||
{
|
||||
private static array $roles = [];
|
||||
|
||||
public static function select(): array
|
||||
{
|
||||
// self::initCache();
|
||||
|
||||
return collect(Role::cases())
|
||||
// ->mapWithKeys(fn (Role $r) => [self::$roles[$r->value] => $r->name])
|
||||
->mapWithKeys(fn (Role $r) => [$r->value => $r->name])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private static function initCache(): void
|
||||
{
|
||||
if (empty(self::$roles)) {
|
||||
self::$roles = SpatieRole::pluck('id', 'name')->toArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@
|
||||
enum WorklistColumn: string
|
||||
{
|
||||
case WorkflowLevel = 'workflow_level';
|
||||
case ReportStatus = 'report_status';
|
||||
case StudyHash = 'hash';
|
||||
case PatientName = 'patient_name';
|
||||
case PatientId = 'patient_id';
|
||||
|
@ -14,7 +14,7 @@ public static function worklistColumns(User|int|null $usr = null): Collection
|
||||
$user = me($usr);
|
||||
$columns = collect([
|
||||
WorklistColumn::Priority,
|
||||
WorklistColumn::ReportStatus,
|
||||
WorklistColumn::WorkflowLevel,
|
||||
WorklistColumn::ActionButtons,
|
||||
// WorklistColumn::AssignedPhysician,
|
||||
WorklistColumn::PatientId,
|
||||
@ -57,7 +57,7 @@ public static function worklistButtons(Study $study, User|int|null $usr = null):
|
||||
if ($user->isRadiologist()) {
|
||||
return collect([
|
||||
WorklistButton::StudyMetadata,
|
||||
WorklistButton::Notes,
|
||||
// WorklistButton::Notes,
|
||||
// WorklistButton::Audit,
|
||||
WorklistButton::Bookmark,
|
||||
]);
|
||||
@ -66,7 +66,7 @@ public static function worklistButtons(Study $study, User|int|null $usr = null):
|
||||
$buttons = collect([
|
||||
WorklistButton::StudyMetadata,
|
||||
WorklistButton::History,
|
||||
WorklistButton::Notes,
|
||||
// WorklistButton::Notes,
|
||||
]);
|
||||
|
||||
if ($study->canAssignRad()) {
|
||||
|
273
app/Services/GeoLocation.php
Normal file
273
app/Services/GeoLocation.php
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
final class GeoLocation
|
||||
{
|
||||
private static array $countries = [
|
||||
'AF' => 'Afghanistan',
|
||||
'AX' => 'Åland Islands',
|
||||
'AL' => 'Albania',
|
||||
'DZ' => 'Algeria',
|
||||
'AS' => 'American Samoa',
|
||||
'AD' => 'Andorra',
|
||||
'AO' => 'Angola',
|
||||
'AI' => 'Anguilla',
|
||||
'AQ' => 'Antarctica',
|
||||
'AG' => 'Antigua & Barbuda',
|
||||
'AR' => 'Argentina',
|
||||
'AM' => 'Armenia',
|
||||
'AW' => 'Aruba',
|
||||
'AU' => 'Australia',
|
||||
'AT' => 'Austria',
|
||||
'AZ' => 'Azerbaijan',
|
||||
'BS' => 'Bahamas',
|
||||
'BH' => 'Bahrain',
|
||||
'BD' => 'Bangladesh',
|
||||
'BB' => 'Barbados',
|
||||
'BY' => 'Belarus',
|
||||
'BE' => 'Belgium',
|
||||
'BZ' => 'Belize',
|
||||
'BJ' => 'Benin',
|
||||
'BM' => 'Bermuda',
|
||||
'BT' => 'Bhutan',
|
||||
'BO' => 'Bolivia',
|
||||
'BA' => 'Bosnia & Herzegovina',
|
||||
'BW' => 'Botswana',
|
||||
'BV' => 'Bouvet Island',
|
||||
'BR' => 'Brazil',
|
||||
'IO' => 'British Indian Ocean Territory',
|
||||
'VG' => 'British Virgin Islands',
|
||||
'BN' => 'Brunei',
|
||||
'BG' => 'Bulgaria',
|
||||
'BF' => 'Burkina Faso',
|
||||
'BI' => 'Burundi',
|
||||
'KH' => 'Cambodia',
|
||||
'CM' => 'Cameroon',
|
||||
'CA' => 'Canada',
|
||||
'CV' => 'Cape Verde',
|
||||
'BQ' => 'Caribbean Netherlands',
|
||||
'KY' => 'Cayman Islands',
|
||||
'CF' => 'Central African Republic',
|
||||
'TD' => 'Chad',
|
||||
'CL' => 'Chile',
|
||||
'CN' => 'China',
|
||||
'CX' => 'Christmas Island',
|
||||
'CC' => 'Cocos (Keeling) Islands',
|
||||
'CO' => 'Colombia',
|
||||
'KM' => 'Comoros',
|
||||
'CG' => 'Congo - Brazzaville',
|
||||
'CD' => 'Congo - Kinshasa',
|
||||
'CK' => 'Cook Islands',
|
||||
'CR' => 'Costa Rica',
|
||||
'CI' => 'Côte d’Ivoire',
|
||||
'HR' => 'Croatia',
|
||||
'CU' => 'Cuba',
|
||||
'CW' => 'Curaçao',
|
||||
'CY' => 'Cyprus',
|
||||
'CZ' => 'Czechia',
|
||||
'DK' => 'Denmark',
|
||||
'DJ' => 'Djibouti',
|
||||
'DM' => 'Dominica',
|
||||
'DO' => 'Dominican Republic',
|
||||
'EC' => 'Ecuador',
|
||||
'EG' => 'Egypt',
|
||||
'SV' => 'El Salvador',
|
||||
'GQ' => 'Equatorial Guinea',
|
||||
'ER' => 'Eritrea',
|
||||
'EE' => 'Estonia',
|
||||
'SZ' => 'Eswatini',
|
||||
'ET' => 'Ethiopia',
|
||||
'FK' => 'Falkland Islands',
|
||||
'FO' => 'Faroe Islands',
|
||||
'FJ' => 'Fiji',
|
||||
'FI' => 'Finland',
|
||||
'FR' => 'France',
|
||||
'GF' => 'French Guiana',
|
||||
'PF' => 'French Polynesia',
|
||||
'TF' => 'French Southern Territories',
|
||||
'GA' => 'Gabon',
|
||||
'GM' => 'Gambia',
|
||||
'GE' => 'Georgia',
|
||||
'DE' => 'Germany',
|
||||
'GH' => 'Ghana',
|
||||
'GI' => 'Gibraltar',
|
||||
'GR' => 'Greece',
|
||||
'GL' => 'Greenland',
|
||||
'GD' => 'Grenada',
|
||||
'GP' => 'Guadeloupe',
|
||||
'GU' => 'Guam',
|
||||
'GT' => 'Guatemala',
|
||||
'GG' => 'Guernsey',
|
||||
'GN' => 'Guinea',
|
||||
'GW' => 'Guinea-Bissau',
|
||||
'GY' => 'Guyana',
|
||||
'HT' => 'Haiti',
|
||||
'HM' => 'Heard & McDonald Islands',
|
||||
'HN' => 'Honduras',
|
||||
'HK' => 'Hong Kong SAR China',
|
||||
'HU' => 'Hungary',
|
||||
'IS' => 'Iceland',
|
||||
'IN' => 'India',
|
||||
'ID' => 'Indonesia',
|
||||
'IR' => 'Iran',
|
||||
'IQ' => 'Iraq',
|
||||
'IE' => 'Ireland',
|
||||
'IM' => 'Isle of Man',
|
||||
'IL' => 'Israel',
|
||||
'IT' => 'Italy',
|
||||
'JM' => 'Jamaica',
|
||||
'JP' => 'Japan',
|
||||
'JE' => 'Jersey',
|
||||
'JO' => 'Jordan',
|
||||
'KZ' => 'Kazakhstan',
|
||||
'KE' => 'Kenya',
|
||||
'KI' => 'Kiribati',
|
||||
'KW' => 'Kuwait',
|
||||
'KG' => 'Kyrgyzstan',
|
||||
'LA' => 'Laos',
|
||||
'LV' => 'Latvia',
|
||||
'LB' => 'Lebanon',
|
||||
'LS' => 'Lesotho',
|
||||
'LR' => 'Liberia',
|
||||
'LY' => 'Libya',
|
||||
'LI' => 'Liechtenstein',
|
||||
'LT' => 'Lithuania',
|
||||
'LU' => 'Luxembourg',
|
||||
'MO' => 'Macao SAR China',
|
||||
'MG' => 'Madagascar',
|
||||
'MW' => 'Malawi',
|
||||
'MY' => 'Malaysia',
|
||||
'MV' => 'Maldives',
|
||||
'ML' => 'Mali',
|
||||
'MT' => 'Malta',
|
||||
'MH' => 'Marshall Islands',
|
||||
'MQ' => 'Martinique',
|
||||
'MR' => 'Mauritania',
|
||||
'MU' => 'Mauritius',
|
||||
'YT' => 'Mayotte',
|
||||
'MX' => 'Mexico',
|
||||
'FM' => 'Micronesia',
|
||||
'MD' => 'Moldova',
|
||||
'MC' => 'Monaco',
|
||||
'MN' => 'Mongolia',
|
||||
'ME' => 'Montenegro',
|
||||
'MS' => 'Montserrat',
|
||||
'MA' => 'Morocco',
|
||||
'MZ' => 'Mozambique',
|
||||
'MM' => 'Myanmar (Burma)',
|
||||
'NA' => 'Namibia',
|
||||
'NR' => 'Nauru',
|
||||
'NP' => 'Nepal',
|
||||
'NL' => 'Netherlands',
|
||||
'NC' => 'New Caledonia',
|
||||
'NZ' => 'New Zealand',
|
||||
'NI' => 'Nicaragua',
|
||||
'NE' => 'Niger',
|
||||
'NG' => 'Nigeria',
|
||||
'NU' => 'Niue',
|
||||
'NF' => 'Norfolk Island',
|
||||
'KP' => 'North Korea',
|
||||
'MK' => 'North Macedonia',
|
||||
'MP' => 'Northern Mariana Islands',
|
||||
'NO' => 'Norway',
|
||||
'OM' => 'Oman',
|
||||
'PK' => 'Pakistan',
|
||||
'PW' => 'Palau',
|
||||
'PS' => 'Palestinian Territories',
|
||||
'PA' => 'Panama',
|
||||
'PG' => 'Papua New Guinea',
|
||||
'PY' => 'Paraguay',
|
||||
'PE' => 'Peru',
|
||||
'PH' => 'Philippines',
|
||||
'PN' => 'Pitcairn Islands',
|
||||
'PL' => 'Poland',
|
||||
'PT' => 'Portugal',
|
||||
'PR' => 'Puerto Rico',
|
||||
'QA' => 'Qatar',
|
||||
'RE' => 'Réunion',
|
||||
'RO' => 'Romania',
|
||||
'RU' => 'Russia',
|
||||
'RW' => 'Rwanda',
|
||||
'WS' => 'Samoa',
|
||||
'SM' => 'San Marino',
|
||||
'ST' => 'São Tomé & Príncipe',
|
||||
'SA' => 'Saudi Arabia',
|
||||
'SN' => 'Senegal',
|
||||
'RS' => 'Serbia',
|
||||
'SC' => 'Seychelles',
|
||||
'SL' => 'Sierra Leone',
|
||||
'SG' => 'Singapore',
|
||||
'SX' => 'Sint Maarten',
|
||||
'SK' => 'Slovakia',
|
||||
'SI' => 'Slovenia',
|
||||
'SB' => 'Solomon Islands',
|
||||
'SO' => 'Somalia',
|
||||
'ZA' => 'South Africa',
|
||||
'GS' => 'South Georgia & South Sandwich Islands',
|
||||
'KR' => 'South Korea',
|
||||
'SS' => 'South Sudan',
|
||||
'ES' => 'Spain',
|
||||
'LK' => 'Sri Lanka',
|
||||
'BL' => 'St. Barthélemy',
|
||||
'SH' => 'St. Helena',
|
||||
'KN' => 'St. Kitts & Nevis',
|
||||
'LC' => 'St. Lucia',
|
||||
'MF' => 'St. Martin',
|
||||
'PM' => 'St. Pierre & Miquelon',
|
||||
'VC' => 'St. Vincent & Grenadines',
|
||||
'SD' => 'Sudan',
|
||||
'SR' => 'Suriname',
|
||||
'SJ' => 'Svalbard & Jan Mayen',
|
||||
'SE' => 'Sweden',
|
||||
'CH' => 'Switzerland',
|
||||
'SY' => 'Syria',
|
||||
'TW' => 'Taiwan',
|
||||
'TJ' => 'Tajikistan',
|
||||
'TZ' => 'Tanzania',
|
||||
'TH' => 'Thailand',
|
||||
'TL' => 'Timor-Leste',
|
||||
'TG' => 'Togo',
|
||||
'TK' => 'Tokelau',
|
||||
'TO' => 'Tonga',
|
||||
'TT' => 'Trinidad & Tobago',
|
||||
'TN' => 'Tunisia',
|
||||
'TR' => 'Turkey',
|
||||
'TM' => 'Turkmenistan',
|
||||
'TC' => 'Turks & Caicos Islands',
|
||||
'TV' => 'Tuvalu',
|
||||
'UM' => 'U.S. Outlying Islands',
|
||||
'VI' => 'U.S. Virgin Islands',
|
||||
'UG' => 'Uganda',
|
||||
'UA' => 'Ukraine',
|
||||
'AE' => 'United Arab Emirates',
|
||||
'GB' => 'United Kingdom',
|
||||
'US' => 'United States',
|
||||
'UY' => 'Uruguay',
|
||||
'UZ' => 'Uzbekistan',
|
||||
'VU' => 'Vanuatu',
|
||||
'VA' => 'Vatican City',
|
||||
'VE' => 'Venezuela',
|
||||
'VN' => 'Vietnam',
|
||||
'WF' => 'Wallis & Futuna',
|
||||
'EH' => 'Western Sahara',
|
||||
'YE' => 'Yemen',
|
||||
'ZM' => 'Zambia',
|
||||
'ZW' => 'Zimbabwe',
|
||||
];
|
||||
|
||||
public static function codes(): array
|
||||
{
|
||||
return array_keys(self::$countries);
|
||||
}
|
||||
|
||||
public static function select(): array
|
||||
{
|
||||
return self::$countries;
|
||||
}
|
||||
|
||||
public static function name(string $code): string
|
||||
{
|
||||
return self::$countries[strtoupper($code)] ?? '';
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'is_archived' => true,
|
||||
'archived_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
DB::table('studies')->where('id', $study_id)->update($payload);
|
||||
@ -30,7 +30,7 @@ public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
||||
->category(Category::SYSTEM)
|
||||
->on($study_id)
|
||||
->orthanc($orthanc_uuid)
|
||||
->did(Activity::Study_Archived)
|
||||
->did(Activity::Study_Archive)
|
||||
->log(false);
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
||||
$sync->resetQueues();
|
||||
|
||||
$studies = DB::table('studies')
|
||||
->where('dicom_server_id', $sync->getDicomServer()->id)
|
||||
->whereNull('archived_at')
|
||||
->get(['orthanc_uuid', 'workflow_level'])
|
||||
->pluck('workflow_level', 'orthanc_uuid');
|
||||
|
@ -26,7 +26,6 @@ public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
||||
|
||||
$row = Study::create($payload['study']);
|
||||
$payload['details']['study_id'] = $row->id;
|
||||
$payload['details']['orthanc_uuid'] = $orthanc_uuid;
|
||||
StudyDetails::create($payload['details']);
|
||||
|
||||
audit()
|
||||
|
@ -4,13 +4,22 @@
|
||||
|
||||
use App\Services\Pacs\Sync\StudiesSync;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
final readonly class ScanStudies
|
||||
{
|
||||
public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
||||
{
|
||||
$study_ids = $sync->getClient()->getStudiesIds();
|
||||
$sync->setStudyIds($study_ids);
|
||||
$study_ids = [];
|
||||
try {
|
||||
$study_ids = $sync->getClient()->getStudiesIds();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
if (! empty($study_ids)) {
|
||||
$sync->setStudyIds($study_ids);
|
||||
}
|
||||
|
||||
return $next($sync);
|
||||
}
|
||||
|
@ -8,14 +8,17 @@
|
||||
use App\Services\Pacs\DicomUtils;
|
||||
use App\Services\Pacs\OrthancRestClient;
|
||||
use App\Services\StudyRouter\DicomStudyRouter;
|
||||
use App\Services\StudyRouter\RawDicomTags;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StudiesSync
|
||||
final class StudiesSync
|
||||
{
|
||||
public const SYNC_AGENT = '$$_pacs_agent_$$';
|
||||
|
||||
private Collection $study_ids;
|
||||
|
||||
private Collection $insert_queue;
|
||||
@ -145,6 +148,29 @@ public function transformData(mixed $orthanc_src): array
|
||||
|
||||
$inst_name = data_get($orthanc_src, 'MainDicomTags.InstitutionName');
|
||||
$patient_name = data_get($orthanc_src, 'PatientMainDicomTags.PatientName');
|
||||
$name_parts = tokenizeString($patient_name);
|
||||
$patient_age = data_get($orthanc_src, 'RequestedTags.PatientAge');
|
||||
if (blank($patient_age) && ! empty($name_parts)) {
|
||||
// try to get age from last part of patient name
|
||||
$last = end($name_parts);
|
||||
if (preg_match('/^\d+[YMD]$/i', $last)) {
|
||||
$patient_age = $last;
|
||||
/*
|
||||
// sanitize patient name
|
||||
array_pop($name_parts);
|
||||
$patient_name = implode(' ', $name_parts);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
if ($patient_age !== null) {
|
||||
$age = strtoupper(ltrim($patient_age, '0'));
|
||||
if (strlen($age) > 1) {
|
||||
$patient_age = $age;
|
||||
}
|
||||
}
|
||||
|
||||
// $patient_name = trim($patient_name, '.^ ');
|
||||
|
||||
$descr = $this->getStudyDescription($orthanc_src);
|
||||
$study = [
|
||||
@ -158,7 +184,7 @@ public function transformData(mixed $orthanc_src): array
|
||||
'patient_id' => data_get($orthanc_src, 'PatientMainDicomTags.PatientID'),
|
||||
'patient_name' => $patient_name,
|
||||
'patient_sex' => data_get($orthanc_src, 'PatientMainDicomTags.PatientSex'),
|
||||
'patient_age' => data_get($orthanc_src, 'RequestedTags.PatientAge'),
|
||||
'patient_age' => $patient_age,
|
||||
|
||||
'accession_number' => data_get($orthanc_src, 'MainDicomTags.AccessionNumber'),
|
||||
'referring_physician_name' => data_get($orthanc_src, 'MainDicomTags.ReferringPhysicianName'),
|
||||
@ -179,17 +205,41 @@ public function transformData(mixed $orthanc_src): array
|
||||
|
||||
$study['workflow_level'] = $stable_study
|
||||
? WorkflowLevel::Unassigned->value
|
||||
: WorkflowLevel::Pending->value;
|
||||
$study['patient_birthdate'] = null;
|
||||
: WorkflowLevel::Received->value;
|
||||
|
||||
$patient_birthdate = null;
|
||||
$dob = data_get($orthanc_src, 'PatientMainDicomTags.PatientBirthDate');
|
||||
if (filled($dob)) {
|
||||
try {
|
||||
$study['patient_birthdate'] = Carbon::parse($dob);
|
||||
} catch (Exception) {
|
||||
Log::error('Failed to parse PatientMainDicomTags.PatientBirthDate: {dob}', ['dob' => $dob]);
|
||||
$patient_birthdate = Carbon::parse($dob);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to parse PatientMainDicomTags.PatientBirthDate: {dob}', ['dob' => $dob, 'exception' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($patient_birthdate == null && $patient_age !== null) {
|
||||
try {
|
||||
// $age = (int) preg_replace('/[^0-9]/', '', $patient_age);
|
||||
$age_num = (int) filter_var($patient_age, FILTER_SANITIZE_NUMBER_INT);
|
||||
$now = now();
|
||||
switch (strtoupper(substr($patient_age, -1))) {
|
||||
case 'Y':
|
||||
$patient_birthdate = $now->subYears($age_num);
|
||||
break;
|
||||
case 'M':
|
||||
$patient_birthdate = $now->subMonths($age_num);
|
||||
break;
|
||||
case 'D':
|
||||
$patient_birthdate = $now->subDays($age_num);
|
||||
break;
|
||||
}
|
||||
} catch (Exception) {
|
||||
Log::error('Failed to parse patient_age: {age}', ['age' => $patient_age]);
|
||||
}
|
||||
}
|
||||
|
||||
$study['patient_birthdate'] = $patient_birthdate;
|
||||
|
||||
// check for priority in patient name or description
|
||||
if (preg_match('/\b(urgent|stat)\b/i', implode(' ', [$descr, $patient_name]))) {
|
||||
$this->setValue($study, 'priority', Priority::Stat->value);
|
||||
@ -281,22 +331,25 @@ private function getStudyDicomTags(string $study_uuid): array
|
||||
}
|
||||
|
||||
// randomly sample few instances for tags collection
|
||||
$selectedInstances = count($instances) <= $this->maxInstances ? $instances : array_rand(array_flip($instances), $this->maxInstances);
|
||||
$selectedInstances = count($instances) <= $this->maxInstances
|
||||
? $instances
|
||||
: array_intersect_key($instances, array_flip(array_rand($instances, $this->maxInstances)));
|
||||
|
||||
$tags = [];
|
||||
$tags = collect();
|
||||
foreach ($selectedInstances as $instance) {
|
||||
foreach ($this->fetchInstancesTags($instance) as $key => $value) {
|
||||
if ($key == 'MainDicomTags' || $key == 'RequestedTags') {
|
||||
foreach ($value as $tag => $val) {
|
||||
if (! isset($tags[$tag]) || $tags[$tag] !== $val) {
|
||||
$tags[$tag] = $val;
|
||||
$dcmTag = RawDicomTags::tryFrom($tag);
|
||||
if ($dcmTag != null && (! $tags->has($dcmTag->name) || $tags->get($dcmTag->name) !== $val)) {
|
||||
$tags->put($dcmTag->name, $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
return $tags->toArray();
|
||||
}
|
||||
|
||||
private function setValue(array &$array, string $key, mixed $value): void
|
||||
|
@ -3,7 +3,7 @@
|
||||
namespace App\Services\Report;
|
||||
|
||||
use App\Domain\ACL\Permission;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Study;
|
||||
use App\Models\StudyReport;
|
||||
use App\Services\AuditTrail\Activity;
|
||||
@ -41,13 +41,13 @@ public function getReports(): Collection
|
||||
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([
|
||||
'study_id' => $this->study->id,
|
||||
'organization_id' => $this->study->organization_id,
|
||||
'department_id' => $this->study->department_id,
|
||||
'report_status' => $status->value,
|
||||
'workflow_level' => $level->value,
|
||||
'read_by_id' => me()->id,
|
||||
]);
|
||||
$report->saveContent($content);
|
||||
@ -65,14 +65,14 @@ public function createReport(string $content, ReportStatus $status): StudyReport
|
||||
|
||||
public function finalizeReport(StudyReport $report): void
|
||||
{
|
||||
$report->setStatus(ReportStatus::Finalized);
|
||||
$report->setWorkflowLevel(WorkflowLevel::Finalized);
|
||||
audit()
|
||||
->on($this->study)
|
||||
->did(Activity::Report_Finalize)
|
||||
->notes($report->accession_number)
|
||||
->log();
|
||||
|
||||
$this->study->setReportStatus(ReportStatus::Finalized);
|
||||
$this->study->setReportWorkflowLevel(WorkflowLevel::Finalized);
|
||||
|
||||
audit()
|
||||
->on($this->study)
|
||||
@ -112,7 +112,7 @@ public function check(): ?View
|
||||
public function lockStudyIfRequired(): void
|
||||
{
|
||||
if ($this->study->isUnlocked()) {
|
||||
$this->study->lockStudy();
|
||||
$this->study->lockStudy(level: WorkflowLevel::ReadInProgress);
|
||||
audit()
|
||||
->on($this->study)
|
||||
->did(Activity::Study_Lock)
|
||||
@ -123,7 +123,7 @@ public function lockStudyIfRequired(): void
|
||||
public function latestReport(): ?StudyReport
|
||||
{
|
||||
return StudyReport::forStudy($this->study)
|
||||
->where('report_status', ReportStatus::Preliminary->value)
|
||||
->where('workflow_level', WorkflowLevel::DraftAvailable->value)
|
||||
->select(['id', 'accession_number', 'file_path'])
|
||||
->latest()
|
||||
->first();
|
||||
|
@ -3,16 +3,15 @@
|
||||
namespace App\Services\StudyRouter;
|
||||
|
||||
use App\Domain\ACL\Role;
|
||||
use App\Domain\Rule\MatchCondition;
|
||||
use App\Domain\Rule\MatchMode;
|
||||
use App\Models\AssignmentPanel;
|
||||
use App\Models\DicomRoutingRule;
|
||||
use App\Models\DicomRuleCondition;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Services\ContentMatcher;
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
|
||||
final class DicomStudyRouter
|
||||
{
|
||||
@ -30,22 +29,27 @@ public static function matchStudy(array $dicomHeaders): array
|
||||
return self::fallbackRouting();
|
||||
}
|
||||
|
||||
$study = json_decode(json_encode($dicomHeaders)); // convert to object
|
||||
$expression = new ExpressionLanguage;
|
||||
foreach (self::$rules as $rule) {
|
||||
$conditions = $rule->conditions()->orderByDesc('priority')->get();
|
||||
$matches = $rule->match_condition === MatchCondition::ALL
|
||||
? $conditions->every(fn ($condition) => self::matchCondition($condition, $dicomHeaders))
|
||||
: $conditions->contains(fn ($condition) => self::matchCondition($condition, $dicomHeaders));
|
||||
try {
|
||||
$matches = (bool) $expression->evaluate($rule->condition, ['study' => $study]);
|
||||
} catch (Exception $exc) {
|
||||
Log::error('Error evaluating rule expression', [
|
||||
'rule_id' => $rule->id,
|
||||
'condition' => $rule->condition,
|
||||
'error' => $exc->getMessage(),
|
||||
]);
|
||||
|
||||
/*
|
||||
if ($dicomHeaders[RawDicomTags::Modality->value] === 'CR') {
|
||||
dd($rule, $conditions->toArray(), $matches, $dicomHeaders[RawDicomTags::Modality->value]);
|
||||
return self::fallbackRouting();
|
||||
}
|
||||
*/
|
||||
|
||||
if ($matches) {
|
||||
return [
|
||||
'organization_id' => $rule->organization_id,
|
||||
'department_id' => $rule->department_id,
|
||||
'rule_id' => $rule->id,
|
||||
'rule_name' => $rule->name,
|
||||
'radiologists' => self::getRadiologists($rule),
|
||||
];
|
||||
}
|
||||
@ -70,7 +74,6 @@ private static function initialize(): void
|
||||
self::$rules = Cache::remember('dicom.rules',
|
||||
now()->addMinutes(self::CACHE_TTL),
|
||||
fn () => DicomRoutingRule::active()
|
||||
->with('conditions')
|
||||
->orderByDesc('priority')
|
||||
->get()
|
||||
);
|
||||
@ -92,22 +95,6 @@ private static function initialize(): void
|
||||
}
|
||||
}
|
||||
|
||||
private static function matchCondition(DicomRuleCondition $condition, array $dicomHeaders): bool
|
||||
{
|
||||
$dicomTag = $condition->dicom_tag;
|
||||
$dicomValue = $dicomHeaders[$dicomTag] ?? '';
|
||||
$searchPattern = $condition->search_pattern;
|
||||
|
||||
if (! $condition->case_sensitive) {
|
||||
$dicomValue = strtolower($dicomValue);
|
||||
if ($condition->match_mode !== MatchMode::Regex) {
|
||||
$searchPattern = strtolower($searchPattern);
|
||||
}
|
||||
}
|
||||
|
||||
return ContentMatcher::match($dicomValue, $searchPattern, $condition->match_mode);
|
||||
}
|
||||
|
||||
private static function getRadiologists(DicomRoutingRule $rule): array
|
||||
{
|
||||
if (! is_null($rule->assignment_panel_id)) {
|
||||
|
@ -4,62 +4,62 @@
|
||||
|
||||
enum RawDicomTags: string
|
||||
{
|
||||
case PatientName = '0010,0010'; // Patient's Name
|
||||
case PatientID = '0010,0020'; // Patient ID
|
||||
case PatientBirthDate = '0010,0030'; // Patient's Birth Date
|
||||
case PatientSex = '0010,0040'; // Patient's Sex
|
||||
case StudyInstanceUID = '0020,000D'; // Study Instance UID
|
||||
case SeriesInstanceUID = '0020,000E'; // Series Instance UID
|
||||
case StudyID = '0020,0010'; // Study ID
|
||||
case SeriesNumber = '0020,0011'; // Series Number
|
||||
case InstanceNumber = '0020,0013'; // Instance Number
|
||||
case SOPClassUID = '0008,0016'; // SOP Class UID
|
||||
case SOPInstanceUID = '0008,0018'; // SOP Instance UID
|
||||
case StudyDate = '0008,0020'; // Study Date
|
||||
case StudyTime = '0008,0030'; // Study Time
|
||||
case AccessionNumber = '0008,0050'; // Accession Number
|
||||
case Modality = '0008,0060'; // Modality
|
||||
case Manufacturer = '0008,0070'; // Manufacturer
|
||||
case InstitutionName = '0008,0080'; // Institution Name
|
||||
case ReferringPhysicianName = '0008,0090'; // Referring Physician's Name
|
||||
case StationName = '0008,1010'; // Station Name
|
||||
case SeriesDescription = '0008,103E'; // Series Description
|
||||
case ManufacturerModelName = '0008,1090'; // Manufacturer's Model Name
|
||||
case PatientAge = '0010,1010'; // Patient's Age
|
||||
case PatientWeight = '0010,1030'; // Patient's Weight
|
||||
case BodyPartExamined = '0018,0015'; // Body Part Examined
|
||||
case ProtocolName = '0018,1030'; // Protocol Name
|
||||
case SoftwareVersions = '0018,1020'; // Software Versions
|
||||
case AcquisitionDate = '0008,0022'; // Acquisition Date
|
||||
case AcquisitionTime = '0008,0032'; // Acquisition Time
|
||||
case ContentDate = '0008,0023'; // Content Date
|
||||
case ContentTime = '0008,0033'; // Content Time
|
||||
case AcquisitionDeviceProcessingDescription = '0018,1400'; // Acquisition Device Processing Description
|
||||
case InstitutionAddress = '0008,0081'; // Institution Address
|
||||
case StudyDescription = '0008,1030'; // Study Description
|
||||
case OperatorsName = '0008,1070'; // Operator's Name
|
||||
case Private10 = '0029,0010'; // Private Tag 10
|
||||
case IW_Private = '0009,0010'; // IW Private Tag
|
||||
case ImageType = '0008,0008'; // Image Type
|
||||
case PatientOrientation = '0020,0020'; // Patient Orientation
|
||||
case ImagePositionPatient = '0020,0032'; // Image Position (Patient)
|
||||
case ImageOrientationPatient = '0020,0037'; // Image Orientation (Patient)
|
||||
case FrameOfReferenceUID = '0020,0052'; // Frame of Reference UID
|
||||
case PositionReferenceIndicator = '0020,1040'; // Position Reference Indicator
|
||||
case SliceLocation = '0020,1041'; // Slice Location
|
||||
case SamplesPerPixel = '0028,0002'; // Samples per Pixel
|
||||
case PhotometricInterpretation = '0028,0004'; // Photometric Interpretation
|
||||
case Rows = '0028,0010'; // Rows
|
||||
case Columns = '0028,0011'; // Columns
|
||||
case PixelSpacing = '0028,0030'; // Pixel Spacing
|
||||
case BitsAllocated = '0028,0100'; // Bits Allocated
|
||||
case BitsStored = '0028,0101'; // Bits Stored
|
||||
case HighBit = '0028,0102'; // High Bit
|
||||
case PixelRepresentation = '0028,0103'; // Pixel Representation
|
||||
case WindowCenter = '0028,1050'; // Window Center
|
||||
case WindowWidth = '0028,1051'; // Window Width
|
||||
case RescaleIntercept = '0028,1052'; // Rescale Intercept
|
||||
case RescaleSlope = '0028,1053'; // Rescale Slope
|
||||
case InoWave_Private = '0011,1060';
|
||||
case RadFusion_SenderId = '1971,1020';
|
||||
case patient_name = '0010,0010'; // Patient's Name
|
||||
case patient_id = '0010,0020'; // Patient ID
|
||||
case patient_birth_date = '0010,0030'; // Patient's Birth Date
|
||||
case patient_sex = '0010,0040'; // Patient's Sex
|
||||
case study_instance_uid = '0020,000D'; // Study Instance UID
|
||||
case series_instance_uid = '0020,000E'; // Series Instance UID
|
||||
case study_id = '0020,0010'; // Study ID
|
||||
case series_number = '0020,0011'; // Series Number
|
||||
case instance_number = '0020,0013'; // Instance Number
|
||||
case sop_class_uid = '0008,0016'; // SOP Class UID
|
||||
case sop_instance_uid = '0008,0018'; // SOP Instance UID
|
||||
case study_date = '0008,0020'; // Study Date
|
||||
case study_time = '0008,0030'; // Study Time
|
||||
case accession_number = '0008,0050'; // Accession Number
|
||||
case modality = '0008,0060'; // Modality
|
||||
case manufacturer = '0008,0070'; // Manufacturer
|
||||
case institution_name = '0008,0080'; // Institution Name
|
||||
case referring_physician_name = '0008,0090'; // Referring Physician's Name
|
||||
case station_name = '0008,1010'; // Station Name
|
||||
case series_description = '0008,103E'; // Series Description
|
||||
case manufacturer_model_name = '0008,1090'; // Manufacturer's Model Name
|
||||
case patient_age = '0010,1010'; // Patient's Age
|
||||
case patient_weight = '0010,1030'; // Patient's Weight
|
||||
case body_part_examined = '0018,0015'; // Body Part Examined
|
||||
case protocol_name = '0018,1030'; // Protocol Name
|
||||
case software_versions = '0018,1020'; // Software Versions
|
||||
case acquisition_date = '0008,0022'; // Acquisition Date
|
||||
case acquisition_time = '0008,0032'; // Acquisition Time
|
||||
case content_date = '0008,0023'; // Content Date
|
||||
case content_time = '0008,0033'; // Content Time
|
||||
case acquisition_device_processing_description = '0018,1400'; // Acquisition Device Processing Description
|
||||
case institution_address = '0008,0081'; // Institution Address
|
||||
case study_description = '0008,1030'; // Study Description
|
||||
case operators_name = '0008,1070'; // Operator's Name
|
||||
case private_10 = '0029,0010'; // Private Tag 10
|
||||
case iw_private = '0009,0010'; // IW Private Tag
|
||||
case image_type = '0008,0008'; // Image Type
|
||||
case patient_orientation = '0020,0020'; // Patient Orientation
|
||||
case image_position_patient = '0020,0032'; // Image Position (Patient)
|
||||
case image_orientation_patient = '0020,0037'; // Image Orientation (Patient)
|
||||
case frame_of_reference_uid = '0020,0052'; // Frame of Reference UID
|
||||
case position_reference_indicator = '0020,1040'; // Position Reference Indicator
|
||||
case slice_location = '0020,1041'; // Slice Location
|
||||
case samples_per_pixel = '0028,0002'; // Samples per Pixel
|
||||
case photometric_interpretation = '0028,0004'; // Photometric Interpretation
|
||||
case rows = '0028,0010'; // Rows
|
||||
case columns = '0028,0011'; // Columns
|
||||
case pixel_spacing = '0028,0030'; // Pixel Spacing
|
||||
case bits_allocated = '0028,0100'; // Bits Allocated
|
||||
case bits_stored = '0028,0101'; // Bits Stored
|
||||
case high_bit = '0028,0102'; // High Bit
|
||||
case pixel_representation = '0028,0103'; // Pixel Representation
|
||||
case window_center = '0028,1050'; // Window Center
|
||||
case window_width = '0028,1051'; // Window Width
|
||||
case rescale_intercept = '0028,1052'; // Rescale Intercept
|
||||
case rescale_slope = '0028,1053'; // Rescale Slope
|
||||
case ino_wave_private = '0011,1060';
|
||||
case rad_fusion_sender_id = '1971,1020';
|
||||
}
|
||||
|
398
app/Services/TimezoneList.php
Normal file
398
app/Services/TimezoneList.php
Normal file
@ -0,0 +1,398 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
|
||||
final class TimezoneList
|
||||
{
|
||||
/**
|
||||
* HTML entities.
|
||||
*/
|
||||
private const MINUS = '−';
|
||||
private const PLUS = '+';
|
||||
private const WHITESPACE = ' ';
|
||||
|
||||
/**
|
||||
* General timezones.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $generalTimezones = [
|
||||
'GMT',
|
||||
'UTC',
|
||||
];
|
||||
|
||||
/**
|
||||
* All continents of the world.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $continents = [
|
||||
'Africa' => DateTimeZone::AFRICA,
|
||||
'America' => DateTimeZone::AMERICA,
|
||||
'Antarctica' => DateTimeZone::ANTARCTICA,
|
||||
'Arctic' => DateTimeZone::ARCTIC,
|
||||
'Asia' => DateTimeZone::ASIA,
|
||||
'Atlantic' => DateTimeZone::ATLANTIC,
|
||||
'Australia' => DateTimeZone::AUSTRALIA,
|
||||
'Europe' => DateTimeZone::EUROPE,
|
||||
'Indian' => DateTimeZone::INDIAN,
|
||||
'Pacific' => DateTimeZone::PACIFIC,
|
||||
];
|
||||
|
||||
/**
|
||||
* The filter of the groups to get.
|
||||
*/
|
||||
protected array $groupsFilter = [];
|
||||
|
||||
/**
|
||||
* Status of grouping the return list.
|
||||
*/
|
||||
protected bool $splitGroup = true;
|
||||
|
||||
/**
|
||||
* Status of showing timezone offset.
|
||||
*/
|
||||
protected bool $showOffset = true;
|
||||
|
||||
/**
|
||||
* The offset prefix in list.
|
||||
*/
|
||||
protected string $offsetPrefix = 'GMT/UTC';
|
||||
|
||||
/**
|
||||
* Set the filter of the groups want to get.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyGroups(array $groups = []): static
|
||||
{
|
||||
$this->groupsFilter = $groups;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter of the groups do not want to get.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function excludeGroups(array $groups = []): static
|
||||
{
|
||||
if (empty($groups)) {
|
||||
$this->groupsFilter = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->groupsFilter = array_values(array_diff(array_keys($this->continents), $groups));
|
||||
|
||||
if (! in_array('General', $groups)) {
|
||||
$this->groupsFilter[] = 'General';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether to split group or not.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function splitGroup(bool $status = true): static
|
||||
{
|
||||
$this->splitGroup = (bool) $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether to show the offset or not.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function showOffset(bool $status = true): static
|
||||
{
|
||||
$this->showOffset = (bool) $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new static to reset all config.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reset(): static
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of timezones.
|
||||
*/
|
||||
public function toArray(bool $htmlEncode = true): mixed
|
||||
{
|
||||
$list = [];
|
||||
|
||||
// If do not split group
|
||||
if (! $this->splitGroup) {
|
||||
if ($this->includeGeneral()) {
|
||||
foreach ($this->generalTimezones as $timezone) {
|
||||
$list[$timezone] = $this->formatTimezone($timezone, null, $htmlEncode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->loadContinents() as $continent => $mask) {
|
||||
$timezones = DateTimeZone::listIdentifiers($mask);
|
||||
|
||||
foreach ($timezones as $timezone) {
|
||||
$list[$timezone] = $this->formatTimezone($timezone, null, $htmlEncode);
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
// If split group
|
||||
if ($this->includeGeneral()) {
|
||||
foreach ($this->generalTimezones as $timezone) {
|
||||
$list['General'][$timezone] = $this->formatTimezone($timezone, null, $htmlEncode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->loadContinents() as $continent => $mask) {
|
||||
$timezones = DateTimeZone::listIdentifiers($mask);
|
||||
|
||||
foreach ($timezones as $timezone) {
|
||||
$list[$continent][$timezone] = $this->formatTimezone($timezone, $continent, $htmlEncode);
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of the `toSelectBox()` method.
|
||||
*
|
||||
* @param string $name The name of the select tag
|
||||
* @param string|null $selected The selected value
|
||||
* @param array|string|null $attrs The HTML attributes of select tag
|
||||
* @param bool $htmlEncode Use HTML entities for values of select tag
|
||||
*
|
||||
*@deprecated 6.0.0 This method name no longer matches the semantics
|
||||
*/
|
||||
public function create(string $name, ?string $selected = null, array|string|null $attrs = null, bool $htmlEncode = true): string
|
||||
{
|
||||
return $this->toSelectBox($name, $selected, $attrs, $htmlEncode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a select box of timezones.
|
||||
*
|
||||
* @param string $name The name of the select tag
|
||||
* @param string|null $selected The selected value
|
||||
* @param array|string|null $attrs The HTML attributes of select tag
|
||||
* @param bool $htmlEncode Use HTML entities for values of select tag
|
||||
* @return string
|
||||
*/
|
||||
public function toSelectBox(string $name, ?string $selected = null, array|string|null $attrs = null, bool $htmlEncode = true)
|
||||
{
|
||||
// Attributes for select element
|
||||
$attrString = null;
|
||||
|
||||
if (! empty($attrs)) {
|
||||
if (is_array($attrs)) {
|
||||
foreach ($attrs as $attr_name => $attr_value) {
|
||||
$attrString .= ' ' . $attr_name . '="' . $attr_value . '"';
|
||||
}
|
||||
} else {
|
||||
$attrString = $attrs;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->splitGroup) {
|
||||
return $this->makeSelectTagWithGroup($name, $selected, $attrString, $htmlEncode);
|
||||
}
|
||||
|
||||
return $this->makeSelectTagWithoutGroup($name, $selected, $attrString, $htmlEncode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate select element with the optgroup tag.
|
||||
*
|
||||
* @param string $name The name of the select tag
|
||||
* @param null|string $selected The selected value
|
||||
* @param null|string $attrs The HTML attributes of select tag
|
||||
* @param bool $htmlEncode Use HTML entities for values of select tag
|
||||
* @return string
|
||||
*/
|
||||
protected function makeSelectTagWithGroup(string $name, ?string $selected = null, ?string $attrs = null, bool $htmlEncode = true)
|
||||
{
|
||||
$attrs = ! empty($attrs) ? ' ' . trim((string) $attrs) : '';
|
||||
$output = '<select name="' . (string) $name . '"' . $attrs . '>';
|
||||
|
||||
if ($this->includeGeneral()) {
|
||||
$output .= '<optgroup label="General">';
|
||||
|
||||
foreach ($this->generalTimezones as $timezone) {
|
||||
$output .= $this->makeOptionTag($this->formatTimezone($timezone, null, $htmlEncode), $timezone, ($selected == $timezone));
|
||||
}
|
||||
|
||||
$output .= '</optgroup>';
|
||||
}
|
||||
|
||||
foreach ($this->loadContinents() as $continent => $mask) {
|
||||
$timezones = DateTimeZone::listIdentifiers($mask);
|
||||
$output .= '<optgroup label="' . $continent . '">';
|
||||
|
||||
foreach ($timezones as $timezone) {
|
||||
$output .= $this->makeOptionTag($this->formatTimezone($timezone, $continent, $htmlEncode), $timezone, ($selected == $timezone));
|
||||
}
|
||||
|
||||
$output .= '</optgroup>';
|
||||
}
|
||||
|
||||
$output .= '</select>';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate select element without the optgroup tag.
|
||||
*
|
||||
* @param string $name The name of the select tag
|
||||
* @param null|string $selected The selected value
|
||||
* @param null|string $attrs The HTML attributes of select tag
|
||||
* @param bool $htmlEncode Use HTML entities for values of select tag
|
||||
* @return string
|
||||
*/
|
||||
protected function makeSelectTagWithoutGroup(string $name, ?string $selected = null, ?string $attrs = null, bool $htmlEncode = true)
|
||||
{
|
||||
$attrs = ! empty($attrs) ? ' ' . trim((string) $attrs) : '';
|
||||
$output = '<select name="' . (string) $name . '"' . $attrs . '>';
|
||||
|
||||
if ($this->includeGeneral()) {
|
||||
foreach ($this->generalTimezones as $timezone) {
|
||||
$output .= $this->makeOptionTag($this->formatTimezone($timezone, null, $htmlEncode), $timezone, ($selected == $timezone));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->loadContinents() as $continent => $mask) {
|
||||
$timezones = DateTimeZone::listIdentifiers($mask);
|
||||
|
||||
foreach ($timezones as $timezone) {
|
||||
$output .= $this->makeOptionTag($this->formatTimezone($timezone, null, $htmlEncode), $timezone, ($selected == $timezone));
|
||||
}
|
||||
}
|
||||
|
||||
$output .= '</select>';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the option HTML tag.
|
||||
*/
|
||||
protected function makeOptionTag(string $display, string $value, bool $selected = false): string
|
||||
{
|
||||
$attrs = (bool) $selected ? ' selected="selected"' : '';
|
||||
|
||||
return '<option value="' . $value . '"' . $attrs . '>' . $display . '</option>';
|
||||
}
|
||||
|
||||
/**
|
||||
* DetermineCheck if the general timezones is loaded in the returned result.
|
||||
*/
|
||||
protected function includeGeneral(): bool
|
||||
{
|
||||
return empty($this->groupsFilter) || in_array('General', $this->groupsFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filtered continents.
|
||||
*/
|
||||
protected function loadContinents(): array
|
||||
{
|
||||
if (empty($this->groupsFilter)) {
|
||||
return $this->continents;
|
||||
}
|
||||
|
||||
return array_filter($this->continents, function ($key) {
|
||||
return in_array($key, $this->groupsFilter);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format to display timezones.
|
||||
*/
|
||||
protected function formatTimezone(string $timezone, ?string $cutOffContinent = null, bool $htmlEncode = true): string
|
||||
{
|
||||
$displayedTimezone = empty($cutOffContinent) ? $timezone : substr($timezone, strlen($cutOffContinent) + 1);
|
||||
$normalizedTimezone = $this->normalizeTimezone($displayedTimezone, $htmlEncode);
|
||||
|
||||
if (! $this->showOffset) {
|
||||
return $normalizedTimezone;
|
||||
}
|
||||
|
||||
$offset = $this->normalizeOffset($this->getOffset($timezone), $htmlEncode);
|
||||
$separator = $this->normalizeSeparator($htmlEncode);
|
||||
|
||||
return '(' . $this->offsetPrefix . $offset . ')' . $separator . $normalizedTimezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the offset.
|
||||
*/
|
||||
protected function normalizeOffset(string $offset, bool $htmlEncode = true): string
|
||||
{
|
||||
$search = ['-', '+'];
|
||||
$replace = $htmlEncode ? [' ' . self::MINUS . ' ', ' ' . self::PLUS . ' '] : [' - ', ' + '];
|
||||
|
||||
return str_replace($search, $replace, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the timezone.
|
||||
*/
|
||||
protected function normalizeTimezone(string $timezone, bool $htmlEncode = true): string
|
||||
{
|
||||
$search = ['St_', '/', '_'];
|
||||
$replace = ['St. ', ' / ', ' '];
|
||||
|
||||
return str_replace($search, $replace, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the separator between the timezone and offset.
|
||||
*/
|
||||
protected function normalizeSeparator(bool $htmlEncode = true): string
|
||||
{
|
||||
return $htmlEncode ? str_repeat(self::WHITESPACE, 5) : ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timezone offset.
|
||||
*/
|
||||
protected function getOffset(string $timezone): string
|
||||
{
|
||||
$time = new DateTime('', new DateTimeZone($timezone));
|
||||
|
||||
return $time->format('P');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the difference of timezone to Coordinated Universal Time (UTC).
|
||||
*/
|
||||
protected function getUTCOffset(string $timezone): string
|
||||
{
|
||||
$dateTimeZone = new DateTimeZone($timezone);
|
||||
$utcTime = new DateTime('', new DateTimeZone('UTC'));
|
||||
$offset = $dateTimeZone->getOffset($utcTime);
|
||||
$format = gmdate('H:i', abs($offset));
|
||||
|
||||
return $offset >= 0 ? "+{$format}" : "-{$format}";
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
namespace App\Services\Workflow;
|
||||
|
||||
use App\Domain\ACL\Permission;
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Study;
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
@ -42,28 +42,28 @@ public function can(Permission $permission): 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
|
||||
{
|
||||
return $this->activeStudy(fn () => $this->study->report_status <= ReportStatus::Preliminary);
|
||||
return $this->activeStudy(fn () => $this->study->workflow_level <= WorkflowLevel::DraftAvailable);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return $this->activeStudy(fn () => $this->study->report_status >= ReportStatus::Finalized);
|
||||
return $this->activeStudy(fn () => $this->study->workflow_level >= WorkflowLevel::Finalized);
|
||||
}
|
||||
|
||||
public function canReportEdit(): bool
|
||||
{
|
||||
return $this->activeStudy(fn () => $this->study->isUnlocked() &&
|
||||
$this->study->report_status <= ReportStatus::Preliminary &&
|
||||
$this->study->workflow_level <= WorkflowLevel::DraftAvailable &&
|
||||
$this->study->isAssignedTo($this->user)
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
use App\Models\Study;
|
||||
use App\Models\User;
|
||||
use App\Services\AuditTrail\ActivityLogger;
|
||||
use App\Services\Pacs\Sync\StudiesSync;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
@ -35,16 +36,11 @@ function array_purge(array $ary): array
|
||||
}
|
||||
|
||||
if (! function_exists('array_trim_strings')) {
|
||||
function array_trim_strings(array $ary): array
|
||||
function array_trim_strings(array $ary, bool $null_if_blank = false): array
|
||||
{
|
||||
return array_filter($ary, function ($v) {
|
||||
if (! is_string($v)) {
|
||||
return $v;
|
||||
}
|
||||
$v = trim($v);
|
||||
$trimmed = array_map(static fn ($v) => is_string($v) ? trim($v) : $v, $ary);
|
||||
|
||||
return blank($v) ? null : $v;
|
||||
});
|
||||
return $null_if_blank ? array_map(static fn ($v) => empty($v) ? null : $v, $trimmed) : $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +48,7 @@ function array_trim_strings(array $ary): array
|
||||
function sync_agent_id(): int
|
||||
{
|
||||
return cache()->rememberForever('sync_agent_id',
|
||||
fn () => User::where('username', '$$_pacs_sync_$$')->first()->id);
|
||||
fn () => User::where('username', StudiesSync::SYNC_AGENT)->first()->id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,11 +118,13 @@ function me(User|int|null $user = null): User
|
||||
{
|
||||
if (is_int($user) && $user > 0) {
|
||||
return User::find($user);
|
||||
} elseif ($user instanceof User) {
|
||||
return $user;
|
||||
} else {
|
||||
return auth()->user();
|
||||
}
|
||||
|
||||
if ($user instanceof User) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return auth()->user();
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,3 +138,10 @@ function formatTitle(string $str): string
|
||||
->toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('tokenizeString')) {
|
||||
function tokenizeString(string $str): array
|
||||
{
|
||||
return preg_split('/\W+/', trim($str), -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@
|
||||
"sentry/sentry-laravel": "^4.10",
|
||||
"spatie/laravel-medialibrary": "^11.11",
|
||||
"spatie/laravel-permission": "^6.10",
|
||||
"symfony/expression-language": "^7.2",
|
||||
"vinkla/hashids": "^12.0",
|
||||
"yajra/laravel-datatables": "^11.0"
|
||||
},
|
||||
|
@ -13,7 +13,9 @@
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
'name' => env('APP_NAME', 'PixelBridge'),
|
||||
|
||||
'version' => env('APP_VERSION', '0.1.0'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
3
database/data/departments.csv
Normal file
3
database/data/departments.csv
Normal file
@ -0,0 +1,3 @@
|
||||
is_active,organization,name
|
||||
1,Chevron Panchlaish,DR
|
||||
1,Chevron Panchlaish,Imaging
|
|
3
database/data/dicom-servers.csv
Normal file
3
database/data/dicom-servers.csv
Normal file
@ -0,0 +1,3 @@
|
||||
is_active,server_name,geo_code,host,http_port,dicom_port,rest_api_endpoint,ae_title,username,password,wado_path,stone_viewer_path,ohif_viewer_path,meddream_viewer_path,organization,department
|
||||
1,CTG-1,BD,cgp1.telerad24.com,8042,4242,http://cgp1.telerad24.com:8042/,BLACKFISH,,,dicom-web,stone-webviewer/index.html,ohif/viewer,,,
|
||||
1,NBG-1,DE,de1.telerad24.com,8042,4242,http://de1.telerad24.com:8042/,BLACKFISH,,,dicom-web,stone-webviewer/index.html,ohif/viewer,,,
|
|
5
database/data/organizations.csv
Normal file
5
database/data/organizations.csv
Normal file
@ -0,0 +1,5 @@
|
||||
name,is_active
|
||||
CATCH-ALL,1
|
||||
Chevron Panchlaish,1
|
||||
Chevron Boropole,1
|
||||
Chevron Shantibag,1
|
|
27
database/data/users.csv
Normal file
27
database/data/users.csv
Normal file
@ -0,0 +1,27 @@
|
||||
DNAME,PREFIX,FNAME,LNAME,DEGREE,USERNAME,EMAIL,PHONE,ROLE,ORG,DEPT,SIGN_IMG
|
||||
Dr. Sadrul ,Dr.,Sadrul ,Amin ,"MBBS, FCPS, M-Phil, MD (Radiology & Imaging), Ex-Associate Professor, Department of Radiology & Imaging, Bangabandhu Sheikh Mujib Medical University, BMDC Reg. No: A-26619",aminsadrul@gmail.com,aminsadrul@gmail.com,8801711362820,RAD,,,sadrul-amin.png
|
||||
Prof. Dr. Subash ,Prof. Dr.,Subash ,Mazumder,"MBBS, MD, HOD (Radiology & Imaging), Chittagong Medical College Hospital. BMDC Reg. No: A 23715",subashmajumder@yahoo.com,subashmajumder@yahoo.com,8801819329484,RAD,,,subash.png
|
||||
Dr. Goutam ,Dr.,Goutam,Chowdhury ,"MBBS, M.MED, MRCR (London), Fellow Radiology, Act (Neuro-Radiology), Consultant, Radiology & Imaging. BMDC Reg. No: A-32086",drgoutam,,,RAD,,,
|
||||
Brig. Gen. Khairul,Brig. Gen. Dr.,Md. Khairul ,Islam,"MBBS, FCPS (Radiology & Imaging), Advanced Training on MRI, CT & Coronary CT Angiogram (India). Senior Consultant, Radiology & Imaging",drkhairul,,8801715059151,RAD,,,khairul-islam.png
|
||||
Prof. Dr. Col. Nasir,Prof. Dr.Col. (Retd),Md. Nasir ,Uddin,"MBBS, FCPS, ARDMS, RVT, ARMRIT (USA), TMSS Medical College & Hospital, Bogura, Consultant Radiologist, Popular Diagnostic Center. BMDC Reg. No: A-21609",drnasir770@gmail.com,drnasir770@gmail.com,8801715748987,RAD,,,nasir-uddin.png
|
||||
Prof. Brig. Gen. Saad ,Prof. (Brig. Gen),A M Hossain,Saad,"Classified Specialist, Radiology & Imaging, Armed Forces Medical College, Dhaka Cantonment. MBBS, FCPS. Fellow CT Scan (Dublin & Germany). Fellow MRI (KSA)",drsaad,,8801715059151,RAD,,,hossain-saad.png
|
||||
Brig. Gen. Anwarul,Brig. Gen. (Retd),M Anwarul ,Haque,"MBBS. FCPS.(Radiology). Advance Training in CT & MRI (KSA), BMDC Reg. No: A -13336",dr.mahaque60@gmail.com,dr.mahaque60@gmail.com,8801911302505,RAD,,,
|
||||
Dr. Moinul ,Dr.,Mohammad Moinul,Hossain,"MBBS, MD (Radiology & Imaging)-BSMMU, Consultant (Radiology & Imaging), Radiology & Imaging Specialist, BMDC Reg. No: A-33097",mmhossainsa06@gmail.com,mmhossainsa06@gmail.com,8801715129437,RAD,,,
|
||||
Prof. Rabeya ,Asst. Prof. Dr. ,Rabeya,Khatoon,"MBBS, MCPS, MD (BSMMU), Consultant, Radiolgoy & Imaging, BIRDEM, Dhaka. BMDC Reg. No: A-47981",drrabeya,,,RAD,,,rabeya-khatoon.png
|
||||
Prof. Col. Anowar,Prof. Col. Dr.,Anowar ,Hossain,"MBBS,FCPS (Radiology & Imaging), Fellow Cardiovascular Imaging (CT and MRI). All India Institute of Medical Science, New Delhi, India. Department of Radiology & Imaging Armed Forces Medical College & CMH Dhaka, BMDC Reg No : A-32496",dranowar,,8801795430240,RAD,,,
|
||||
Dr. Hamid,Dr.,Hamid ,Alam ,,drhamid,,16462956323,RAD,,,
|
||||
Dr. Shabiha ,Dr.,Shabiha,Quadir ,,dr.shabihaquadir@gmail.com,dr.shabihaquadir@gmail.com,8801819876760,RAD,,,
|
||||
Dr. Ehsan,Dr.,Ehsan S,Choudhury,,ehsanchy@gmail.com,ehsanchy@gmail.com,8801711898269,RAD,,,
|
||||
Dr. Mobin,Dr.,Mobinul Alam,Chowdhury ,,mobinchy@hotmail.com,mobinchy@hotmail.com,8801819775757,RAD,,,
|
||||
Noor Mohammad ,,Noor ,Mohammad ,,noor.mohammad024@gmail.com,noor.mohammad024@gmail.com,8801716817712,TECH,Chevron Panchlaish,Imaging,
|
||||
Ali Akbar ,,Ali ,Akbar ,,ali.akbar.ri@gmail.com,ali.akbar.ri@gmail.com,8801816913564,TECH,Chevron Panchlaish,Imaging,
|
||||
Bulbul,,Mahbubul ,Islam ,,bulbulislam10@gmail.com,bulbulislam10@gmail.com,8801761587552,TECH,Chevron Panchlaish,Imaging,
|
||||
Zulhas,,Zillur Rahman,Zulhas,,rahmanmdzillur9@gmail.com,rahmanmdzillur9@gmail.com,8801868658028,TECH,Chevron Panchlaish,Imaging,
|
||||
Dr. Masroor,Dr.,Masroor Ehsan,Choudhury,,masroor,masroore@gmail.com,8801733938582,ADMIN,,,
|
||||
Sumit,,Sumit,Majumder,,sumit.chvrnit@gmail.com,sumit.chvrnit@gmail.com,8801822224492,ADMIN,,,
|
||||
Saiful,,Saiful,Islam,,saiful,,8801824534945,TECH,Chevron Panchlaish,Imaging,
|
||||
Apon,,Apon,,,apon,,8801627970974,TECH,Chevron Panchlaish,Imaging,
|
||||
Chevron CR,,X-ray,Tech,,chevroncr,,,TECH,Chevron Panchlaish,DR,
|
||||
Dr. Gopinathan,Dr.,Gopinathan,,"MBBS, MDRD, Reg No.62980",drgopinathan,,,RAD,,,
|
||||
Dr. Sudha,Dr.,K Sudha,,"MBBS, MDRD",drsudha,,,RAD,,,
|
||||
Dr. Rajkumar,Dr.,Rajkumar,,MBBS MDRD Reg No.TMMC130572,drrajkumar,,,RAD,,,
|
|
@ -10,7 +10,7 @@ public function up(): void
|
||||
{
|
||||
Schema::create('organizations', static function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('guid', 40)->unique()->index()->default(DB::raw("concat('INS-', gen_random_uuid())"));
|
||||
$table->string('guid', 40)->unique()->index()->default(DB::raw("concat('ORG-', gen_random_uuid())"));
|
||||
$table->string('name')->unique();
|
||||
$table->boolean('is_active')->default(false);
|
||||
$table->string('address')->nullable();
|
||||
|
@ -11,7 +11,7 @@ public function up(): void
|
||||
{
|
||||
Schema::create('departments', static function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('guid', 40)->unique()->index()->default(DB::raw("concat('FAC-', gen_random_uuid())"));
|
||||
$table->string('guid', 40)->unique()->index()->default(DB::raw("concat('DEP-', gen_random_uuid())"));
|
||||
$table->boolean('is_active')->default(false)->index();
|
||||
$table->foreignIdFor(Organization::class)->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
|
@ -16,7 +16,8 @@ public function up(): void
|
||||
$table->string('server_name')->unique();
|
||||
$table->string('geo_code', 2)->index();
|
||||
$table->string('host');
|
||||
$table->integer('port');
|
||||
$table->integer('http_port');
|
||||
$table->integer('dicom_port');
|
||||
$table->string('rest_api_endpoint');
|
||||
$table->string('ae_title')->nullable();
|
||||
$table->string('username')->nullable();
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\Priority;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Department;
|
||||
@ -21,8 +20,7 @@ public function up(): void
|
||||
$table->foreignIdFor(Department::class)->nullable()->index();
|
||||
|
||||
$table->unsignedTinyInteger('priority')->default(Priority::Routine->value);
|
||||
$table->unsignedTinyInteger('workflow_level')->default(WorkflowLevel::Pending->value);
|
||||
$table->unsignedTinyInteger('report_status')->default(ReportStatus::Unread->value);
|
||||
$table->unsignedTinyInteger('workflow_level')->default(WorkflowLevel::Received->value);
|
||||
$table->boolean('requires_approval')->default(false);
|
||||
|
||||
$table->string('orthanc_uuid')->index();
|
||||
@ -73,7 +71,7 @@ public function up(): void
|
||||
$table->index(['department_id', 'assigned_at']);
|
||||
$table->index(['department_id', 'locked_at']);
|
||||
|
||||
$table->index(['requires_approval', 'report_status']);
|
||||
$table->index(['requires_approval', 'workflow_level']);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Report\ReportStatus;
|
||||
use App\Domain\Study\WorkflowLevel;
|
||||
use App\Models\Department;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Study;
|
||||
@ -15,7 +15,7 @@ public function up(): void
|
||||
{
|
||||
Schema::create('study_reports', function (Blueprint $table) {
|
||||
$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->foreignIdFor(Organization::class)->index()->nullable()->constrained()->nullOnDelete();
|
||||
|
@ -11,8 +11,6 @@ public function up(): void
|
||||
Schema::create('study_details', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('study_id')->unique()->constrained('studies')->cascadeOnDelete();
|
||||
$table->string('orthanc_uuid')->unique();
|
||||
// $table->foreignId('user_id')->constrained('users');
|
||||
$table->text('clinical_history')->nullable();
|
||||
$table->text('surgical_history')->nullable();
|
||||
$table->text('lab_results')->nullable();
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Rule\MatchCondition;
|
||||
use App\Models\AssignmentPanel;
|
||||
use App\Models\Department;
|
||||
use App\Models\Organization;
|
||||
@ -21,7 +20,7 @@ public function up(): void
|
||||
$table->foreignIdFor(User::class)->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignIdFor(AssignmentPanel::class)->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('name')->nullable();
|
||||
$table->string('match_condition')->default(MatchCondition::ANY->value);
|
||||
$table->string('condition');
|
||||
$table->unsignedTinyInteger('priority')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Domain\Rule\MatchMode;
|
||||
use App\Models\DicomRoutingRule;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('dicom_rule_conditions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(DicomRoutingRule::class)->constrained()->cascadeOnDelete();
|
||||
$table->string('dicom_tag');
|
||||
$table->string('search_pattern');
|
||||
$table->boolean('case_sensitive')->default(false);
|
||||
$table->string('match_mode')->default(MatchMode::Exact->value);
|
||||
$table->unsignedTinyInteger('priority')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['dicom_routing_rule_id', 'priority']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('dicom_rule_conditions');
|
||||
}
|
||||
};
|
@ -15,9 +15,10 @@ public function run(): void
|
||||
$this->call([
|
||||
RoleSeeder::class,
|
||||
OrganizationSeeder::class,
|
||||
DicomServerSeeder::class,
|
||||
DicomRoutingRuleSeeder::class,
|
||||
UserSeeder::class,
|
||||
ModalityProcedureSeeder::class,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
43
database/seeders/DicomRoutingRuleSeeder.php
Normal file
43
database/seeders/DicomRoutingRuleSeeder.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\DicomRoutingRule;
|
||||
use App\Models\Organization;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DicomRoutingRuleSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$chev = Organization::where('name', 'Chevron Panchlaish')->first();
|
||||
$dept_chev_xr = Department::where('name', 'DR')->first();
|
||||
$chev_dep_ct_mr = Department::where('name', 'Imaging')->first();
|
||||
|
||||
$dr_modalities = ['DR', 'CR', 'DX', 'MG', 'OT', 'RF', 'XA', 'XRF', 'DXA', 'DBT', 'OPT'];
|
||||
$dr = collect($dr_modalities)->map(fn ($modality) => sprintf('"%s"', $modality))->implode(', ');
|
||||
|
||||
DicomRoutingRule::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'name' => 'Chevron Panchlaish Imaging',
|
||||
'organization_id' => $chev->id,
|
||||
'department_id' => $chev_dep_ct_mr->id,
|
||||
'condition' => sprintf('(study.institution_name ?? "" starts with "chevron") and (study.modality not in [%s])', $dr),
|
||||
]
|
||||
);
|
||||
|
||||
DicomRoutingRule::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'name' => 'Chevron Panchlaish X-ray',
|
||||
'organization_id' => $chev->id,
|
||||
'department_id' => $dept_chev_xr->id,
|
||||
'condition' => sprintf('(study.institution_name ?? "" starts with "chevron") and (study.modality in [%s])', $dr),
|
||||
'priority' => 99,
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
43
database/seeders/DicomServerSeeder.php
Normal file
43
database/seeders/DicomServerSeeder.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\DicomServer;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DicomServerSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$skip_first = true;
|
||||
$csv_file = fopen(base_path('database/data/dicom-servers.csv'), 'r');
|
||||
while (($data = fgetcsv($csv_file, 2000, ',')) !== false) {
|
||||
if ($skip_first) {
|
||||
$skip_first = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
$data = array_trim_strings($data, true);
|
||||
DicomServer::create([
|
||||
'is_active' => (bool) $data['0'],
|
||||
'server_name' => strtoupper($data['1']),
|
||||
'geo_code' => $data['2'],
|
||||
'host' => $data['3'],
|
||||
'http_port' => $data['4'],
|
||||
'dicom_port' => $data['5'],
|
||||
'rest_api_endpoint' => $data['6'],
|
||||
'ae_title' => strtoupper($data['7']),
|
||||
'username' => strtolower($data['8'] ?? ''),
|
||||
'password' => $data['9'],
|
||||
'wado_path' => $data['10'],
|
||||
'stone_viewer_path' => $data['11'],
|
||||
'ohif_viewer_path' => $data['12'],
|
||||
'meddream_viewer_path' => $data['13'],
|
||||
// 'organization' => $data['14'],
|
||||
// 'department' => $data['15'],
|
||||
]);
|
||||
}
|
||||
fclose($csv_file);
|
||||
|
||||
}
|
||||
}
|
@ -2,139 +2,47 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Domain\Rule\MatchCondition;
|
||||
use App\Domain\Rule\MatchMode;
|
||||
use App\Models\Department;
|
||||
use App\Models\DicomRoutingRule;
|
||||
use App\Models\DicomRuleCondition;
|
||||
use App\Models\DicomServer;
|
||||
use App\Models\Organization;
|
||||
use App\Services\StudyRouter\RawDicomTags;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class OrganizationSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Organization::create([
|
||||
'name' => 'CATCH-ALL',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$chev = Organization::create([
|
||||
'name' => 'Chevron',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$cmch = Organization::create([
|
||||
'name' => 'CMCH',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$dummy = Organization::create([
|
||||
'name' => 'Dummy Site',
|
||||
'is_active' => false,
|
||||
]);
|
||||
$organizations = [];
|
||||
|
||||
$dept_chev_xr = Department::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'name' => 'Chev-CR',
|
||||
'organization_id' => $chev->id,
|
||||
]
|
||||
);
|
||||
$chev_dep_ct_mr = Department::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'name' => 'Chev-MR',
|
||||
'organization_id' => $chev->id,
|
||||
]
|
||||
);
|
||||
$dept_cmch_mr = Department::create(
|
||||
[
|
||||
'is_active' => false,
|
||||
'name' => 'CMCH MR',
|
||||
'organization_id' => $cmch->id,
|
||||
]
|
||||
);
|
||||
$skip_first = true;
|
||||
$org_csv = fopen(base_path('database/data/organizations.csv'), 'r');
|
||||
while (($data = fgetcsv($org_csv, 2000, ',')) !== false) {
|
||||
if (! $skip_first) {
|
||||
$data = array_trim_strings($data);
|
||||
$name = $data['0'];
|
||||
$organizations[$name] = Organization::create([
|
||||
'name' => $name,
|
||||
'is_active' => (bool) $data['1'],
|
||||
]);
|
||||
}
|
||||
$skip_first = false;
|
||||
}
|
||||
fclose($org_csv);
|
||||
|
||||
$rul_chev_mr_ct = DicomRoutingRule::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'name' => 'Chevron MR/CT',
|
||||
'organization_id' => $chev->id,
|
||||
'department_id' => $chev_dep_ct_mr->id,
|
||||
'match_condition' => MatchCondition::ALL->value,
|
||||
]
|
||||
);
|
||||
|
||||
$rul_chev_xr = DicomRoutingRule::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'name' => 'Chevron X-ray',
|
||||
'organization_id' => $chev->id,
|
||||
'department_id' => $dept_chev_xr->id,
|
||||
'match_condition' => MatchCondition::ALL->value,
|
||||
'priority' => 10,
|
||||
]
|
||||
);
|
||||
|
||||
DicomRuleCondition::create([
|
||||
'dicom_tag' => RawDicomTags::InstitutionName->value,
|
||||
'search_pattern' => 'chevron',
|
||||
'dicom_routing_rule_id' => $rul_chev_mr_ct->id,
|
||||
'match_mode' => MatchMode::Contains->value,
|
||||
'case_sensitive' => false,
|
||||
]);
|
||||
DicomRuleCondition::create([
|
||||
'dicom_tag' => RawDicomTags::Modality->value,
|
||||
'search_pattern' => 'MR,CT',
|
||||
'dicom_routing_rule_id' => $rul_chev_mr_ct->id,
|
||||
'match_mode' => MatchMode::InList->value,
|
||||
'case_sensitive' => true,
|
||||
]);
|
||||
|
||||
DicomRuleCondition::create([
|
||||
'dicom_tag' => RawDicomTags::InstitutionName->value,
|
||||
'search_pattern' => 'chevron',
|
||||
'dicom_routing_rule_id' => $rul_chev_xr->id,
|
||||
'match_mode' => MatchMode::Contains->value,
|
||||
'case_sensitive' => false,
|
||||
]);
|
||||
DicomRuleCondition::create([
|
||||
'dicom_tag' => RawDicomTags::Modality->value,
|
||||
'search_pattern' => 'CR,DX,MG',
|
||||
'dicom_routing_rule_id' => $rul_chev_xr->id,
|
||||
'match_mode' => MatchMode::InList->value,
|
||||
'case_sensitive' => true,
|
||||
]);
|
||||
|
||||
DicomServer::create(
|
||||
[
|
||||
'is_active' => true,
|
||||
'server_name' => 'CTG-1',
|
||||
'geo_code' => 'BD',
|
||||
'host' => 'pacs.mylabctg.com',
|
||||
'port' => 8042,
|
||||
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
|
||||
'wado_path' => 'dicom-web',
|
||||
'ae_title' => 'RADFUSION',
|
||||
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||
'ohif_viewer_path' => 'ohif/viewer',
|
||||
]
|
||||
);
|
||||
DicomServer::create(
|
||||
[
|
||||
'is_active' => false,
|
||||
'server_name' => 'MAA-1',
|
||||
'host' => 'pacs.mylabctg.com',
|
||||
'geo_code' => 'IN',
|
||||
'port' => 8043,
|
||||
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
|
||||
'ae_title' => 'RADFUSION',
|
||||
'wado_path' => 'dicom-web',
|
||||
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||
'ohif_viewer_path' => 'ohif/viewer',
|
||||
'organization_id' => $chev->id,
|
||||
'department_id' => $dept_chev_xr->id,
|
||||
]
|
||||
);
|
||||
$dept_csv = fopen(base_path('database/data/departments.csv'), 'r');
|
||||
$skip_first = true;
|
||||
while (($data = fgetcsv($dept_csv, 2000, ',')) !== false) {
|
||||
if (! $skip_first) {
|
||||
$data = array_trim_strings($data, true);
|
||||
$name = $data['2'];
|
||||
$org_name = $data['1'];
|
||||
$org = $organizations[$org_name];
|
||||
Department::create([
|
||||
'name' => $name,
|
||||
'is_active' => (bool) $data['0'],
|
||||
'organization_id' => $org->id,
|
||||
]);
|
||||
}
|
||||
$skip_first = false;
|
||||
}
|
||||
fclose($dept_csv);
|
||||
}
|
||||
}
|
||||
|
@ -6,91 +6,79 @@
|
||||
use App\Models\Department;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Services\Pacs\Sync\StudiesSync;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
$usr = User::factory()->create([
|
||||
'first_name' => 'PACS Sync',
|
||||
'display_name' => 'PACS Sync Agent',
|
||||
'username' => '$$_pacs_sync_$$',
|
||||
'first_name' => 'PACS Agent',
|
||||
'display_name' => 'PACS Agent',
|
||||
'username' => StudiesSync::SYNC_AGENT,
|
||||
'password' => bcrypt(fake()->password(20)),
|
||||
'is_active' => false,
|
||||
]);
|
||||
$usr->assignRole(Role::SystemAgent);
|
||||
|
||||
$usr = User::factory()->create([
|
||||
'first_name' => 'Administrator',
|
||||
'display_name' => 'Admin',
|
||||
'username' => 'admin',
|
||||
'email' => 'admin@example.com',
|
||||
'email_verified_at' => now(),
|
||||
'phone' => '+8801733938582',
|
||||
]);
|
||||
$usr->assignRole(Role::Admin);
|
||||
$orgs = Organization::pluck('id', 'name');
|
||||
$depts = Department::pluck('id', 'name');
|
||||
|
||||
$chevron = Organization::where('name', 'Chevron')->first();
|
||||
$cmch = Organization::where('name', 'CMCH')->first();
|
||||
$skip_first = true;
|
||||
$csv_file = fopen(base_path('database/data/users.csv'), 'r');
|
||||
while (($data = fgetcsv($csv_file, 2 * 1024, ',')) !== false) {
|
||||
if ($skip_first) {
|
||||
$skip_first = false;
|
||||
|
||||
$chev_xr = Department::where('organization_id', $chevron->id)->first();
|
||||
User::factory(4)
|
||||
->create([
|
||||
'organization_id' => $chevron->id,
|
||||
])
|
||||
->each(function ($u, $key) {
|
||||
$u->assignRole(Role::Technician);
|
||||
$u->update([
|
||||
'username' => sprintf('tech%d', $key + 1),
|
||||
]);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
User::factory(4)
|
||||
->create([
|
||||
'organization_id' => $chevron->id,
|
||||
'department_id' => $chev_xr->id,
|
||||
])
|
||||
->each(function ($u, $key) {
|
||||
$u->assignRole(Role::Technician);
|
||||
$u->update([
|
||||
'username' => sprintf('xrt%d', $key + 1),
|
||||
]);
|
||||
});
|
||||
$data = array_trim_strings($data, true);
|
||||
$role = Role::from(strtolower($data[8]));
|
||||
$org_id = $data[9] ? $orgs[$data[9]] : null;
|
||||
$dept_id = $data[10] ? $depts[$data[10]] : null;
|
||||
$image = $data[11] ? 'signatures/' . $data[11] : null;
|
||||
|
||||
User::factory(4)
|
||||
->create([
|
||||
'organization_id' => $cmch->id,
|
||||
])
|
||||
->each(function ($u, $key) {
|
||||
$u->assignRole(Role::Technician);
|
||||
$u->update([
|
||||
'username' => sprintf('cmctech%d', $key + 1),
|
||||
]);
|
||||
});
|
||||
$user = User::create([
|
||||
'is_active' => true,
|
||||
'display_name' => $data[0],
|
||||
'prefix' => $data[1],
|
||||
'first_name' => $data[2],
|
||||
'last_name' => $data[3],
|
||||
'signature_text' => $data[4],
|
||||
'signature_image_path' => $image,
|
||||
'username' => $data[5],
|
||||
'email' => strtolower($data[6]),
|
||||
'phone' => (string) $data[7],
|
||||
'email_verified_at' => $data[6] ? now() : null,
|
||||
'password' => bcrypt('password'),
|
||||
'organization_id' => $org_id,
|
||||
'department_id' => $dept_id,
|
||||
]);
|
||||
$user->assignRole($role->value);
|
||||
}
|
||||
fclose($csv_file);
|
||||
|
||||
$images = [
|
||||
'hossain-saad.png', 'nasir-uddin.png', 'sadrul-amin.png',
|
||||
'khairul-islam.png', 'rabeya-khatoon.png', 'subash.png',
|
||||
];
|
||||
$this->copySignatureImages();
|
||||
}
|
||||
|
||||
User::factory(9)
|
||||
->create()
|
||||
->each(function (User $u, $key) use ($images) {
|
||||
$u->assignRole(Role::Radiologist);
|
||||
$u->update([
|
||||
'display_name' => sprintf('Dr. %s.%d', $u->first_name, $key + 1),
|
||||
'username' => sprintf('rad%d', $key + 1),
|
||||
'signature_image_path' => 'signatures/' . fake()->randomElement($images),
|
||||
]);
|
||||
});
|
||||
private function copySignatureImages()
|
||||
{
|
||||
$sourcePath = base_path('resources/report/signatures');
|
||||
$dest = storage_path('app/public/signatures');
|
||||
|
||||
User::factory(3)
|
||||
->create()
|
||||
->each(function ($u) {
|
||||
$u->assignRole(Role::Guest);
|
||||
});
|
||||
if (! is_dir($dest)) {
|
||||
mkdir($dest, 0755, true);
|
||||
}
|
||||
|
||||
$files = scandir($sourcePath);
|
||||
foreach ($files as $file) {
|
||||
if (in_array($file, ['.', '..'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
copy("{$sourcePath}/{$file}", "{$dest}/{$file}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 902 B After Width: | Height: | Size: 1.9 KiB |
BIN
resources/imgs/info-green.png
Normal file
BIN
resources/imgs/info-green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 1.2 KiB |
@ -7,7 +7,7 @@
|
||||
<div class="{{ $containerFooter }}">
|
||||
<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">
|
||||
© 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 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
|
||||
|
@ -32,11 +32,23 @@
|
||||
<h5 class="mb-0">Patient Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@include('staff.meta.partials._text', ['name' => 'patient_id', 'label' => 'Patient ID', 'value' => $study->patient_id])
|
||||
@include('staff.meta.partials._text', ['name' => 'patient_name', 'label' => 'Patient Name', 'value' => $study->patient_name])
|
||||
@include('staff.meta.partials._text', ['name' => 'patient_sex', 'label' => 'Sex', 'value' => $study->patient_sex])
|
||||
@include('staff.meta.partials._date', ['name' => 'patient_birthdate', 'label' => 'Birth Date', 'value' => $study->patient_birthdate->toDateString()])
|
||||
@include('staff.meta.partials._text', ['name' => 'accession_number', 'label' => 'Accession number', 'value' => $study->accession_number])
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'patient_id', 'label' => 'Patient ID', 'value' => $study->patient_id])
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'accession_number', 'label' => 'Accession number', 'value' => $study->accession_number])
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'patient_sex', 'label' => 'Sex', 'value' => $study->patient_sex])
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._date', ['name' => 'patient_birthdate', 'label' => 'Birth Date', 'value' => $study->patient_birthdate?->toDateString()])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,10 +59,24 @@
|
||||
<h5 class="mb-0">Study Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@include('staff.meta.partials._text', ['name' => 'study_description', 'label' => 'Study Description', 'value' => $study->study_description])
|
||||
@include('staff.meta.partials._text', ['name' => 'body_part_examined', 'label' => 'Body Part Examined', 'value' => $study->body_part_examined])
|
||||
@include('staff.meta.partials._text', ['name' => 'institution_name', 'label' => 'Institution', 'value' => $study->institution_name])
|
||||
@include('staff.meta.partials._text', ['name' => 'referring_physician_name', 'label' => 'Referring Physician', 'value' => $study->referring_physician_name])
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'study_description', 'label' => 'Study Description', 'value' => $study->study_description])
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'body_part_examined', 'label' => 'Body Part Examined', 'value' => $study->body_part_examined])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'institution_name', 'label' => 'Institution', 'value' => $study->institution_name])
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@include('staff.meta.partials._text', ['name' => 'referring_physician_name', 'label' => 'Referring Physician', 'value' => $study->referring_physician_name])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-check-label">Priority</label>
|
||||
<div class="col my-2">
|
||||
@ -62,7 +88,7 @@
|
||||
|
||||
<label class="form-check form-check-danger mt-4">
|
||||
<input name="cancel_read" type="checkbox" class="form-check-input"/>
|
||||
<span class="form-check-label">Cancel interpretation by radiologist</span>
|
||||
<span class="form-check-label">No interpretation required</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">
|
||||
<label class="form-check-label custom-option-content" for="radio_prelim">
|
||||
<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/>
|
||||
|
||||
<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">
|
||||
<label class="form-check-label custom-option-content" for="radio_final">
|
||||
<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"/>
|
||||
|
||||
<span class="custom-option-body">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<td class="bg-gray-100">{{ $report->created_at }}</td>
|
||||
<td>
|
||||
<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>
|
||||
</a>
|
||||
</td>
|
||||
|
45
update_ver.sh
Executable file
45
update_ver.sh
Executable file
@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define the default increment type (patch if not provided)
|
||||
INCREMENT_TYPE=${1:-patch}
|
||||
|
||||
# Read the current version from .env
|
||||
if ! grep -q "^APP_VERSION=" .env; then
|
||||
echo "APP_VERSION=$(date +'%y.%m')-0.1.0" >> .env
|
||||
fi
|
||||
|
||||
CURRENT_VERSION=$(grep "^APP_VERSION=" .env | cut -d '=' -f2)
|
||||
|
||||
# Extract parts of the version
|
||||
IFS='.-' read -r YY MM MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
||||
|
||||
# Get the current date-based prefix
|
||||
CURRENT_YY=$(date +'%y')
|
||||
CURRENT_MM=$(date +'%m')
|
||||
|
||||
# If the month changes, reset the version
|
||||
if [[ "$YY" != "$CURRENT_YY" || "$MM" != "$CURRENT_MM" ]]; then
|
||||
MAJOR=0
|
||||
MINOR=1
|
||||
PATCH=0
|
||||
else
|
||||
# Increment version based on type
|
||||
if [[ "$INCREMENT_TYPE" == "major" ]]; then
|
||||
((MAJOR++))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
elif [[ "$INCREMENT_TYPE" == "minor" ]]; then
|
||||
((MINOR++))
|
||||
PATCH=0
|
||||
else
|
||||
((PATCH++))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate new version
|
||||
NEW_VERSION="$CURRENT_YY.$CURRENT_MM-$MAJOR.$MINOR.$PATCH"
|
||||
|
||||
# Update the .env file
|
||||
sed -i "s/^APP_VERSION=.*/APP_VERSION=$NEW_VERSION/" .env
|
||||
|
||||
echo "Updated .env APP_VERSION to $NEW_VERSION"
|
Loading…
Reference in New Issue
Block a user