Compare commits
10 Commits
7455709731
...
0bed01ff68
Author | SHA1 | Date | |
---|---|---|---|
0bed01ff68 | |||
db4a6901a4 | |||
3ea8245f85 | |||
96d7d4048e | |||
ed6e462f2c | |||
235bc0b405 | |||
2a2370b3a5 | |||
70b82cd410 | |||
103df6436d | |||
90dce16aea |
@ -17,8 +17,8 @@ ## Installation
|
|||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/masroore/radfusion-pacs.git
|
git clone https://github.com/masroore/pixelbridge-pacs.git
|
||||||
cd radfusion-pacs
|
cd pixelbridge-pacs
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies:**
|
2. **Install dependencies:**
|
||||||
|
@ -6,13 +6,14 @@ enum Role: string
|
|||||||
{
|
{
|
||||||
case Guest = 'guest';
|
case Guest = 'guest';
|
||||||
case Patient = 'patient';
|
case Patient = 'patient';
|
||||||
case Clinician = 'clinician';
|
case Clinician = 'clinic';
|
||||||
case Technician = 'technician';
|
case Technician = 'tech';
|
||||||
case Radiologist = 'radiologist';
|
case Radiologist = 'rad';
|
||||||
case Associate = 'associate';
|
case Reviewer = 'review';
|
||||||
case SystemAgent = 'system_agent';
|
case Associate = 'assoc';
|
||||||
case SiteAdmin = 'site_admin';
|
case SystemAgent = 'sys';
|
||||||
|
case SiteAdmin = 'site-admin';
|
||||||
case Clerk = 'clerk';
|
case Clerk = 'clerk';
|
||||||
case Transcriptionist = 'transcriptionist';
|
case Transcriptionist = 'trans';
|
||||||
case Admin = 'admin';
|
case Admin = 'admin';
|
||||||
}
|
}
|
||||||
|
196
app/Filament/Resources/UserResource.php
Normal file
196
app/Filament/Resources/UserResource.php
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<?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;
|
||||||
|
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([
|
||||||
|
Forms\Components\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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,6 @@
|
|||||||
use App\Http\Requests\AssignPhysicianRequest;
|
use App\Http\Requests\AssignPhysicianRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\AuditTrail\Activity;
|
use App\Services\AuditTrail\Activity;
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class AssignmentController extends HashedStudyControllerBase
|
class AssignmentController extends HashedStudyControllerBase
|
||||||
{
|
{
|
||||||
@ -18,13 +17,17 @@ public function show()
|
|||||||
{
|
{
|
||||||
abort_unless(me()->may(Permission::AssignRadiologist), 403);
|
abort_unless(me()->may(Permission::AssignRadiologist), 403);
|
||||||
$study = $this->getStudy('assignedPhysicians');
|
$study = $this->getStudy('assignedPhysicians');
|
||||||
$rads = User::active()->role(Role::Radiologist)->get(['id', 'display_name', 'profile_photo_path', 'first_name', 'last_name', 'created_at']);
|
$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, ReportStatus::Finalized->value);
|
$stats = Radiologists::worklist_stats(3, ReportStatus::Finalized->value);
|
||||||
foreach ($stats as $rad) {
|
foreach ($stats as $rad) {
|
||||||
$found = $rads->where('id', $rad->id)->first();
|
$found = $rads->where('id', $rad->id)->first();
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$found->info['workload'] = $rad->workload;
|
$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() ?? '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,17 +3,13 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Traits\Active;
|
use App\Models\Traits\Active;
|
||||||
|
use App\Models\Traits\HasOrganization;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class Department extends Model
|
class Department extends Model
|
||||||
{
|
{
|
||||||
use Active;
|
use Active;
|
||||||
|
use HasOrganization;
|
||||||
public function organization(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Organization::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
|
@ -5,14 +5,15 @@
|
|||||||
use App\Domain\ACL\Permission;
|
use App\Domain\ACL\Permission;
|
||||||
use App\Domain\ACL\Role;
|
use App\Domain\ACL\Role;
|
||||||
use App\Models\Traits\Active;
|
use App\Models\Traits\Active;
|
||||||
|
use App\Models\Traits\HasDepartment;
|
||||||
use App\Models\Traits\HashableId;
|
use App\Models\Traits\HashableId;
|
||||||
|
use App\Models\Traits\HasOrganization;
|
||||||
use App\Services\UserService;
|
use App\Services\UserService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Database\Factories\UserFactory;
|
use Database\Factories\UserFactory;
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
@ -30,6 +31,7 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
{
|
{
|
||||||
use Active;
|
use Active;
|
||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
|
use HasDepartment, HasOrganization;
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HashableId;
|
use HashableId;
|
||||||
@ -40,26 +42,7 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
|
|
||||||
public array $info = [];
|
public array $info = [];
|
||||||
|
|
||||||
/**
|
protected $guarded = ['id'];
|
||||||
* 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',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be hidden for serialization.
|
* The attributes that should be hidden for serialization.
|
||||||
@ -159,19 +142,9 @@ public function radiologistProfile(): HasOne
|
|||||||
return $this->hasOne(RadiologistProfile::class);
|
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
|
public function canAccessPanel(Panel $panel): bool
|
||||||
{
|
{
|
||||||
return $this->isAdmin();
|
return $this->isAdmin() && $this->is_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function panels(): HasManyThrough
|
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 = [];
|
||||||
|
|
||||||
|
private static function initCache(): void
|
||||||
|
{
|
||||||
|
if (empty(self::$roles)) {
|
||||||
|
self::$roles = SpatieRole::pluck('id', 'name')->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,22 @@
|
|||||||
|
|
||||||
use App\Services\Pacs\Sync\StudiesSync;
|
use App\Services\Pacs\Sync\StudiesSync;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
final readonly class ScanStudies
|
final readonly class ScanStudies
|
||||||
{
|
{
|
||||||
public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
public function __invoke(StudiesSync $sync, Closure $next): StudiesSync
|
||||||
{
|
{
|
||||||
|
$study_ids = [];
|
||||||
|
try {
|
||||||
$study_ids = $sync->getClient()->getStudiesIds();
|
$study_ids = $sync->getClient()->getStudiesIds();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error($e->getMessage());
|
||||||
|
}
|
||||||
|
if (! empty($study_ids)) {
|
||||||
$sync->setStudyIds($study_ids);
|
$sync->setStudyIds($study_ids);
|
||||||
|
}
|
||||||
|
|
||||||
return $next($sync);
|
return $next($sync);
|
||||||
}
|
}
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
@ -82,7 +82,7 @@ public function run(): void
|
|||||||
'dicom_port' => 4242,
|
'dicom_port' => 4242,
|
||||||
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
|
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
|
||||||
'wado_path' => 'dicom-web',
|
'wado_path' => 'dicom-web',
|
||||||
'ae_title' => 'RADFUSION',
|
'ae_title' => 'BLACKFISH',
|
||||||
'stone_viewer_path' => 'stone-webviewer/index.html',
|
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||||
'ohif_viewer_path' => 'ohif/viewer',
|
'ohif_viewer_path' => 'ohif/viewer',
|
||||||
]
|
]
|
||||||
@ -97,7 +97,52 @@ public function run(): void
|
|||||||
'dicom_port' => 4242,
|
'dicom_port' => 4242,
|
||||||
'rest_api_endpoint' => 'http://helsinki.mylabctg.com:8042/',
|
'rest_api_endpoint' => 'http://helsinki.mylabctg.com:8042/',
|
||||||
'wado_path' => 'dicom-web',
|
'wado_path' => 'dicom-web',
|
||||||
'ae_title' => 'RADFUSION',
|
'ae_title' => 'BLACKFISH',
|
||||||
|
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||||
|
'ohif_viewer_path' => 'ohif/viewer',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
DicomServer::create(
|
||||||
|
[
|
||||||
|
'is_active' => true,
|
||||||
|
'server_name' => 'SGP-1',
|
||||||
|
'geo_code' => 'SG',
|
||||||
|
'host' => 'singapore.mylabctg.com',
|
||||||
|
'http_port' => 8042,
|
||||||
|
'dicom_port' => 4242,
|
||||||
|
'rest_api_endpoint' => 'http://singapore.mylabctg.com:8042/',
|
||||||
|
'wado_path' => 'dicom-web',
|
||||||
|
'ae_title' => 'BLACKFISH',
|
||||||
|
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||||
|
'ohif_viewer_path' => 'ohif/viewer',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
DicomServer::create(
|
||||||
|
[
|
||||||
|
'is_active' => true,
|
||||||
|
'server_name' => 'Texas',
|
||||||
|
'geo_code' => 'US',
|
||||||
|
'host' => 'texas.mylabctg.com',
|
||||||
|
'http_port' => 8042,
|
||||||
|
'dicom_port' => 4242,
|
||||||
|
'rest_api_endpoint' => 'http://texas.mylabctg.com:8042/',
|
||||||
|
'wado_path' => 'dicom-web',
|
||||||
|
'ae_title' => 'BLACKFISH',
|
||||||
|
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||||
|
'ohif_viewer_path' => 'ohif/viewer',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
DicomServer::create(
|
||||||
|
[
|
||||||
|
'is_active' => false,
|
||||||
|
'server_name' => 'San Jose',
|
||||||
|
'geo_code' => 'US',
|
||||||
|
'host' => 'san-jose.mylabctg.com',
|
||||||
|
'http_port' => 8042,
|
||||||
|
'dicom_port' => 4242,
|
||||||
|
'rest_api_endpoint' => 'http://san-jose.mylabctg.com:8042/',
|
||||||
|
'wado_path' => 'dicom-web',
|
||||||
|
'ae_title' => 'BLACKFISH',
|
||||||
'stone_viewer_path' => 'stone-webviewer/index.html',
|
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||||
'ohif_viewer_path' => 'ohif/viewer',
|
'ohif_viewer_path' => 'ohif/viewer',
|
||||||
]
|
]
|
||||||
@ -111,7 +156,7 @@ public function run(): void
|
|||||||
'http_port' => 8043,
|
'http_port' => 8043,
|
||||||
'dicom_port' => 4242,
|
'dicom_port' => 4242,
|
||||||
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
|
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
|
||||||
'ae_title' => 'RADFUSION',
|
'ae_title' => 'BLACKFISH',
|
||||||
'wado_path' => 'dicom-web',
|
'wado_path' => 'dicom-web',
|
||||||
'stone_viewer_path' => 'stone-webviewer/index.html',
|
'stone_viewer_path' => 'stone-webviewer/index.html',
|
||||||
'ohif_viewer_path' => 'ohif/viewer',
|
'ohif_viewer_path' => 'ohif/viewer',
|
||||||
|
Loading…
Reference in New Issue
Block a user