Merge pull request #36 from masroore/main

new rules engine
This commit is contained in:
Masroor Ehsan 2025-01-27 19:25:51 +06:00 committed by GitHub
commit 4a79935e4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 119 additions and 199 deletions

View File

@ -238,11 +238,21 @@ public function getColumns(): array
break; break;
case WorklistColumn::Organization: case WorklistColumn::Organization:
$columns[] = Column::make($col->value)
->searchable(false)
->title('Org');
break;
case WorklistColumn::Department: case WorklistColumn::Department:
$columns[] = Column::make($col->value) $columns[] = Column::make($col->value)
->searchable(true) ->searchable(false)
->orderable(true) ->title('Dept');
->title(formatTitle($col->value)); break;
case WorklistColumn::DicomServer:
$columns[] = Column::make($col->value)
->searchable(false)
->title('Server');
break; break;
default: default:
@ -445,7 +455,9 @@ private function renderCustomColumns(): array
}; };
break; break;
case WorklistColumn::StudyDescription: case WorklistColumn::StudyDescription:
$columns[$col->value] = static fn (Study $study) => $study->sanitizedStudyDescription(); $columns[$col->value] = function (Study $study) {
return sprintf('<span data-bs-toggle="tooltip" data-bs-placement="top" title="%s">%s</span>', $study->study_description, str_limit($study->sanitizedStudyDescription(), 20));
};
break; break;
case WorklistColumn::AssignedPhysician: case WorklistColumn::AssignedPhysician:
$columns[$col->value] = static function (Study $study) { $columns[$col->value] = static function (Study $study) {
@ -500,6 +512,13 @@ private function renderCustomColumns(): array
case WorklistColumn::ReportButtons: case WorklistColumn::ReportButtons:
$columns[$col->value] = fn (Study $study) => $this->generateReportingButtons($study); $columns[$col->value] = fn (Study $study) => $this->generateReportingButtons($study);
break; break;
case WorklistColumn::DicomServer:
$columns[$col->value] = function (Study $study) {
return sprintf('<span class="fi fi-%s msg-icon me-1"></span>%s',
strtolower($study->dicomServer->geo_code),
$study->dicomServer->server_name);
};
break;
} }
} }

View File

@ -2,21 +2,14 @@
namespace App\Models; namespace App\Models;
use App\Domain\Rule\MatchCondition;
use App\Models\Traits\Active; use App\Models\Traits\Active;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
class DicomRoutingRule extends BaseModel class DicomRoutingRule extends BaseModel
{ {
use Active; use Active;
public function conditions(): HasMany
{
return $this->hasMany(DicomRuleCondition::class);
}
public function organization(): BelongsTo public function organization(): BelongsTo
{ {
return $this->belongsTo(Organization::class); return $this->belongsTo(Organization::class);
@ -41,7 +34,6 @@ protected function casts(): array
{ {
return [ return [
'is_active' => 'boolean', 'is_active' => 'boolean',
'match_condition' => MatchCondition::class,
]; ];
} }
} }

View File

@ -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,
];
}
}

View File

@ -36,4 +36,5 @@ enum WorklistColumn: string
case ReportButtons = 'report_buttons'; case ReportButtons = 'report_buttons';
case Organization = 'organization'; case Organization = 'organization';
case Department = 'department'; case Department = 'department';
case DicomServer = 'dicom_server';
} }

View File

@ -45,6 +45,7 @@ public static function worklistColumns(User|int|null $usr = null): Collection
if ($user->isAdmin()) { if ($user->isAdmin()) {
$columns->push(WorklistColumn::Organization); $columns->push(WorklistColumn::Organization);
$columns->push(WorklistColumn::Department); $columns->push(WorklistColumn::Department);
$columns->push(WorklistColumn::DicomServer);
} }
return $columns; return $columns;

View File

@ -8,6 +8,7 @@
use App\Services\Pacs\DicomUtils; use App\Services\Pacs\DicomUtils;
use App\Services\Pacs\OrthancRestClient; use App\Services\Pacs\OrthancRestClient;
use App\Services\StudyRouter\DicomStudyRouter; use App\Services\StudyRouter\DicomStudyRouter;
use App\Services\StudyRouter\RawDicomTags;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Pipeline\Pipeline; use Illuminate\Pipeline\Pipeline;
@ -283,20 +284,21 @@ private function getStudyDicomTags(string $study_uuid): array
// randomly sample few instances for tags collection // 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_rand(array_flip($instances), $this->maxInstances);
$tags = []; $tags = collect();
foreach ($selectedInstances as $instance) { foreach ($selectedInstances as $instance) {
foreach ($this->fetchInstancesTags($instance) as $key => $value) { foreach ($this->fetchInstancesTags($instance) as $key => $value) {
if ($key == 'MainDicomTags' || $key == 'RequestedTags') { if ($key == 'MainDicomTags' || $key == 'RequestedTags') {
foreach ($value as $tag => $val) { foreach ($value as $tag => $val) {
if (! isset($tags[$tag]) || $tags[$tag] !== $val) { $dcmTag = RawDicomTags::tryFrom($tag);
$tags[$tag] = $val; 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 private function setValue(array &$array, string $key, mixed $value): void

View File

@ -3,16 +3,15 @@
namespace App\Services\StudyRouter; namespace App\Services\StudyRouter;
use App\Domain\ACL\Role; use App\Domain\ACL\Role;
use App\Domain\Rule\MatchCondition;
use App\Domain\Rule\MatchMode;
use App\Models\AssignmentPanel; use App\Models\AssignmentPanel;
use App\Models\DicomRoutingRule; use App\Models\DicomRoutingRule;
use App\Models\DicomRuleCondition;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use App\Services\ContentMatcher; use Exception;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
final class DicomStudyRouter final class DicomStudyRouter
{ {
@ -30,22 +29,27 @@ public static function matchStudy(array $dicomHeaders): array
return self::fallbackRouting(); return self::fallbackRouting();
} }
$study = json_decode(json_encode($dicomHeaders)); // convert to object
$expression = new ExpressionLanguage;
foreach (self::$rules as $rule) { foreach (self::$rules as $rule) {
$conditions = $rule->conditions()->orderByDesc('priority')->get(); try {
$matches = $rule->match_condition === MatchCondition::ALL $matches = (bool) $expression->evaluate($rule->condition, ['study' => $study]);
? $conditions->every(fn ($condition) => self::matchCondition($condition, $dicomHeaders)) } catch (Exception $exc) {
: $conditions->contains(fn ($condition) => self::matchCondition($condition, $dicomHeaders)); Log::error('Error evaluating rule expression', [
'rule_id' => $rule->id,
'condition' => $rule->condition,
'error' => $exc->getMessage(),
]);
/* return self::fallbackRouting();
if ($dicomHeaders[RawDicomTags::Modality->value] === 'CR') {
dd($rule, $conditions->toArray(), $matches, $dicomHeaders[RawDicomTags::Modality->value]);
} }
*/
if ($matches) { if ($matches) {
return [ return [
'organization_id' => $rule->organization_id, 'organization_id' => $rule->organization_id,
'department_id' => $rule->department_id, 'department_id' => $rule->department_id,
'rule_id' => $rule->id, 'rule_id' => $rule->id,
'rule_name' => $rule->name,
'radiologists' => self::getRadiologists($rule), 'radiologists' => self::getRadiologists($rule),
]; ];
} }
@ -70,7 +74,6 @@ private static function initialize(): void
self::$rules = Cache::remember('dicom.rules', self::$rules = Cache::remember('dicom.rules',
now()->addMinutes(self::CACHE_TTL), now()->addMinutes(self::CACHE_TTL),
fn () => DicomRoutingRule::active() fn () => DicomRoutingRule::active()
->with('conditions')
->orderByDesc('priority') ->orderByDesc('priority')
->get() ->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 private static function getRadiologists(DicomRoutingRule $rule): array
{ {
if (! is_null($rule->assignment_panel_id)) { if (! is_null($rule->assignment_panel_id)) {

View File

@ -4,62 +4,62 @@
enum RawDicomTags: string enum RawDicomTags: string
{ {
case PatientName = '0010,0010'; // Patient's Name case patient_name = '0010,0010'; // Patient's Name
case PatientID = '0010,0020'; // Patient ID case patient_id = '0010,0020'; // Patient ID
case PatientBirthDate = '0010,0030'; // Patient's Birth Date case patient_birth_date = '0010,0030'; // Patient's Birth Date
case PatientSex = '0010,0040'; // Patient's Sex case patient_sex = '0010,0040'; // Patient's Sex
case StudyInstanceUID = '0020,000D'; // Study Instance UID case study_instance_uid = '0020,000D'; // Study Instance UID
case SeriesInstanceUID = '0020,000E'; // Series Instance UID case series_instance_uid = '0020,000E'; // Series Instance UID
case StudyID = '0020,0010'; // Study ID case study_id = '0020,0010'; // Study ID
case SeriesNumber = '0020,0011'; // Series Number case series_number = '0020,0011'; // Series Number
case InstanceNumber = '0020,0013'; // Instance Number case instance_number = '0020,0013'; // Instance Number
case SOPClassUID = '0008,0016'; // SOP Class UID case sop_class_uid = '0008,0016'; // SOP Class UID
case SOPInstanceUID = '0008,0018'; // SOP Instance UID case sop_instance_uid = '0008,0018'; // SOP Instance UID
case StudyDate = '0008,0020'; // Study Date case study_date = '0008,0020'; // Study Date
case StudyTime = '0008,0030'; // Study Time case study_time = '0008,0030'; // Study Time
case AccessionNumber = '0008,0050'; // Accession Number case accession_number = '0008,0050'; // Accession Number
case Modality = '0008,0060'; // Modality case modality = '0008,0060'; // Modality
case Manufacturer = '0008,0070'; // Manufacturer case manufacturer = '0008,0070'; // Manufacturer
case InstitutionName = '0008,0080'; // Institution Name case institution_name = '0008,0080'; // Institution Name
case ReferringPhysicianName = '0008,0090'; // Referring Physician's Name case referring_physician_name = '0008,0090'; // Referring Physician's Name
case StationName = '0008,1010'; // Station Name case station_name = '0008,1010'; // Station Name
case SeriesDescription = '0008,103E'; // Series Description case series_description = '0008,103E'; // Series Description
case ManufacturerModelName = '0008,1090'; // Manufacturer's Model Name case manufacturer_model_name = '0008,1090'; // Manufacturer's Model Name
case PatientAge = '0010,1010'; // Patient's Age case patient_age = '0010,1010'; // Patient's Age
case PatientWeight = '0010,1030'; // Patient's Weight case patient_weight = '0010,1030'; // Patient's Weight
case BodyPartExamined = '0018,0015'; // Body Part Examined case body_part_examined = '0018,0015'; // Body Part Examined
case ProtocolName = '0018,1030'; // Protocol Name case protocol_name = '0018,1030'; // Protocol Name
case SoftwareVersions = '0018,1020'; // Software Versions case software_versions = '0018,1020'; // Software Versions
case AcquisitionDate = '0008,0022'; // Acquisition Date case acquisition_date = '0008,0022'; // Acquisition Date
case AcquisitionTime = '0008,0032'; // Acquisition Time case acquisition_time = '0008,0032'; // Acquisition Time
case ContentDate = '0008,0023'; // Content Date case content_date = '0008,0023'; // Content Date
case ContentTime = '0008,0033'; // Content Time case content_time = '0008,0033'; // Content Time
case AcquisitionDeviceProcessingDescription = '0018,1400'; // Acquisition Device Processing Description case acquisition_device_processing_description = '0018,1400'; // Acquisition Device Processing Description
case InstitutionAddress = '0008,0081'; // Institution Address case institution_address = '0008,0081'; // Institution Address
case StudyDescription = '0008,1030'; // Study Description case study_description = '0008,1030'; // Study Description
case OperatorsName = '0008,1070'; // Operator's Name case operators_name = '0008,1070'; // Operator's Name
case Private10 = '0029,0010'; // Private Tag 10 case private_10 = '0029,0010'; // Private Tag 10
case IW_Private = '0009,0010'; // IW Private Tag case iw_private = '0009,0010'; // IW Private Tag
case ImageType = '0008,0008'; // Image Type case image_type = '0008,0008'; // Image Type
case PatientOrientation = '0020,0020'; // Patient Orientation case patient_orientation = '0020,0020'; // Patient Orientation
case ImagePositionPatient = '0020,0032'; // Image Position (Patient) case image_position_patient = '0020,0032'; // Image Position (Patient)
case ImageOrientationPatient = '0020,0037'; // Image Orientation (Patient) case image_orientation_patient = '0020,0037'; // Image Orientation (Patient)
case FrameOfReferenceUID = '0020,0052'; // Frame of Reference UID case frame_of_reference_uid = '0020,0052'; // Frame of Reference UID
case PositionReferenceIndicator = '0020,1040'; // Position Reference Indicator case position_reference_indicator = '0020,1040'; // Position Reference Indicator
case SliceLocation = '0020,1041'; // Slice Location case slice_location = '0020,1041'; // Slice Location
case SamplesPerPixel = '0028,0002'; // Samples per Pixel case samples_per_pixel = '0028,0002'; // Samples per Pixel
case PhotometricInterpretation = '0028,0004'; // Photometric Interpretation case photometric_interpretation = '0028,0004'; // Photometric Interpretation
case Rows = '0028,0010'; // Rows case rows = '0028,0010'; // Rows
case Columns = '0028,0011'; // Columns case columns = '0028,0011'; // Columns
case PixelSpacing = '0028,0030'; // Pixel Spacing case pixel_spacing = '0028,0030'; // Pixel Spacing
case BitsAllocated = '0028,0100'; // Bits Allocated case bits_allocated = '0028,0100'; // Bits Allocated
case BitsStored = '0028,0101'; // Bits Stored case bits_stored = '0028,0101'; // Bits Stored
case HighBit = '0028,0102'; // High Bit case high_bit = '0028,0102'; // High Bit
case PixelRepresentation = '0028,0103'; // Pixel Representation case pixel_representation = '0028,0103'; // Pixel Representation
case WindowCenter = '0028,1050'; // Window Center case window_center = '0028,1050'; // Window Center
case WindowWidth = '0028,1051'; // Window Width case window_width = '0028,1051'; // Window Width
case RescaleIntercept = '0028,1052'; // Rescale Intercept case rescale_intercept = '0028,1052'; // Rescale Intercept
case RescaleSlope = '0028,1053'; // Rescale Slope case rescale_slope = '0028,1053'; // Rescale Slope
case InoWave_Private = '0011,1060'; case ino_wave_private = '0011,1060';
case RadFusion_SenderId = '1971,1020'; case rad_fusion_sender_id = '1971,1020';
} }

View File

@ -29,6 +29,7 @@
"sentry/sentry-laravel": "^4.10", "sentry/sentry-laravel": "^4.10",
"spatie/laravel-medialibrary": "^11.11", "spatie/laravel-medialibrary": "^11.11",
"spatie/laravel-permission": "^6.10", "spatie/laravel-permission": "^6.10",
"symfony/expression-language": "^7.2",
"vinkla/hashids": "^12.0", "vinkla/hashids": "^12.0",
"yajra/laravel-datatables": "^11.0" "yajra/laravel-datatables": "^11.0"
}, },

View File

@ -14,6 +14,7 @@ public function up(): void
$table->id(); $table->id();
$table->boolean('is_active')->index(); $table->boolean('is_active')->index();
$table->string('server_name')->unique(); $table->string('server_name')->unique();
$table->string('geo_code', 2)->index();
$table->string('host'); $table->string('host');
$table->integer('port'); $table->integer('port');
$table->string('rest_api_endpoint'); $table->string('rest_api_endpoint');

View File

@ -1,6 +1,5 @@
<?php <?php
use App\Domain\Rule\MatchCondition;
use App\Models\AssignmentPanel; use App\Models\AssignmentPanel;
use App\Models\Department; use App\Models\Department;
use App\Models\Organization; use App\Models\Organization;
@ -21,7 +20,7 @@ public function up(): void
$table->foreignIdFor(User::class)->nullable()->constrained()->nullOnDelete(); $table->foreignIdFor(User::class)->nullable()->constrained()->nullOnDelete();
$table->foreignIdFor(AssignmentPanel::class)->nullable()->constrained()->nullOnDelete(); $table->foreignIdFor(AssignmentPanel::class)->nullable()->constrained()->nullOnDelete();
$table->string('name')->nullable(); $table->string('name')->nullable();
$table->string('match_condition')->default(MatchCondition::ANY->value); $table->string('condition');
$table->unsignedTinyInteger('priority')->default(0); $table->unsignedTinyInteger('priority')->default(0);
$table->timestamps(); $table->timestamps();

View File

@ -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');
}
};

View File

@ -2,14 +2,10 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Domain\Rule\MatchCondition;
use App\Domain\Rule\MatchMode;
use App\Models\Department; use App\Models\Department;
use App\Models\DicomRoutingRule; use App\Models\DicomRoutingRule;
use App\Models\DicomRuleCondition;
use App\Models\DicomServer; use App\Models\DicomServer;
use App\Models\Organization; use App\Models\Organization;
use App\Services\StudyRouter\RawDicomTags;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class OrganizationSeeder extends Seeder class OrganizationSeeder extends Seeder
@ -36,14 +32,14 @@ public function run(): void
$dept_chev_xr = Department::create( $dept_chev_xr = Department::create(
[ [
'is_active' => true, 'is_active' => true,
'name' => 'Chevron XR', 'name' => 'Chev-CR',
'organization_id' => $chev->id, 'organization_id' => $chev->id,
] ]
); );
$chev_dep_ct_mr = Department::create( $chev_dep_ct_mr = Department::create(
[ [
'is_active' => true, 'is_active' => true,
'name' => 'Chevron CT/MR', 'name' => 'Chev-MR',
'organization_id' => $chev->id, 'organization_id' => $chev->id,
] ]
); );
@ -61,7 +57,7 @@ public function run(): void
'name' => 'Chevron MR/CT', 'name' => 'Chevron MR/CT',
'organization_id' => $chev->id, 'organization_id' => $chev->id,
'department_id' => $chev_dep_ct_mr->id, 'department_id' => $chev_dep_ct_mr->id,
'match_condition' => MatchCondition::ALL->value, 'condition' => '(study.institution_name ?? "" starts with "chevron") and (study.modality not in ["CR", "DX", "MG"])',
] ]
); );
@ -71,45 +67,16 @@ public function run(): void
'name' => 'Chevron X-ray', 'name' => 'Chevron X-ray',
'organization_id' => $chev->id, 'organization_id' => $chev->id,
'department_id' => $dept_chev_xr->id, 'department_id' => $dept_chev_xr->id,
'match_condition' => MatchCondition::ALL->value, 'condition' => '(study.institution_name ?? "" starts with "chevron") and (study.modality in ["CR", "DX", "MG"])',
'priority' => 10, 'priority' => 99,
] ]
); );
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( DicomServer::create(
[ [
'is_active' => true, 'is_active' => true,
'server_name' => 'Orthanc Main', 'server_name' => 'CTG-1',
'geo_code' => 'BD',
'host' => 'pacs.mylabctg.com', 'host' => 'pacs.mylabctg.com',
'port' => 8042, 'port' => 8042,
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/', 'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
@ -122,8 +89,9 @@ public function run(): void
DicomServer::create( DicomServer::create(
[ [
'is_active' => false, 'is_active' => false,
'server_name' => 'Orthanc XR', 'server_name' => 'MAA-1',
'host' => 'pacs.mylabctg.com', 'host' => 'pacs.mylabctg.com',
'geo_code' => 'IN',
'port' => 8043, 'port' => 8043,
'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/', 'rest_api_endpoint' => 'http://pacs.mylabctg.com:8042/',
'ae_title' => 'RADFUSION', 'ae_title' => 'RADFUSION',

View File

@ -27,6 +27,8 @@
'resources/assets/vendor/libs/bootstrap-datepicker/bootstrap-datepicker.scss', 'resources/assets/vendor/libs/bootstrap-datepicker/bootstrap-datepicker.scss',
'resources/assets/vendor/libs/bootstrap-daterangepicker/bootstrap-daterangepicker.scss', 'resources/assets/vendor/libs/bootstrap-daterangepicker/bootstrap-daterangepicker.scss',
]) ])
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/7.2.3/css/flag-icons.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
@endsection @endsection
@section('vendor-script') @section('vendor-script')