From a2ff8f9609f59c6f2cfd11124aff0b2f33871b52 Mon Sep 17 00:00:00 2001 From: Timo Schwarzer Date: Wed, 26 Jun 2024 16:42:11 +0200 Subject: [PATCH 001/661] Add Department Manager to single and multiple user views --- app/Http/Transformers/UsersTransformer.php | 4 + app/Presenters/UserPresenter.php | 9 ++ resources/lang/en-US/admin/users/general.php | 1 + resources/views/users/view.blade.php | 98 +++++++++++--------- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/app/Http/Transformers/UsersTransformer.php b/app/Http/Transformers/UsersTransformer.php index 64752d0445..8b3b94b64d 100644 --- a/app/Http/Transformers/UsersTransformer.php +++ b/app/Http/Transformers/UsersTransformer.php @@ -50,6 +50,10 @@ class UsersTransformer 'id' => (int) $user->department->id, 'name'=> e($user->department->name), ] : null, + 'department_manager' => ($user->department?->manager) ? [ + 'id' => (int) $user->department->manager->id, + 'name'=> e($user->department->manager->full_name), + ] : null, 'location' => ($user->userloc) ? [ 'id' => (int) $user->userloc->id, 'name'=> e($user->userloc->name), diff --git a/app/Presenters/UserPresenter.php b/app/Presenters/UserPresenter.php index a5b99adb14..4b26f95f40 100644 --- a/app/Presenters/UserPresenter.php +++ b/app/Presenters/UserPresenter.php @@ -197,6 +197,15 @@ class UserPresenter extends Presenter 'visible' => true, 'formatter' => 'departmentsLinkObjFormatter', ], + [ + 'field' => 'department_manager', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('admin/users/general.department_manager'), + 'visible' => true, + 'formatter' => 'usersLinkObjFormatter', + ], [ 'field' => 'location', 'searchable' => true, diff --git a/resources/lang/en-US/admin/users/general.php b/resources/lang/en-US/admin/users/general.php index b097ccec69..9c1042d44d 100644 --- a/resources/lang/en-US/admin/users/general.php +++ b/resources/lang/en-US/admin/users/general.php @@ -51,4 +51,5 @@ return [ 'next_save_user' => 'Next: Save User', 'all_assigned_list_generation' => 'Generated on:', 'email_user_creds_on_create' => 'Email this user their credentials?', + 'department_manager' => 'Department Manager', ]; diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index 00bfc84df8..b090ca3dd8 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -51,7 +51,7 @@ + @@ -117,7 +117,7 @@ - + @@ -148,7 +148,7 @@
- + @if ($user->deleted_at!='')
@@ -161,22 +161,22 @@
- +
- + @if (($user->isSuperUser()) || ($user->hasAccess('admin')))
{{ ($user->isSuperUser()) ? strtolower(trans('general.superuser')) : strtolower(trans('general.admin')) }}
@endif - +
- {{ $user->present()->fullName() }} + {{ $user->present()->fullName() }}
- - + + @can('update', $user)
@@ -192,7 +192,7 @@ @can('view', $user)
- @if($user->allAssignedCount() != '0') + @if($user->allAssignedCount() != '0') {{ trans('admin/users/general.print_assigned') }} @else @@ -224,7 +224,7 @@ @else - + @endif
@endif @@ -264,16 +264,16 @@ @endcan

- + - +
- +
- +
{{ trans('admin/users/table.name') }}
@@ -283,7 +283,7 @@
- + @if (!is_null($user->company)) @@ -297,7 +297,7 @@
- + @endif @@ -326,7 +326,7 @@ {{ trans('general.address') }}
- + @if ($user->address) {{ $user->address }}
@endif @@ -418,7 +418,7 @@
{{ $user->employee_num }}
- +
@endif @@ -439,7 +439,7 @@ @endif - + @if ($user->email)
@@ -500,17 +500,29 @@ @if ($user->department) - -
-
- {{ trans('general.department') }} + +
+
+ {{ trans('general.department') }} +
+
- -
+ @if($user->department->manager) +
+
+ {{ trans('admin/users/general.department_manager') }} +
+ +
+ @endif @endif @if ($user->created_at) @@ -544,8 +556,8 @@
{!! ($user->vip=='1') ? ' '.trans('general.yes') : ' '.trans('general.no') !!}
-
- +
+
@@ -596,12 +608,12 @@ {{ trans('admin/users/general.two_factor_active') }}
- + {!! ($user->two_factor_active()) ? ' '.trans('general.yes') : ' '.trans('general.no') !!} - +
- +
@@ -612,16 +624,16 @@
- + @if ((Auth::user()->isSuperUser()) && ($user->two_factor_active_and_enrolled()) && ($snipeSettings->two_factor_enabled!='0') && ($snipeSettings->two_factor_enabled!='')) - +
- + {{ trans('admin/settings/general.two_factor_reset') }} @@ -633,13 +645,13 @@

{{ trans('admin/settings/general.two_factor_reset_help') }}

- - + +
- @endif + @endif @endif - + @if ($user->notes) @@ -1205,4 +1217,4 @@ $(function () { -@stop \ No newline at end of file +@stop From 38efc6290036f95aaacc2e13d40e071e8dc8a42b Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 6 Mar 2025 16:01:46 +0000 Subject: [PATCH 002/661] Add index on action_date, copy from created_at Signed-off-by: snipe --- app/Models/Actionlog.php | 6 ++++ ...eated_at_to_action_date_in_action_logs.php | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 database/migrations/2025_03_06_152922_copy_created_at_to_action_date_in_action_logs.php diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index a14910208e..67f30701c3 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -93,7 +93,13 @@ class Actionlog extends SnipeModel } elseif (auth()->user() && auth()->user()->company) { $actionlog->company_id = auth()->user()->company_id; } + + if ($actionlog->action_date == '') { + $actionlog->action_date = Carbon::now(); + } + }); + } diff --git a/database/migrations/2025_03_06_152922_copy_created_at_to_action_date_in_action_logs.php b/database/migrations/2025_03_06_152922_copy_created_at_to_action_date_in_action_logs.php new file mode 100644 index 0000000000..8ce7de13bd --- /dev/null +++ b/database/migrations/2025_03_06_152922_copy_created_at_to_action_date_in_action_logs.php @@ -0,0 +1,31 @@ +index(['action_date']); + }); + + DB::update('update '.DB::getTablePrefix().'action_logs set action_date = created_at where created_at IS NOT NULL and action_date IS NULL'); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + + } +}; From 4e3df93349f3b1894ccb7b3eb741b087be7efea0 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 6 Mar 2025 16:16:17 +0000 Subject: [PATCH 003/661] Change action_date display to date from datetime Signed-off-by: snipe --- app/Http/Transformers/ActionlogsTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Transformers/ActionlogsTransformer.php b/app/Http/Transformers/ActionlogsTransformer.php index 702ea123d8..04a7861d40 100644 --- a/app/Http/Transformers/ActionlogsTransformer.php +++ b/app/Http/Transformers/ActionlogsTransformer.php @@ -201,7 +201,7 @@ class ActionlogsTransformer 'remote_ip' => ($actionlog->remote_ip) ?? null, 'user_agent' => ($actionlog->user_agent) ?? null, 'action_source' => ($actionlog->action_source) ?? null, - 'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'), + 'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'date'): Helper::getFormattedDateObject($actionlog->created_at, 'date'), ]; // Log::info("Clean Meta is: ".print_r($clean_meta,true)); From d4dc8d2b79021e17843d730271e0a8dd5e0cb6f6 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 6 Mar 2025 17:43:07 +0000 Subject: [PATCH 004/661] Remove action_date from loggable as a changed field Signed-off-by: snipe --- app/Models/Loggable.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/Models/Loggable.php b/app/Models/Loggable.php index 1e4a912027..f2fdd192f8 100644 --- a/app/Models/Loggable.php +++ b/app/Models/Loggable.php @@ -89,13 +89,10 @@ trait Loggable $log->note = $note; $log->action_date = $action_date; - if (! $log->action_date) { - $log->action_date = date('Y-m-d H:i:s'); - } $changed = []; $array_to_flip = array_keys($fields_array); - $array_to_flip = array_merge($array_to_flip, ['action_date','name','status_id','location_id','expected_checkin']); + $array_to_flip = array_merge($array_to_flip, ['name','status_id','location_id','expected_checkin']); $originalValues = array_intersect_key($originalValues, array_flip($array_to_flip)); @@ -191,7 +188,7 @@ trait Loggable $changed = []; $array_to_flip = array_keys($fields_array); - $array_to_flip = array_merge($array_to_flip, ['action_date','name','status_id','location_id','expected_checkin']); + $array_to_flip = array_merge($array_to_flip, ['name','status_id','location_id','expected_checkin']); $originalValues = array_intersect_key($originalValues, array_flip($array_to_flip)); From 80a69bfe90bb42b43c8af2bc966abc9cbbe9a26b Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 6 Mar 2025 18:09:27 +0000 Subject: [PATCH 005/661] Revert datetime to date Signed-off-by: snipe --- app/Http/Transformers/ActionlogsTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Transformers/ActionlogsTransformer.php b/app/Http/Transformers/ActionlogsTransformer.php index 04a7861d40..702ea123d8 100644 --- a/app/Http/Transformers/ActionlogsTransformer.php +++ b/app/Http/Transformers/ActionlogsTransformer.php @@ -201,7 +201,7 @@ class ActionlogsTransformer 'remote_ip' => ($actionlog->remote_ip) ?? null, 'user_agent' => ($actionlog->user_agent) ?? null, 'action_source' => ($actionlog->action_source) ?? null, - 'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'date'): Helper::getFormattedDateObject($actionlog->created_at, 'date'), + 'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'), ]; // Log::info("Clean Meta is: ".print_r($clean_meta,true)); From c825878c46d9981b9d8048912853c006d71bf5aa Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 10:57:33 +0000 Subject: [PATCH 006/661] Added history presenter Signed-off-by: snipe --- app/Presenters/HistoryPresenter.php | 146 ++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 app/Presenters/HistoryPresenter.php diff --git a/app/Presenters/HistoryPresenter.php b/app/Presenters/HistoryPresenter.php new file mode 100644 index 0000000000..543d840bc9 --- /dev/null +++ b/app/Presenters/HistoryPresenter.php @@ -0,0 +1,146 @@ + 'icon', + 'searchable' => false, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('admin/hardware/table.icon'), + 'visible' => true, + 'class' => 'hidden-xs', + 'formatter' => 'iconFormatter', + ], + [ + 'field' => 'created_at', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.created_at'), + 'visible' => false, + 'formatter' => 'dateDisplayFormatter', + ], + [ + 'field' => 'created_by', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.created_by'), + 'visible' => false, + 'formatter' => 'usersLinkObjFormatter', + ], + [ + 'field' => 'action_date', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.action_date'), + 'visible' => false, + 'formatter' => 'dateDisplayFormatter', + ], + [ + 'field' => 'action_type', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.action'), + 'visible' => false, + ], + [ + 'field' => 'item', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.item'), + 'visible' => false, + 'formatter' => 'polymorphicItemFormatter', + ], [ + 'field' => 'target', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.target'), + 'visible' => false, + 'formatter' => 'polymorphicItemFormatter', + ], + [ + 'field' => 'file', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.file_name'), + 'visible' => false, + 'formatter' => 'fileUploadNameFormatter', + ], + [ + 'field' => 'file_download', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.download'), + 'visible' => false, + 'formatter' => 'fileUploadFormatter', + ], + [ + 'field' => 'note', + 'searchable' => true, + 'sortable' => true, + 'visible' => false, + 'title' => trans('general.notes'), + 'formatter' => 'notesFormatter' + ], + [ + 'field' => 'signature_file', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.signature'), + 'visible' => false, + 'formatter' => 'imageFormatter', + ], + [ + 'field' => 'log_meta', + 'searchable' => false, + 'sortable' => false, + 'visible' => true, + 'title' => trans('admin/hardware/table.changed'), + 'formatter' => 'changeLogFormatter', + ], + [ + 'field' => 'remote_ip', + 'searchable' => false, + 'sortable' => false, + 'visible' => true, + 'title' => trans('admin/settings/general.login_ip'), + ], + [ + 'field' => 'user_agent', + 'searchable' => false, + 'sortable' => false, + 'visible' => true, + 'title' => trans('admin/settings/general.login_user_agent'), + ], + [ + 'field' => 'action_source', + 'searchable' => false, + 'sortable' => false, + 'visible' => true, + 'title' => trans('general.action_source'), + ], + ]; + + return json_encode($layout); + } + +} \ No newline at end of file From 55694fa2fc94deb6a17740673fdc92fe4887ad07 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 10:57:40 +0000 Subject: [PATCH 007/661] Added strings Signed-off-by: snipe --- resources/lang/en-US/general.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 8366a68216..3c5de482dc 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -8,9 +8,11 @@ return [ 'accessory' => 'Accessory', 'accessory_report' => 'Accessory Report', 'action' => 'Action', + 'action_date' => 'Action Date', 'activity_report' => 'Activity Report', 'address' => 'Address', 'admin' => 'Admin Settings', + 'admin_user' => 'Admin User', 'admin_tooltip' => 'This user has admin privileges', 'superuser' => 'Superuser', 'superuser_tooltip' => 'This user has superuser privileges', From 7ba361b10df12fda099bd40b04a12b142b4f7740 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 10:57:54 +0000 Subject: [PATCH 008/661] Use date formatter for filestable Signed-off-by: snipe --- resources/views/blade/filestable.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/blade/filestable.blade.php b/resources/views/blade/filestable.blade.php index e198e339da..1c9aa72175 100644 --- a/resources/views/blade/filestable.blade.php +++ b/resources/views/blade/filestable.blade.php @@ -120,7 +120,7 @@ @endif - {{ $file->created_at }} + {{ $file->created_at ? Helper::getFormattedDateObject($file->created_at, 'datetime', false) : '' }} {{ ($file->adminuser) ? $file->adminuser->present()->getFullNameAttribute() : '' }} From cb7822576fb7277d89492d8143d4ad51ab604f7c Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 11:48:19 +0000 Subject: [PATCH 009/661] Use new presenters Signed-off-by: snipe --- resources/views/accessories/view.blade.php | 34 ++++++++++++++++------ resources/views/consumables/view.blade.php | 21 ++----------- resources/views/hardware/view.blade.php | 19 +----------- 3 files changed, 28 insertions(+), 46 deletions(-) diff --git a/resources/views/accessories/view.blade.php b/resources/views/accessories/view.blade.php index 8042b4ec51..c7f3ddc7a1 100644 --- a/resources/views/accessories/view.blade.php +++ b/resources/views/accessories/view.blade.php @@ -106,6 +106,7 @@
- - - - - - - - + + + + + + + @if ($snipeSettings->require_accept_signature=='1') - + @endif diff --git a/resources/views/consumables/view.blade.php b/resources/views/consumables/view.blade.php index ac95e6391b..0a340765bf 100644 --- a/resources/views/consumables/view.blade.php +++ b/resources/views/consumables/view.blade.php @@ -446,6 +446,7 @@
{{ trans('general.record_created') }}{{ trans('general.admin') }}{{ trans('general.action') }}{{ trans('general.file_name') }}{{ trans('general.item') }}{{ trans('general.target') }}{{ trans('general.notes') }}{{ trans('general.date') }} + {{ trans('general.date') }} + + {{ trans('general.admin_user') }} + + {{ trans('general.action') }} + + {{ trans('general.file_name') }} + + {{ trans('general.item') }} + + {{ trans('general.target') }} + + {{ trans('general.notes') }} + {{ trans('general.signature') }} + {{ trans('general.signature') }} +
- - - - - - - - - - - - - - - - - -
{{ trans('general.date') }}{{ trans('general.admin') }}{{ trans('general.action') }}{{ trans('general.file_name') }}{{ trans('general.item') }}{{ trans('general.target') }}{{ trans('general.notes') }}{{ trans('general.signature') }}{{ trans('general.download') }}{{ trans('admin/hardware/table.changed')}}{{ trans('admin/settings/general.login_ip') }}{{ trans('admin/settings/general.login_user_agent') }}{{ trans('general.action_source') }}
diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index a59b0534b2..dc37e8a62b 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -1389,6 +1389,7 @@
- - - - - - - - - - - - - - - - - -
{{ trans('general.date') }}{{ trans('general.admin') }}{{ trans('general.action') }}{{ trans('general.file_name') }}{{ trans('general.item') }}{{ trans('general.target') }}{{ trans('general.notes') }}{{ trans('general.signature') }}{{ trans('general.download') }}{{ trans('admin/hardware/table.changed')}}{{ trans('admin/settings/general.login_ip') }}{{ trans('admin/settings/general.login_user_agent') }}{{ trans('general.action_source') }}
From ef56177372afedf0b0e3243205d23843afb99d23 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 11:48:31 +0000 Subject: [PATCH 010/661] Use presenter Signed-off-by: snipe --- resources/views/licenses/view.blade.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/resources/views/licenses/view.blade.php b/resources/views/licenses/view.blade.php index 2de0f9d6e0..b827e34217 100755 --- a/resources/views/licenses/view.blade.php +++ b/resources/views/licenses/view.blade.php @@ -479,6 +479,7 @@
- - - - - - - - - - - - @if ($snipeSettings->require_accept_signature=='1') - - @endif - -
{{ trans('general.record_created') }}{{ trans('general.admin') }}{{ trans('general.action') }}{{ trans('general.file_name') }}{{ trans('general.item') }}{{ trans('general.target') }}{{ trans('general.notes') }}{{ trans('general.date') }}{{ trans('general.signature') }}
From 92b2da9b1b8c59b0f6bd0b8619a548f739c4dae8 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 11:48:38 +0000 Subject: [PATCH 011/661] Added history tab to components Signed-off-by: snipe --- resources/views/components/view.blade.php | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/resources/views/components/view.blade.php b/resources/views/components/view.blade.php index f090b95665..db059641a2 100644 --- a/resources/views/components/view.blade.php +++ b/resources/views/components/view.blade.php @@ -67,6 +67,16 @@ +
  • + + + + +
  • @can('components.files', $component)
  • @@ -137,6 +147,35 @@
  • +
    +
    + + +
    +
    +
    + @can('components.files', $component)
    From df5437647b40c422a918696c26006c17f107d2df Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 12:43:38 +0000 Subject: [PATCH 012/661] Add optional serial value in presenter Signed-off-by: snipe --- app/Presenters/HistoryPresenter.php | 34 +++++++++++++-- resources/views/reports/activity.blade.php | 51 +--------------------- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/app/Presenters/HistoryPresenter.php b/app/Presenters/HistoryPresenter.php index 543d840bc9..3b90dff31a 100644 --- a/app/Presenters/HistoryPresenter.php +++ b/app/Presenters/HistoryPresenter.php @@ -11,9 +11,19 @@ class HistoryPresenter extends Presenter * Json Column Layout for bootstrap table * @return string */ - public static function dataTableLayout() + public static function dataTableLayout($serial = false) { - $layout = [ + $extra = []; + $layout_start = [ + [ + 'id' => 'id', + 'searchable' => false, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.id'), + 'visible' => false, + 'class' => 'hidden-xs', + ], [ 'field' => 'icon', 'searchable' => false, @@ -65,7 +75,22 @@ class HistoryPresenter extends Presenter 'title' => trans('general.item'), 'visible' => false, 'formatter' => 'polymorphicItemFormatter', - ], [ + ], + ]; + + + if ($serial) { + $extra = [ + [ + 'field' => 'item.serial', + 'title' => trans('admin/hardware/table.serial'), + 'visible' => false, + ] + ]; + } + + $layout_end = [ + [ 'field' => 'target', 'searchable' => true, 'sortable' => true, @@ -140,7 +165,8 @@ class HistoryPresenter extends Presenter ], ]; - return json_encode($layout); + $merged = array_merge($layout_start, $extra, $layout_end); + return json_encode($merged); } } \ No newline at end of file diff --git a/resources/views/reports/activity.blade.php b/resources/views/reports/activity.blade.php index c8f789ec76..f671e114a8 100644 --- a/resources/views/reports/activity.blade.php +++ b/resources/views/reports/activity.blade.php @@ -25,6 +25,7 @@
    - - - - - - - - - - - - - - - - - - - -
    - {{ trans('general.date') }} - - {{ trans('general.admin') }} - - {{ trans('general.action') }} - - {{ trans('general.file_name') }} - - {{ trans('general.type') }} - - {{ trans('admin/hardware/table.serial') }} - - {{ trans('general.item') }} - - {{ trans('general.to') }} - - {{ trans('general.notes') }} - - {{ trans('general.changed') }} - - {{ trans('admin/settings/general.login_ip') }} - - {{ trans('admin/settings/general.login_user_agent') }} - - {{ trans('general.action_source') }} -
    From 220537fbfb8ef14dfe70aee42c3ef0ee76e56d19 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 10 Mar 2025 12:59:57 +0000 Subject: [PATCH 013/661] Updated presenter name Signed-off-by: snipe --- resources/views/accessories/view.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/accessories/view.blade.php b/resources/views/accessories/view.blade.php index c7f3ddc7a1..9119f6ddfa 100644 --- a/resources/views/accessories/view.blade.php +++ b/resources/views/accessories/view.blade.php @@ -106,7 +106,7 @@
    Date: Mon, 10 Mar 2025 13:05:31 +0000 Subject: [PATCH 014/661] Removed extra headers Signed-off-by: snipe --- resources/views/accessories/view.blade.php | 33 +--------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/resources/views/accessories/view.blade.php b/resources/views/accessories/view.blade.php index 9119f6ddfa..fc86bbd3c0 100644 --- a/resources/views/accessories/view.blade.php +++ b/resources/views/accessories/view.blade.php @@ -121,38 +121,7 @@ "fileName": "export-{{ str_slug($accessory->name) }}-history-{{ date('Y-m-d') }}", "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"] }' - data-url="{{ route('api.activity.index', ['item_id' => $accessory->id, 'item_type' => 'accessory']) }}"> - - - - - - - - - - - @if ($snipeSettings->require_accept_signature=='1') - - @endif - - + data-url="{{ route('api.activity.index', ['item_id' => $accessory->id, 'item_type' => 'accessory']) }}">
    - {{ trans('general.date') }} - - {{ trans('general.admin_user') }} - - {{ trans('general.action') }} - - {{ trans('general.file_name') }} - - {{ trans('general.item') }} - - {{ trans('general.target') }} - - {{ trans('general.notes') }} - - {{ trans('general.signature') }} -
    From 9a3ac41370e9b288bb5f3d99def9d115e317be1e Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 9 Apr 2025 11:02:45 -0700 Subject: [PATCH 015/661] add default location as a fallback to asset validation --- app/Http/Controllers/Assets/AssetsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 8356899777..b6b19eea91 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -190,10 +190,10 @@ class AssetsController extends Controller if ($asset->isValid() && $asset->save()) { if (request('assigned_user')) { $target = User::find(request('assigned_user')); - $location = $target->location_id; + $location = $target->location_id ?? $target->rtd_location_id; } elseif (request('assigned_asset')) { $target = Asset::find(request('assigned_asset')); - $location = $target->location_id; + $location = $target->location_id ?? $target->rtd_location_id; } elseif (request('assigned_location')) { $target = Location::find(request('assigned_location')); $location = $target->id; From 3e980a4c5790a776481058f86c2941c04b8c698d Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 9 Apr 2025 11:11:59 -0700 Subject: [PATCH 016/661] set location if target is set --- app/Http/Controllers/Assets/AssetsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index b6b19eea91..0e44fd4705 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -190,10 +190,10 @@ class AssetsController extends Controller if ($asset->isValid() && $asset->save()) { if (request('assigned_user')) { $target = User::find(request('assigned_user')); - $location = $target->location_id ?? $target->rtd_location_id; + $location = $target?->location_id; } elseif (request('assigned_asset')) { $target = Asset::find(request('assigned_asset')); - $location = $target->location_id ?? $target->rtd_location_id; + $location = $target?->location_id; } elseif (request('assigned_location')) { $target = Location::find(request('assigned_location')); $location = $target->id; From 100db23210571e22a7cf1987be4461d627d759a2 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 9 Apr 2025 11:54:59 -0700 Subject: [PATCH 017/661] add checks that the target is not null, and redirects back with error messages --- .../Controllers/Assets/AssetsController.php | 33 ++++++++++++++----- .../lang/en-US/admin/hardware/message.php | 5 +++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 0e44fd4705..3a9dc58c82 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -188,14 +188,31 @@ class AssetsController extends Controller // Validate the asset before saving if ($asset->isValid() && $asset->save()) { - if (request('assigned_user')) { - $target = User::find(request('assigned_user')); - $location = $target?->location_id; - } elseif (request('assigned_asset')) { - $target = Asset::find(request('assigned_asset')); - $location = $target?->location_id; - } elseif (request('assigned_location')) { - $target = Location::find(request('assigned_location')); + $target = null; + $location = null; + + if ($userId = request('assigned_user')) { + $target = User::find($userId); + + if (!$target) { + return redirect()->back()->with('error', trans('admin/hardware/message.create.target_not_found.user')); + } + $location = $target->location_id; + + } elseif ($assetId = request('assigned_asset')) { + $target = Asset::find($assetId); + + if (!$target) { + return redirect()->back()->with('error', trans('admin/hardware/message.create.target_not_found.asset')); + } + $location = $target->location_id; + + } elseif ($locationId = request('assigned_location')) { + $target = Location::find($locationId); + + if (!$target) { + return redirect()->back()->with('error', trans('admin/hardware/message.create.target_not_found.location')); + } $location = $target->id; } diff --git a/resources/lang/en-US/admin/hardware/message.php b/resources/lang/en-US/admin/hardware/message.php index c19bc63647..4421fadaee 100644 --- a/resources/lang/en-US/admin/hardware/message.php +++ b/resources/lang/en-US/admin/hardware/message.php @@ -19,6 +19,11 @@ return [ 'success_linked' => 'Asset with tag :tag was created successfully. Click here to view.', 'multi_success_linked' => 'Asset with tag :links was created successfully.|:count assets were created succesfully. :links.', 'partial_failure' => 'An asset was unable to be created. Reason: :failures|:count assets were unable to be created. Reasons: :failures', + 'target_not_found' => [ + 'user' => 'The assigned user could not be found.', + 'asset' => 'The assigned asset could not be found.', + 'location' => 'The assigned location could not be found.', + ], ], 'update' => [ From 1b961346f0c43a54d0729bf001523287ed47d3cc Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 9 Apr 2025 12:11:31 -0700 Subject: [PATCH 018/661] added withInput to the redirects --- app/Http/Controllers/Assets/AssetsController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 3a9dc58c82..33c19ce868 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -190,12 +190,12 @@ class AssetsController extends Controller if ($asset->isValid() && $asset->save()) { $target = null; $location = null; - + request()->merge(['assigned_asset' => -1]); if ($userId = request('assigned_user')) { $target = User::find($userId); if (!$target) { - return redirect()->back()->with('error', trans('admin/hardware/message.create.target_not_found.user')); + return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.user')); } $location = $target->location_id; @@ -203,7 +203,7 @@ class AssetsController extends Controller $target = Asset::find($assetId); if (!$target) { - return redirect()->back()->with('error', trans('admin/hardware/message.create.target_not_found.asset')); + return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.asset')); } $location = $target->location_id; @@ -211,7 +211,7 @@ class AssetsController extends Controller $target = Location::find($locationId); if (!$target) { - return redirect()->back()->with('error', trans('admin/hardware/message.create.target_not_found.location')); + return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.location')); } $location = $target->id; } From c385b4a0821dedf677e0cd4bbfa34619203d32b4 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 9 Apr 2025 12:11:46 -0700 Subject: [PATCH 019/661] remove testing lines --- app/Http/Controllers/Assets/AssetsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 33c19ce868..23c4e044eb 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -190,7 +190,7 @@ class AssetsController extends Controller if ($asset->isValid() && $asset->save()) { $target = null; $location = null; - request()->merge(['assigned_asset' => -1]); + if ($userId = request('assigned_user')) { $target = User::find($userId); From 2c141579dd0993938a201db143629a7d73dd4142 Mon Sep 17 00:00:00 2001 From: Chris Olin Date: Fri, 2 May 2025 07:14:47 -0400 Subject: [PATCH 020/661] adds support for GenericTape label printers, includes class for 53mm tape printer --- .../Labels/Tapes/Generic/GenericTape.php | 103 ++++++++++++++++++ app/Models/Labels/Tapes/Generic/Tape_53mm.php | 32 ++++++ .../Labels/Tapes/Generic/Tape_53mm_A.php | 77 +++++++++++++ .../Labels/Tapes/Generic/Tape_53mm_B.php | 78 +++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 app/Models/Labels/Tapes/Generic/GenericTape.php create mode 100644 app/Models/Labels/Tapes/Generic/Tape_53mm.php create mode 100644 app/Models/Labels/Tapes/Generic/Tape_53mm_A.php create mode 100644 app/Models/Labels/Tapes/Generic/Tape_53mm_B.php diff --git a/app/Models/Labels/Tapes/Generic/GenericTape.php b/app/Models/Labels/Tapes/Generic/GenericTape.php new file mode 100644 index 0000000000..3825d54e2e --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/GenericTape.php @@ -0,0 +1,103 @@ +width = $width; + $this->height = $height; + $this->continuous = $continuous; + $this->spacing = $spacing; + + // Calculate base font size (7% of tape width) + $baseFontSize = static::TAPE_WIDTH * 0.07; + + // Calculate margin (4% of tape width) + $margin = static::TAPE_WIDTH * 0.04; + + // Set margins + $this->marginTop = $margin; + $this->marginBottom = $margin; + $this->marginLeft = $margin; + $this->marginRight = $margin; + + // Calculate and set element sizing based on base font size + $this->titleSize = $baseFontSize; // Same as base font size + $this->titleMargin = $baseFontSize * 0.3; // 30% of base font size + $this->fieldSize = $baseFontSize * 1.1; // 110% of base font size + $this->fieldMargin = $baseFontSize * 0.1; // 10% of base font size + $this->labelSize = $baseFontSize * 0.7; // 70% of base font size + $this->labelMargin = $baseFontSize * -0.1; // -10% of base font size + $this->barcodeMargin = $baseFontSize * 0.5; // 50% of base font size + $this->tagSize = $baseFontSize * 0.8; // 80% of base font size + } + + // Unit of measurement + public function getUnit() { return 'mm'; } + + // Label dimensions + public function getWidth() { return $this->width; } + public function getHeight() { return $this->height; } + + // Margins + public function getMarginTop() { return $this->marginTop; } + public function getMarginBottom() { return $this->marginBottom; } + public function getMarginLeft() { return $this->marginLeft; } + public function getMarginRight() { return $this->marginRight; } + + + /** + * Check if this is a continuous tape + * + * @return bool + */ + public function isContinuous() { + return $this->continuous; + } + + /** + * Get spacing between labels (for die-cut tapes) + * + * @return float + */ + public function getSpacing() { + return $this->spacing; + } +} \ No newline at end of file diff --git a/app/Models/Labels/Tapes/Generic/Tape_53mm.php b/app/Models/Labels/Tapes/Generic/Tape_53mm.php new file mode 100644 index 0000000000..fc46838d66 --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/Tape_53mm.php @@ -0,0 +1,32 @@ +tapeHeight = $height; + } + + /** + * Get the barcode size ratio for calculations + * + * @return float + */ + public function getBarcodeRatio() { + return 0.9; // Barcode should use 90% of available width + } +} \ No newline at end of file diff --git a/app/Models/Labels/Tapes/Generic/Tape_53mm_A.php b/app/Models/Labels/Tapes/Generic/Tape_53mm_A.php new file mode 100644 index 0000000000..399efbe11c --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/Tape_53mm_A.php @@ -0,0 +1,77 @@ +SetAutoPageBreak(false); + } + + public function write($pdf, $record) { + $pa = $this->getPrintableArea(); + + $currentX = $pa->x1; + $currentY = $pa->y1; + $usableWidth = $pa->w; + $usableHeight = $pa->h; + + if ($record->has('title')) { + static::writeText( + $pdf, $record->get('title'), + $pa->x1, $pa->y1, + 'freesans', '', $this->titleSize, 'C', + $pa->w, $this->titleSize, true, 0 + ); + $currentY += $this->titleSize + $this->titleMargin; + $usableHeight -= $this->titleSize + $this->titleMargin; + } + + // Make the barcode as large as possible while still leaving room for fields + $barcodeSize = min($usableHeight * 0.8, $usableWidth * $this->getBarcodeRatio()); + + if ($record->has('barcode2d')) { + $barcodeX = $pa->x1 + ($usableWidth - $barcodeSize) / 2; + + static::write2DBarcode( + $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type, + $barcodeX, $currentY, + $barcodeSize, $barcodeSize + ); + $currentY += $barcodeSize + $this->barcodeMargin; + } + + if ($record->has('fields')) { + foreach ($record->get('fields') as $field) { + static::writeText( + $pdf, $field['label'], + $currentX, $currentY, + 'freesans', '', $this->labelSize, 'L', + $usableWidth, $this->labelSize, true, 0 + ); + $currentY += $this->labelSize + $this->labelMargin; + + static::writeText( + $pdf, $field['value'], + $currentX, $currentY, + 'freemono', 'B', $this->fieldSize, 'L', + $usableWidth, $this->fieldSize, true, 0, 0.01 + ); + $currentY += $this->fieldSize + $this->fieldMargin; + } + } + } +} \ No newline at end of file diff --git a/app/Models/Labels/Tapes/Generic/Tape_53mm_B.php b/app/Models/Labels/Tapes/Generic/Tape_53mm_B.php new file mode 100644 index 0000000000..e297a30248 --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/Tape_53mm_B.php @@ -0,0 +1,78 @@ +SetAutoPageBreak(false); + } + + public function write($pdf, $record) { + $pa = $this->getPrintableArea(); + + $currentX = $pa->x1; + $currentY = $pa->y1; + $usableWidth = $pa->w; + $usableHeight = $pa->h; + + if ($record->has('title')) { + static::writeText( + $pdf, $record->get('title'), + $pa->x1, $pa->y1, + 'freesans', '', $this->titleSize, 'C', + $pa->w, $this->titleSize, true, 0 + ); + $currentY += $this->titleSize + $this->titleMargin; + $usableHeight -= $this->titleSize + $this->titleMargin; + } + + // Make the barcode as large as possible while still leaving room for fields + $barcodeSize = min($usableHeight * 0.8, $usableWidth * $this->getBarcodeRatio()); + + if ($record->has('barcode2d')) { + $barcodeX = $pa->x1; + + static::write2DBarcode( + $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type, + $barcodeX, $currentY, + $barcodeSize, $barcodeSize + ); + $currentX += $barcodeSize + $this->barcodeMargin; + $usableWidth -= $barcodeSize + $this->barcodeMargin; + } + + if ($record->has('fields')) { + foreach ($record->get('fields') as $field) { + static::writeText( + $pdf, $field['label'], + $currentX, $currentY, + 'freesans', '', $this->labelSize, 'L', + $usableWidth, $this->labelSize, true, 0 + ); + $currentY += $this->labelSize + $this->labelMargin; + + static::writeText( + $pdf, $field['value'], + $currentX, $currentY, + 'freemono', 'B', $this->fieldSize, 'L', + $usableWidth, $this->fieldSize, true, 0, 0.01 + ); + $currentY += $this->fieldSize + $this->fieldMargin; + } + } + } +} \ No newline at end of file From 248a05a916b2be38feafc87638d81bd951d18e86 Mon Sep 17 00:00:00 2001 From: Chris Olin Date: Sun, 4 May 2025 14:47:54 -0400 Subject: [PATCH 021/661] adds support for continuous 53mm and 0.59in printers --- .../Labels/Tapes/Generic/Continuous_53mm.php | 104 +++++++++++ ...{Tape_53mm_B.php => Continuous_53mm_A.php} | 15 +- .../Generic/Continuous_Landscape_0_59in.php | 170 ++++++++++++++++++ .../Generic/Continuous_Landscape_0_59in_A.php | 157 ++++++++++++++++ 4 files changed, 436 insertions(+), 10 deletions(-) create mode 100644 app/Models/Labels/Tapes/Generic/Continuous_53mm.php rename app/Models/Labels/Tapes/Generic/{Tape_53mm_B.php => Continuous_53mm_A.php} (88%) create mode 100644 app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php create mode 100644 app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php diff --git a/app/Models/Labels/Tapes/Generic/Continuous_53mm.php b/app/Models/Labels/Tapes/Generic/Continuous_53mm.php new file mode 100644 index 0000000000..4e06e2c96d --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/Continuous_53mm.php @@ -0,0 +1,104 @@ +tapeHeight = $height; + } + + public function getBarcodeRatio() { + return 0.9; // Barcode should use 90% of available width + } + + /** + * Calculate the required height for the content + * + * @param $record The record to calculate height for + * @return float The calculated height in mm + */ + protected function calculateRequiredHeight($record) { + $height = $this->marginTop + $this->marginBottom; + + // Add title height if present + if ($record->has('title') && $this->getSupportTitle()) { + $height += $this->titleSize + $this->titleMargin; + } + + // Add barcode height if present + if (($record->has('barcode2d') && $this->getSupport2DBarcode()) || + ($record->has('barcode') && $this->getSupport1DBarcode())) { + $pa = $this->getPrintableArea(); + $usableWidth = $pa->w; + $barcodeSize = $usableWidth * $this->getBarcodeRatio(); + $height += $barcodeSize + $this->barcodeMargin; + } + + // Add fields height if present + if ($record->has('fields') && $this->getSupportFields() > 0) { + foreach ($record->get('fields') as $field) { + $height += $this->labelSize + $this->labelMargin; + $height += $this->fieldSize + $this->fieldMargin; + } + } + + // Add a small buffer to ensure everything fits + $height += 2.0; + + // Ensure minimum height + return max($this->minHeight, $height); + } + + /** + * Override the writeAll method to support dynamic page sizes for continuous tapes + */ + public function writeAll($pdf, $data) { + // Use auto-sizing for continuous tapes, fixed height for die-cut tapes + if ($this->continuous) { + $data->each(function ($record, $index) use ($pdf) { + // Calculate the required height for this record + $requiredHeight = $this->calculateRequiredHeight($record); + + // Temporarily update the height property + $originalHeight = $this->height; + $this->height = $requiredHeight; + + // Add a new page with the calculated dimensions + $pdf->AddPage( + $this->getOrientation(), + [$this->getWidth(), $requiredHeight], + false, // Don't reset page number + false // Don't reset object ID + ); + + // Write the content + $this->write($pdf, $record); + + // Restore the original height + $this->height = $originalHeight; + }); + } else { + // Use the default implementation for non-continuous (die-cut) tapes + parent::writeAll($pdf, $data); + } + } +} \ No newline at end of file diff --git a/app/Models/Labels/Tapes/Generic/Tape_53mm_B.php b/app/Models/Labels/Tapes/Generic/Continuous_53mm_A.php similarity index 88% rename from app/Models/Labels/Tapes/Generic/Tape_53mm_B.php rename to app/Models/Labels/Tapes/Generic/Continuous_53mm_A.php index e297a30248..632695ea1a 100644 --- a/app/Models/Labels/Tapes/Generic/Tape_53mm_B.php +++ b/app/Models/Labels/Tapes/Generic/Continuous_53mm_A.php @@ -2,16 +2,12 @@ namespace App\Models\Labels\Tapes\Generic; -use Illuminate\Support\Collection; -use TCPDF; - -class Tape_53mm_B extends Tape_53mm +class Continuous_53mm_A extends Continuous_53mm { - - + public function getUnit() { return 'mm'; } public function getSupportAssetTag() { return false; } - public function getSupport1DBarcode() { return false; } + public function getSupport1DBarcode() { return true; } public function getSupport2DBarcode() { return true; } public function getSupportFields() { return 5; } public function getSupportLogo() { return false; } @@ -44,15 +40,14 @@ class Tape_53mm_B extends Tape_53mm $barcodeSize = min($usableHeight * 0.8, $usableWidth * $this->getBarcodeRatio()); if ($record->has('barcode2d')) { - $barcodeX = $pa->x1; + $barcodeX = $pa->x1 + ($usableWidth - $barcodeSize) / 2; static::write2DBarcode( $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type, $barcodeX, $currentY, $barcodeSize, $barcodeSize ); - $currentX += $barcodeSize + $this->barcodeMargin; - $usableWidth -= $barcodeSize + $this->barcodeMargin; + $currentY += $barcodeSize + $this->barcodeMargin; } if ($record->has('fields')) { diff --git a/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php new file mode 100644 index 0000000000..5efd88ff06 --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php @@ -0,0 +1,170 @@ +tapeHeight = $length; + + $this->marginTop = 0.1; + $this->marginBottom = 0.1; + // Keep small horizontal margins + $this->marginLeft = self::TAPE_WIDTH * 0.2; + // $this->marginRight = self::TAPE_WIDTH * 0.1; + + // Override font sizes to make them larger + // Calculate a larger base font size (3x the default) + $baseFontSize = self::TAPE_WIDTH * 0.16; // 3x the default 0.07 + + // Recalculate all element sizing based on the larger base font size + $this->titleSize = $baseFontSize; // Same as base font size + $this->titleMargin = $baseFontSize * 0.3; // 30% of base font size + $this->fieldSize = $baseFontSize * 1.1; // 110% of base font size + $this->fieldMargin = $baseFontSize * 0.1; // 10% of base font size + $this->labelSize = $baseFontSize * 0.7; // 70% of base font size + $this->labelMargin = $baseFontSize * -0.1; // -10% of base font size + $this->barcodeMargin = $baseFontSize * 0.9; // 20% of base font size + $this->tagSize = $baseFontSize * 0.8; // 80% of base font size + } + + public function getBarcodeRatio() { + return 1.0; // Barcode should use 100% of available height + } + + /** + * Calculate the required length for the content + * + * @param $record The record to calculate length for + * @return float The calculated length in inches + */ + protected function calculateRequiredLength($record) { + + // Calculate length needed for barcode and fields side by side + $requiredLength = 0; + + // Add barcode length if present + if (($record->has('barcode2d') && $this->getSupport2DBarcode()) || + ($record->has('barcode') && $this->getSupport1DBarcode())) { + // Use full tape width for barcode size + $barcodeSize = self::TAPE_WIDTH; + $requiredLength += $barcodeSize + $this->barcodeMargin * 0.3; // Minimal margin + } + + // Add fields length if present - calculate based on actual content + if ($record->has('fields') && $this->getSupportFields() > 0) { + $fields = array_slice($record->get('fields')->toArray(), 0, $this->getSupportFields()); + + // Base width for field area + $fieldsWidth = self::TAPE_WIDTH; + + // Calculate additional width based on text length + foreach ($fields as $field) { + // Get label and value text + $labelText = $field['label'] ?? ''; + $valueText = $field['value'] ?? ''; + + // Calculate approximate width needed based on text length + // Increase character width to ensure enough space (0.15 inches per character) + $labelWidth = strlen($labelText) * 0.09; + $valueWidth = strlen($valueText) * 0.09; + + // Use the longer of the two + $textWidth = max($labelWidth, $valueWidth); + + // Ensure minimum width and add to total + $fieldsWidth = max($fieldsWidth, $textWidth); + } + + // Add the calculated width for fields + $requiredLength += $fieldsWidth; + + // Add minimal extra space for field padding + // Reduce padding to eliminate extraneous space on right edge + // $requiredLength += self::TAPE_WIDTH * 0.1; + } + + // Ensure minimum length + return max($this->minHeight, $requiredLength); + } + + /** + * Calculate text width accurately using the PDF object + * + * @param $pdf The PDF object + * @param string $text The text to measure + * @param string $font The font to use + * @param string $style The font style + * @param float $size The font size + * @return float The calculated width + */ + protected function calculateTextWidth($pdf, $text, $font, $style, $size) { + $originalFont = $pdf->getFontFamily(); + $originalStyle = $pdf->getFontStyle(); + $originalSize = $pdf->getFontSizePt(); + + $pdf->SetFont($font, $style, Helper::convertUnit($size, $this->getUnit(), 'pt', true)); + $width = $pdf->GetStringWidth($text); + + // Restore original font settings + $pdf->SetFont($originalFont, $originalStyle, $originalSize); + + return $width; + } + + /** + * Override the writeAll method to support dynamic page sizes for continuous tapes + */ + public function writeAll($pdf, $data) { + // Use auto-sizing for continuous tapes, fixed height for die-cut tapes + if ($this->continuous) { + $data->each(function ($record, $index) use ($pdf) { + // Calculate the required length by calling write with calculateOnly=true + $requiredLength = $this->write($pdf, $record); + + // If write didn't return a length (old implementation), fall back to calculateRequiredLength + if ($requiredLength === null) { + $requiredLength = $this->calculateRequiredLength($record); + } + + // Temporarily update the height property + $originalHeight = $this->height; + $this->height = self::TAPE_WIDTH; // Keep height fixed at tape width + + // Add a new page with the calculated dimensions + // Keep height fixed at TAPE_WIDTH, use calculated length for width + $pdf->AddPage( + $this->getOrientation(), + [$requiredLength, self::TAPE_WIDTH], + false, // Don't reset page number + false // Don't reset object ID + ); + + // Write the content + $this->write($pdf, $record); + + // Restore the original height + $this->height = $originalHeight; + }); + } else { + // Use the default implementation for non-continuous (die-cut) tapes + parent::writeAll($pdf, $data); + } + } +} \ No newline at end of file diff --git a/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php new file mode 100644 index 0000000000..7c5774e17d --- /dev/null +++ b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php @@ -0,0 +1,157 @@ +SetAutoPageBreak(false); + } + + public function write($pdf, $record, $calculateOnly = false) { + $pa = $this->getPrintableArea(); + + $currentX = $pa->x1; + $currentY = $pa->y1; + $usableWidth = $pa->w; + $usableHeight = $pa->h; + + // Calculate required length based on content + $requiredLength = 0; + + // Use full usable height for barcode + $barcodeSize = $usableHeight; + + // Add barcode width to required length + if ($record->has('barcode2d') && $this->getSupport2DBarcode()) { + $requiredLength += $barcodeSize; + // Add gap between barcode and fields + $requiredLength += $this->barcodeMargin; + } + + // Calculate fields width using accurate text measurement + if ($record->has('fields') && $this->getSupportFields() > 0) { + $fields = array_slice($record->get('fields')->toArray(), 0, $this->getSupportFields()); + $fieldsWidth = 0; + + foreach ($fields as $field) { + $labelText = $field['label'] ?? ''; + $valueText = $field['value'] ?? ''; + + // Calculate accurate width using the PDF object + $labelWidth = $this->calculateTextWidth($pdf, $labelText, 'freesans', 'B', $this->labelSize * 1.2); + $valueWidth = $this->calculateTextWidth($pdf, $valueText, 'freemono', 'B', $this->fieldSize * 1.3); + + // Use the longer of the two + $textWidth = max($labelWidth, $valueWidth); + $fieldsWidth = max($fieldsWidth, $textWidth); + } + + $requiredLength += $fieldsWidth; + } + + // Add more padding to prevent text from being cut off + // $requiredLength += self::TAPE_WIDTH * 0.8; + + // Ensure minimum length + $requiredLength = max($this->minHeight, $requiredLength); + + // If we're just calculating, return the length + if ($calculateOnly) { + return $requiredLength; + } + + // Otherwise, render the content + // Position barcode on the left side + if ($record->has('barcode2d') && $this->getSupport2DBarcode()) { + // Position at top of usable area + static::write2DBarcode( + $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type, + $currentX, $currentY, + $barcodeSize, $barcodeSize + ); + $currentX += $barcodeSize + $this->barcodeMargin; + $usableWidth -= $barcodeSize + $this->barcodeMargin; + } + + // Position fields to the right of the barcode + if ($record->has('fields') && $this->getSupportFields() > 0) { + // Limit to the number of supported fields + $fields = array_slice($record->get('fields')->toArray(), 0, $this->getSupportFields()); + + // Calculate total height needed for fields + $totalFieldsHeight = 0; + foreach ($fields as $field) { + $totalFieldsHeight += $this->labelSize * 1.2 + $this->labelMargin; // Increased label size by 20% + $totalFieldsHeight += $this->fieldSize * 1.3 + $this->fieldMargin * 2; // Increased field size by 30% and margin + } + + // Start position - respect top margin + $fieldY = $currentY; // $currentY already includes the top margin + $fieldWidth = $usableWidth; + + // Calculate available height for fields (respecting margins) + $availableHeight = $usableHeight; + + // If fields don't fill available height, adjust spacing proportionally + // but don't exceed the available height + $scaleFactor = 1.0; // Default scale factor + if ($totalFieldsHeight < $availableHeight && count($fields) > 0) { + // Scale up to fill available height, but not too much + $scaleFactor = min(1.5, $availableHeight / $totalFieldsHeight); + } else if ($totalFieldsHeight > $availableHeight && count($fields) > 0) { + // Scale down to fit within available height + $scaleFactor = $availableHeight / $totalFieldsHeight; + } + + foreach ($fields as $field) { + // Calculate scaled spacing + $labelHeight = $this->labelSize * 1.2 * $scaleFactor; + $labelSpacing = $this->labelMargin * $scaleFactor; + $fieldHeight = $this->fieldSize * 1.3 * $scaleFactor; + $fieldSpacing = $this->fieldMargin * 2 * $scaleFactor; + + // Check if label is empty or null + $labelText = $field['label'] ?? ''; + $valueText = $field['value'] ?? ''; + + if (empty(trim($labelText))) { + // If label is empty, just render the value at the current Y position + static::writeText( + $pdf, $valueText, + $currentX, $fieldY, + 'freemono', 'B', $this->fieldSize * 1.3, 'L', // Increased field size by 30% + $fieldWidth, $fieldHeight, false, 0, 0.00 + ); + $fieldY += ($fieldHeight + $fieldSpacing) + 0.02; // Increased spacing after value + } else { + // If label has content, render both label and value + static::writeText( + $pdf, $labelText, + $currentX, $fieldY, + 'freesans', 'B', $this->labelSize * 1.2, 'L', // Increased label size by 20% and made bold + $labelWidth, $labelHeight, false, 0, + ); + $fieldY += ($labelHeight + $labelSpacing) + 0.01; + + // Value + static::writeText( + $pdf, $valueText, + $currentX, $fieldY, // Position value directly below label + 'freemono', 'B', $this->fieldSize * 1.3, 'L', // Increased field size by 30% + $fieldWidth, $fieldHeight, false, 0, 0.00 + ); + $fieldY += ($fieldHeight + $fieldSpacing) + 0.02; // Increased spacing after value + } + } + } + } +} \ No newline at end of file From 2a2acf6d1c802e73a466262f9c7bb7c5bdbe30d4 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 14 May 2025 14:09:48 -0700 Subject: [PATCH 022/661] Add failing test --- tests/Feature/Assets/Api/StoreAssetTest.php | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/Feature/Assets/Api/StoreAssetTest.php b/tests/Feature/Assets/Api/StoreAssetTest.php index 7136b546a3..1af3569f75 100644 --- a/tests/Feature/Assets/Api/StoreAssetTest.php +++ b/tests/Feature/Assets/Api/StoreAssetTest.php @@ -12,6 +12,7 @@ use App\Models\Supplier; use App\Models\User; use Illuminate\Support\Facades\Crypt; use Illuminate\Testing\Fluent\AssertableJson; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; class StoreAssetTest extends TestCase @@ -572,6 +573,64 @@ class StoreAssetTest extends TestCase $this->assertTrue($asset->assignedTo->is($userAssigned)); } + public static function checkoutTargets() + { + yield 'Users' => [ + function () { + return [ + 'key' => 'assigned_user', + 'value' => [ + User::factory()->create()->id, + User::factory()->create()->id, + ], + ]; + }, + ]; + + yield 'Locations' => [ + function () { + return [ + 'key' => 'assigned_location', + 'value' => [ + Location::factory()->create()->id, + Location::factory()->create()->id, + ], + ]; + }, + ]; + + yield 'Assets' => [ + function () { + return [ + 'key' => 'assigned_asset', + 'value' => [ + Asset::factory()->create()->id, + Asset::factory()->create()->id, + ], + ]; + }, + ]; + } + + /** @link https://app.shortcut.com/grokability/story/29181 */ + #[DataProvider('checkoutTargets')] + public function testAssignedFieldValidationCannotBeArray($data) + { + ['key' => $key, 'value' => $value] = $data(); + + $this->actingAsForApi(User::factory()->createAssets()->create()) + ->postJson(route('api.assets.store'), [ + 'asset_tag' => '123456', + 'model_id' => AssetModel::factory()->create()->id, + 'status_id' => Statuslabel::factory()->readyToDeploy()->create()->id, + $key => $value, + ]) + ->assertStatusMessageIs('error') + ->assertJson(function (AssertableJson $json) use ($key) { + $json->has("messages.{$key}")->etc(); + }); + } + public function testAnAssetCanBeCheckedOutToLocationOnStore() { $model = AssetModel::factory()->create(); From 02fa7daa1d9ec4f2e3ad97ac906b6c03d71fd3b9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 14 May 2025 14:13:07 -0700 Subject: [PATCH 023/661] Require assigned_x to be integer on asset model --- app/Models/Asset.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 29231b22c8..2cb14308c6 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -122,9 +122,9 @@ class Asset extends Depreciable 'assigned_to' => ['nullable', 'integer', 'required_with:assigned_type'], 'assigned_type' => ['nullable', 'required_with:assigned_to', 'in:'.User::class.",".Location::class.",".Asset::class], 'requestable' => ['nullable', 'boolean'], - 'assigned_user' => ['nullable', 'exists:users,id,deleted_at,NULL'], - 'assigned_location' => ['nullable', 'exists:locations,id,deleted_at,NULL', 'fmcs_location'], - 'assigned_asset' => ['nullable', 'exists:assets,id,deleted_at,NULL'] + 'assigned_user' => ['integer', 'nullable', 'exists:users,id,deleted_at,NULL'], + 'assigned_location' => ['integer', 'nullable', 'exists:locations,id,deleted_at,NULL', 'fmcs_location'], + 'assigned_asset' => ['integer', 'nullable', 'exists:assets,id,deleted_at,NULL'] ]; From 3c1088f0306f9272c808fe6760132ccc08978094 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 15:49:16 -0700 Subject: [PATCH 024/661] Improve variable name --- resources/views/blade/input/location-select.blade.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/blade/input/location-select.blade.php b/resources/views/blade/input/location-select.blade.php index c3c1e50b99..5dd31374ea 100644 --- a/resources/views/blade/input/location-select.blade.php +++ b/resources/views/blade/input/location-select.blade.php @@ -34,9 +34,9 @@ @endif > @if ($selected) - @foreach(Arr::wrap($selected) as $id) - @endforeach @endif From 69b9b0bbc00e02acc247a97c0d27d8af2eb3b05a Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 15:53:25 -0700 Subject: [PATCH 025/661] Allow setting id within location-select --- resources/views/blade/input/location-select.blade.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/views/blade/input/location-select.blade.php b/resources/views/blade/input/location-select.blade.php index 5dd31374ea..f9bd28fa63 100644 --- a/resources/views/blade/input/location-select.blade.php +++ b/resources/views/blade/input/location-select.blade.php @@ -11,6 +11,15 @@ 'hideNewButton' => false, ]) +@php + $id = $name . '_location_select'; + + // User provided id, overwrite the default + if ($attributes->has('id')) { + $id = $attributes->get('id'); + } +@endphp +
    Date: Mon, 2 Jun 2025 16:25:06 -0700 Subject: [PATCH 026/661] Scaffold settings page changes --- resources/views/settings/alerts.blade.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 9f31206452..6e5ab2c486 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -96,8 +96,18 @@ {!! $errors->first('admin_cc_email', '
    ') !!}

    {{ trans('admin/settings/general.admin_cc_email_help') }}

    - - +
    +
    +
    +
    + +
    From 367ab8ddd5fb275eaf640c9fdc5dd4016746ead9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 16:27:04 -0700 Subject: [PATCH 027/661] Add help text --- resources/views/settings/alerts.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 6e5ab2c486..6b28a5f3a5 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -108,6 +108,9 @@ Only send if acceptance required +

    + "Always send copy to CC email" requires valid CC Email to be set. +

    From 11b47b308b5df6b4cc97e507c10b9330381fae6d Mon Sep 17 00:00:00 2001 From: spencerrlongg Date: Mon, 2 Jun 2025 18:39:08 -0500 Subject: [PATCH 028/661] front end done, sloppy --- .../custom_fields_form_bulk_edit.blade.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/resources/views/models/custom_fields_form_bulk_edit.blade.php b/resources/views/models/custom_fields_form_bulk_edit.blade.php index cea495ccd2..12bf14b136 100644 --- a/resources/views/models/custom_fields_form_bulk_edit.blade.php +++ b/resources/views/models/custom_fields_form_bulk_edit.blade.php @@ -30,6 +30,12 @@ :selected="old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id)))" class="format form-control" /> +
    + +
    @elseif ($field->element=='textarea') @if($field->is_unique) @@ -37,7 +43,7 @@ @endif @if(!$field->is_unique) - @endif + @endif @elseif ($field->element=='checkbox') @foreach ($field->formatFieldValuesAsArray() as $key => $value) @@ -45,7 +51,6 @@ {$field->db_column_name()}))) ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($key, array_map('trim', explode(',', $field->defaultValue($model->id)))) ? ' checked="checked"' : '')) }}> {{ $value }} - @endforeach @elseif ($field->element=='radio') @foreach ($field->formatFieldValuesAsArray() as $value) @@ -115,8 +120,14 @@ @endif - +
    + +
    - @endforeach + + @endforeach @endif @endforeach From 054ff425474570e93cf8aa30c6a4f59c00d4724f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:03:14 -0700 Subject: [PATCH 029/661] Add migration for admin_cc_always --- ..._add_admin_cc_always_to_settings_table.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php diff --git a/database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php b/database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php new file mode 100644 index 0000000000..d28115a6d2 --- /dev/null +++ b/database/migrations/2025_06_02_233556_add_admin_cc_always_to_settings_table.php @@ -0,0 +1,27 @@ +boolean('admin_cc_always')->after('admin_cc_email')->default(1); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('admin_cc_always'); + }); + } +}; From 6bc3209333fc053b58507f9cd9dfc2220652268e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:05:00 -0700 Subject: [PATCH 030/661] Use @checked for inputs --- resources/views/settings/alerts.blade.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 6b28a5f3a5..53a85ea7a2 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -101,11 +101,21 @@

    From 9e4aab7165dcace335dfc26d8cdefdbcf888af4c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:05:18 -0700 Subject: [PATCH 031/661] Scaffold tests --- tests/Feature/Settings/AlertsSettingTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Feature/Settings/AlertsSettingTest.php b/tests/Feature/Settings/AlertsSettingTest.php index d79bd1cf21..1281547c67 100644 --- a/tests/Feature/Settings/AlertsSettingTest.php +++ b/tests/Feature/Settings/AlertsSettingTest.php @@ -26,4 +26,18 @@ class AlertsSettingTest extends TestCase $this->followRedirects($response)->assertSee('alert-success'); } + public function testCannotUpdateAdminCcAwaysWithoutAdminCcEmail() + { + $this->markTestIncomplete(); + } + + public function test_can_update_admin_cc_always_to_true() + { + $this->markTestIncomplete(); + } + + public function test_can_update_admin_cc_always_to_false() + { + $this->markTestIncomplete(); + } } From d75120000aaf08665d4c34957e054f6255b102f4 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:11:18 -0700 Subject: [PATCH 032/661] Add failing tests --- tests/Feature/Settings/AlertsSettingTest.php | 28 +++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/Feature/Settings/AlertsSettingTest.php b/tests/Feature/Settings/AlertsSettingTest.php index 1281547c67..6dae959648 100644 --- a/tests/Feature/Settings/AlertsSettingTest.php +++ b/tests/Feature/Settings/AlertsSettingTest.php @@ -26,18 +26,32 @@ class AlertsSettingTest extends TestCase $this->followRedirects($response)->assertSee('alert-success'); } - public function testCannotUpdateAdminCcAwaysWithoutAdminCcEmail() - { - $this->markTestIncomplete(); - } - public function test_can_update_admin_cc_always_to_true() { - $this->markTestIncomplete(); + $this->settings->disableAdminCCAlways(); + + $this->actingAs(User::factory()->superuser()->create()) + ->post(route('settings.alerts.save', ['admin_cc_always' => '1'])); + + $this->assertDatabaseHas('settings', ['admin_cc_always' => '1']); + } + + public function test_cannot_update_admin_cc_always_without_admin_cc_email() + { + $this->settings->disableAdminCCAlways(); + + $this->actingAs(User::factory()->superuser()->create()) + ->post(route('settings.alerts.save', ['admin_cc_always' => '1'])) + ->assertSessionHasErrors('admin_cc_always'); } public function test_can_update_admin_cc_always_to_false() { - $this->markTestIncomplete(); + $this->settings->enableAdminCCAlways(); + + $this->actingAs(User::factory()->superuser()->create()) + ->post(route('settings.alerts.save', ['admin_cc_always' => '0'])); + + $this->assertDatabaseHas('settings', ['admin_cc_always' => '0']); } } From 6e37f945ac081493d1db188bb7d773e86414bc12 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:13:37 -0700 Subject: [PATCH 033/661] Add test helpers --- tests/Feature/Settings/AlertsSettingTest.php | 2 +- tests/Support/Settings.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Settings/AlertsSettingTest.php b/tests/Feature/Settings/AlertsSettingTest.php index 6dae959648..42974d6640 100644 --- a/tests/Feature/Settings/AlertsSettingTest.php +++ b/tests/Feature/Settings/AlertsSettingTest.php @@ -47,7 +47,7 @@ class AlertsSettingTest extends TestCase public function test_can_update_admin_cc_always_to_false() { - $this->settings->enableAdminCCAlways(); + $this->settings->enableAdminCC()->enableAdminCCAlways(); $this->actingAs(User::factory()->superuser()->create()) ->post(route('settings.alerts.save', ['admin_cc_always' => '0'])); diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index bcbd83d8b6..d33970e683 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -4,6 +4,7 @@ namespace Tests\Support; use App\Models\Setting; use Illuminate\Support\Facades\Crypt; +use RuntimeException; class Settings { @@ -60,6 +61,24 @@ class Settings ]); } + public function enableAdminCCAlways(): Settings + { + if (is_null($this->setting->admin_cc_email) || $this->setting->admin_cc_email == 0) { + throw new RuntimeException('admin_cc_email requires admin_cc_email to be set.'); + } + + return $this->update([ + 'admin_cc_always' => 1, + ]); + } + + public function disableAdminCCAlways(): Settings + { + return $this->update([ + 'admin_cc_always' => 0, + ]); + } + public function enableMultipleFullCompanySupport(): Settings { return $this->update(['full_multiple_companies_support' => 1]); From 12dc33244da092213fad045ed99782be557147ea Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:20:01 -0700 Subject: [PATCH 034/661] Start storing admin_cc_always --- app/Http/Controllers/SettingsController.php | 1 + app/Http/Requests/StoreNotificationSettings.php | 4 ++++ tests/Feature/Settings/AlertsSettingTest.php | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 747f7b7284..b652455a57 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -650,6 +650,7 @@ class SettingsController extends Controller $setting->alert_email = $alert_email; $setting->admin_cc_email = $admin_cc_email; + $setting->admin_cc_always = $request->validated('admin_cc_always'); $setting->alerts_enabled = $request->input('alerts_enabled', '0'); $setting->alert_interval = $request->input('alert_interval'); $setting->alert_threshold = $request->input('alert_threshold'); diff --git a/app/Http/Requests/StoreNotificationSettings.php b/app/Http/Requests/StoreNotificationSettings.php index bf5f5b3d4e..f58d014c76 100644 --- a/app/Http/Requests/StoreNotificationSettings.php +++ b/app/Http/Requests/StoreNotificationSettings.php @@ -5,6 +5,7 @@ namespace App\Http\Requests; use App\Models\Accessory; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Gate; +use Illuminate\Validation\Rule; class StoreNotificationSettings extends FormRequest { @@ -26,6 +27,9 @@ class StoreNotificationSettings extends FormRequest return [ 'alert_email' => 'email_array|nullable', 'admin_cc_email' => 'email_array|nullable', + 'admin_cc_always' => [ + Rule::in('0', '1'), + ], 'alert_threshold' => 'numeric|nullable', 'alert_interval' => 'numeric|nullable|gt:0', 'audit_warning_days' => 'numeric|nullable', diff --git a/tests/Feature/Settings/AlertsSettingTest.php b/tests/Feature/Settings/AlertsSettingTest.php index 42974d6640..7812d0eb33 100644 --- a/tests/Feature/Settings/AlertsSettingTest.php +++ b/tests/Feature/Settings/AlertsSettingTest.php @@ -18,7 +18,10 @@ class AlertsSettingTest extends TestCase public function testAdminCCEmailArrayCanBeSaved() { $response = $this->actingAs(User::factory()->superuser()->create()) - ->post(route('settings.alerts.save', ['alert_email' => 'me@example.com,you@example.com'])) + ->post(route('settings.alerts.save', [ + 'alert_email' => 'me@example.com,you@example.com', + 'admin_cc_always' => '1', + ])) ->assertStatus(302) ->assertValid('alert_email') ->assertRedirect(route('settings.index')) From 79e00c1191ef6c42579c000d2e50c6c3bac2eccc Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:21:58 -0700 Subject: [PATCH 035/661] Require admin_cc_email if admin_cc_always is true --- app/Http/Requests/StoreNotificationSettings.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/StoreNotificationSettings.php b/app/Http/Requests/StoreNotificationSettings.php index f58d014c76..6b2b1f5aec 100644 --- a/app/Http/Requests/StoreNotificationSettings.php +++ b/app/Http/Requests/StoreNotificationSettings.php @@ -26,7 +26,11 @@ class StoreNotificationSettings extends FormRequest { return [ 'alert_email' => 'email_array|nullable', - 'admin_cc_email' => 'email_array|nullable', + 'admin_cc_email' => [ + 'email_array', + 'nullable', + 'required_if_accepted:admin_cc_always', + ], 'admin_cc_always' => [ Rule::in('0', '1'), ], From dec3c4aff380f0d1819ceaa5e54c04db0ea73e6c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Mon, 2 Jun 2025 17:45:08 -0700 Subject: [PATCH 036/661] Improve wording --- resources/views/settings/alerts.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 53a85ea7a2..3a472fbe20 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -116,7 +116,7 @@ value="0" @checked($setting->admin_cc_always == 0) > - Only send if acceptance required + Only send copy if acceptance required

    "Always send copy to CC email" requires valid CC Email to be set. From b5849500f985a9df18f6231f19a98a659dfdc8e6 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 3 Jun 2025 10:49:17 -0700 Subject: [PATCH 037/661] add isDeplyable check --- .../Controllers/Assets/BulkAssetsController.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 6fc25224f9..691dab8d04 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -53,6 +53,8 @@ class BulkAssetsController extends Controller $asset_ids = $request->input('ids'); if ($request->input('bulk_actions') === 'checkout') { + $this->isDeployable($asset_ids); + $request->session()->flashInput(['selected_assets' => $asset_ids]); return redirect()->route('hardware.bulkcheckout.show'); } @@ -660,4 +662,18 @@ class BulkAssetsController extends Controller return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success')); } } + public function isDeployable ($asset_ids) : bool { + $undeployable = Asset::whereIn('id', $asset_ids) + ->whereHas('assetstatus', function ($query) { + $query->where('deployable', 0) + ->where('pending', 0) + ->where('archived', 0) + ->exists(); + }); + + if( !$undeployable ) { + return true; + } + return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles')); + } } From 3b832f507fa4d6cdd9c2793f56a7f1925dadd3a2 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 3 Jun 2025 11:38:59 -0700 Subject: [PATCH 038/661] fixes status check for bulk checkout --- .../Assets/BulkAssetsController.php | 20 +++++++++---------- .../lang/en-US/admin/hardware/message.php | 2 +- routes/web.php | 9 +++++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 691dab8d04..cec6f302b1 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -53,7 +53,10 @@ class BulkAssetsController extends Controller $asset_ids = $request->input('ids'); if ($request->input('bulk_actions') === 'checkout') { - $this->isDeployable($asset_ids); + + if($this->hasUndeployableStatus($asset_ids)){ + return redirect()->back()->with('error', trans('admin/hardware/message.undeployable')); + } $request->session()->flashInput(['selected_assets' => $asset_ids]); return redirect()->route('hardware.bulkcheckout.show'); @@ -662,18 +665,15 @@ class BulkAssetsController extends Controller return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success')); } } - public function isDeployable ($asset_ids) : bool { + public function hasUndeployableStatus (array $asset_ids) : bool + { $undeployable = Asset::whereIn('id', $asset_ids) - ->whereHas('assetstatus', function ($query) { - $query->where('deployable', 0) - ->where('pending', 0) - ->where('archived', 0) - ->exists(); - }); + ->undeployable() + ->first(); - if( !$undeployable ) { + if($undeployable) { return true; } - return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles')); + return false; } } diff --git a/resources/lang/en-US/admin/hardware/message.php b/resources/lang/en-US/admin/hardware/message.php index c19bc63647..893f992708 100644 --- a/resources/lang/en-US/admin/hardware/message.php +++ b/resources/lang/en-US/admin/hardware/message.php @@ -2,7 +2,7 @@ return [ - 'undeployable' => 'Warning: This asset has been marked as currently undeployable. If this status has changed, please update the asset status.', + 'undeployable' => 'An asset in your selection for checkout has been marked as currently undeployable. If this status has changed, please update the asset status before checking it out.', 'does_not_exist' => 'Asset does not exist.', 'does_not_exist_var' => 'Asset with tag :asset_tag not found.', 'no_tag' => 'No asset tag provided.', diff --git a/routes/web.php b/routes/web.php index add1e62354..68896bbcde 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,6 +25,7 @@ use App\Http\Controllers\StatuslabelsController; use App\Http\Controllers\SuppliersController; use App\Http\Controllers\ViewAssetsController; use App\Livewire\Importer; +use App\Models\Asset; use App\Models\ReportTemplate; use Illuminate\Support\Facades\Route; use Tabuna\Breadcrumbs\Trail; @@ -698,3 +699,11 @@ Route::middleware(['auth'])->get( ->breadcrumbs(fn (Trail $trail) => $trail->push('Home', route('home')) ); + +Route::get( 'blah', + function(){ + Asset::whereIn('id', [1886]) + ->undeployable() + ->get(); + } +); From 71290084287197b8a2accaa2bbf1950420a89dc2 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 3 Jun 2025 11:44:38 -0700 Subject: [PATCH 039/661] remove testing changes --- routes/web.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/routes/web.php b/routes/web.php index 68896bbcde..add1e62354 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,6 @@ use App\Http\Controllers\StatuslabelsController; use App\Http\Controllers\SuppliersController; use App\Http\Controllers\ViewAssetsController; use App\Livewire\Importer; -use App\Models\Asset; use App\Models\ReportTemplate; use Illuminate\Support\Facades\Route; use Tabuna\Breadcrumbs\Trail; @@ -699,11 +698,3 @@ Route::middleware(['auth'])->get( ->breadcrumbs(fn (Trail $trail) => $trail->push('Home', route('home')) ); - -Route::get( 'blah', - function(){ - Asset::whereIn('id', [1886]) - ->undeployable() - ->get(); - } -); From cb608d7fd16d21e1b24c40a4150ade033f49270b Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 3 Jun 2025 11:58:57 -0700 Subject: [PATCH 040/661] testing buttons out --- .../models/custom_fields_form_bulk_edit.blade.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/views/models/custom_fields_form_bulk_edit.blade.php b/resources/views/models/custom_fields_form_bulk_edit.blade.php index cea495ccd2..68a42a8006 100644 --- a/resources/views/models/custom_fields_form_bulk_edit.blade.php +++ b/resources/views/models/custom_fields_form_bulk_edit.blade.php @@ -30,6 +30,9 @@ :selected="old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id)))" class="format form-control" /> + @elseif ($field->element=='textarea') @if($field->is_unique) @@ -45,7 +48,9 @@ {$field->db_column_name()}))) ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($key, array_map('trim', explode(',', $field->defaultValue($model->id)))) ? ' checked="checked"' : '')) }}> {{ $value }} - + @endforeach @elseif ($field->element=='radio') @foreach ($field->formatFieldValuesAsArray() as $value) @@ -54,7 +59,9 @@ {$field->db_column_name()} == $value ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($value, explode(', ', $field->defaultValue($model->id))) ? ' checked="checked"' : '')) }}> {{ $value }} - + @endforeach @endif From ea3364ab6815b0821908ce14cac6555383b11690 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 3 Jun 2025 13:19:44 -0700 Subject: [PATCH 041/661] Split test case --- ...ficationsToAdminAlertEmailUponCheckout.php | 89 +++++++++++++++++++ ...ilNotificationsToUserUponCheckoutTest.php} | 40 +-------- 2 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php rename tests/Feature/Notifications/Email/{EmailNotificationsUponCheckoutTest.php => EmailNotificationsToUserUponCheckoutTest.php} (68%) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php new file mode 100644 index 0000000000..930a1388f0 --- /dev/null +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php @@ -0,0 +1,89 @@ +category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + + $this->assetModel = AssetModel::factory()->for($this->category)->create(); + $this->asset = Asset::factory()->for($this->assetModel, 'model')->create(); + + $this->user = User::factory()->create(); + } + + public function test_admin_alert_email_sends() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_category_is_not_set_to_send_email_to_user() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_user_has_no_email_address() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + $this->user->update(['email' => null]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + private function fireCheckoutEvent(): void + { + event(new CheckoutableCheckedOut( + $this->asset, + $this->user, + User::factory()->superuser()->create(), + '', + )); + } +} diff --git a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckoutTest.php similarity index 68% rename from tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php rename to tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckoutTest.php index e5a1a66cb6..e5741da8ec 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckoutTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\Attributes\Group; use Tests\TestCase; #[Group('notifications')] -class EmailNotificationsUponCheckoutTest extends TestCase +class EmailNotificationsToUserUponCheckoutTest extends TestCase { private Asset $asset; private AssetModel $assetModel; @@ -87,44 +87,6 @@ class EmailNotificationsUponCheckoutTest extends TestCase Mail::assertNothingSent(); } - public function test_admin_alert_email_sends() - { - $this->settings->enableAdminCC('cc@example.com'); - - $this->category->update(['checkin_email' => true]); - - $this->fireCheckoutEvent(); - - Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { - return $mail->hasCc('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_category_is_not_set_to_send_email_to_user() - { - $this->settings->enableAdminCC('cc@example.com'); - - $this->fireCheckoutEvent(); - - Mail::assertSent(CheckoutAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_user_has_no_email_address() - { - $this->settings->enableAdminCC('cc@example.com'); - - $this->category->update(['checkin_email' => true]); - $this->user->update(['email' => null]); - - $this->fireCheckoutEvent(); - - Mail::assertSent(CheckoutAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - private function fireCheckoutEvent(): void { event(new CheckoutableCheckedOut( From 51479c8bbc3a5cc76ed7db79435e15c65930dfb9 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 3 Jun 2025 13:28:40 -0700 Subject: [PATCH 042/661] Scaffold failing test --- ...NotificationsToAdminAlertEmailUponCheckout.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php index 930a1388f0..9493e30b90 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php @@ -77,6 +77,21 @@ class EmailNotificationsToAdminAlertEmailUponCheckout extends TestCase }); } + public function test_admin_alert_email_not_sent_when_always_send_is_false_and_asset_does_not_require_acceptance() + { + $this->settings + ->enableAdminCC('cc@example.com') + ->disableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckoutEvent(); + + Mail::assertNotSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); + } + private function fireCheckoutEvent(): void { event(new CheckoutableCheckedOut( From 3942489d21aeb8538c8084acb5537162727720fa Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 3 Jun 2025 14:13:52 -0700 Subject: [PATCH 043/661] Add Test suffix and scaffold test --- ...ationsToAdminAlertEmailUponCheckoutTest.php} | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) rename tests/Feature/Notifications/Email/{EmailNotificationsToAdminAlertEmailUponCheckout.php => EmailNotificationsToAdminAlertEmailUponCheckoutTest.php} (83%) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckoutTest.php similarity index 83% rename from tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php rename to tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckoutTest.php index 9493e30b90..d881ae7dab 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckout.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckoutTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\Attributes\Group; use Tests\TestCase; #[Group('notifications')] -class EmailNotificationsToAdminAlertEmailUponCheckout extends TestCase +class EmailNotificationsToAdminAlertEmailUponCheckoutTest extends TestCase { private Asset $asset; private AssetModel $assetModel; @@ -77,6 +77,21 @@ class EmailNotificationsToAdminAlertEmailUponCheckout extends TestCase }); } + public function test_admin_alert_email_sent_when_always_send_is_true_and_asset_does_not_require_acceptance() + { + $this->settings + ->enableAdminCC('cc@example.com') + ->enableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); + } + public function test_admin_alert_email_not_sent_when_always_send_is_false_and_asset_does_not_require_acceptance() { $this->settings From 03725c8e0cd92e2f09ff56d3d6e9dfa6352a1c80 Mon Sep 17 00:00:00 2001 From: spencerrlongg Date: Tue, 3 Jun 2025 17:21:07 -0500 Subject: [PATCH 044/661] custom field null and filtering --- .../Assets/BulkAssetsController.php | 21 +++++++++++++++++++ .../custom_fields_form_bulk_edit.blade.php | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 6fc25224f9..443cd2b8fa 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -201,6 +201,7 @@ class BulkAssetsController extends Controller */ public function update(Request $request) : RedirectResponse { + //dd($request->all()); $this->authorize('update', Asset::class); $has_errors = 0; $error_array = array(); @@ -213,6 +214,20 @@ class BulkAssetsController extends Controller } $custom_field_columns = CustomField::all()->pluck('db_column')->toArray(); + // find input attirubtes that start with 'null_' + $temp_custom_fields_to_null = array_filter($request->all(), function ($key) { + // filter out all keys that start with 'null_' + return (strpos($key, 'null_') === 0); + }, ARRAY_FILTER_USE_KEY);; + // remove 'null_' from the keys + $custom_fields_to_null = []; + foreach ($temp_custom_fields_to_null as $key => $value) { + $custom_fields_to_null[str_replace('null_', '', $key)] = $value; + } + + + + if (! $request->filled('ids') || count($request->input('ids')) == 0) { @@ -252,6 +267,7 @@ class BulkAssetsController extends Controller || ($request->filled('null_next_audit_date')) || ($request->filled('null_asset_eol_date')) || ($request->anyFilled($custom_field_columns)) + || ($request->anyFilled(array_keys($custom_fields_to_null))) ) { // Let's loop through those assets and build an update array @@ -278,6 +294,10 @@ class BulkAssetsController extends Controller foreach ($custom_field_columns as $key => $custom_field_column) { $this->conditionallyAddItem($custom_field_column); } + foreach ($custom_fields_to_null as $key => $custom_field_to_null) { + + $this->conditionallyAddItem($key); + } if (!($asset->eol_explicit)) { if ($request->filled('model_id')) { @@ -423,6 +443,7 @@ class BulkAssetsController extends Controller } /** + * * Start all the custom fields shenanigans */ diff --git a/resources/views/models/custom_fields_form_bulk_edit.blade.php b/resources/views/models/custom_fields_form_bulk_edit.blade.php index 12bf14b136..09f06e575b 100644 --- a/resources/views/models/custom_fields_form_bulk_edit.blade.php +++ b/resources/views/models/custom_fields_form_bulk_edit.blade.php @@ -122,7 +122,7 @@

    From d01f7cf3177e7cc948c55adee9806986bf939bba Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 3 Jun 2025 15:32:11 -0700 Subject: [PATCH 045/661] Adhere to admin_cc_always setting --- app/Listeners/CheckoutableListener.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index e647bcd9e0..ec843b980b 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -69,16 +69,16 @@ class CheckoutableListener return; } + $acceptance = $this->getCheckoutAcceptance($event); + $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable); - $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance); $shouldSendWebhookNotification = $this->shouldSendWebhookNotification(); if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) { return; } - $acceptance = $this->getCheckoutAcceptance($event); - if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) { $mailable = $this->getCheckoutMailType($event, $acceptance); $notifiable = $this->getNotifiableUser($event); @@ -419,9 +419,19 @@ class CheckoutableListener return false; } - private function shouldSendEmailToAlertAddress(): bool + private function shouldSendEmailToAlertAddress($acceptance = null): bool { - return Setting::getSettings() && Setting::getSettings()->admin_cc_email; + $setting = Setting::getSettings(); + + if (!$setting) { + return false; + } + + if (is_null($acceptance) && !$setting->admin_cc_always) { + return false; + } + + return (bool) $setting->admin_cc_email; } private function getFormattedAlertAddresses(): array From 0fb1639915f8e0e190d6b0f3ff9c7941aa2756ee Mon Sep 17 00:00:00 2001 From: spencerrlongg Date: Tue, 3 Jun 2025 18:06:41 -0500 Subject: [PATCH 046/661] this works! --- .../Assets/BulkAssetsController.php | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 443cd2b8fa..f70d40e5d8 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -214,15 +214,16 @@ class BulkAssetsController extends Controller } $custom_field_columns = CustomField::all()->pluck('db_column')->toArray(); - // find input attirubtes that start with 'null_' - $temp_custom_fields_to_null = array_filter($request->all(), function ($key) { + + // find custom field input attributes that start with 'null_' + $null_custom_fields_inputs = array_filter($request->all(), function ($key) { // filter out all keys that start with 'null_' return (strpos($key, 'null_') === 0); }, ARRAY_FILTER_USE_KEY);; - // remove 'null_' from the keys + // remove 'null' from the keys $custom_fields_to_null = []; - foreach ($temp_custom_fields_to_null as $key => $value) { - $custom_fields_to_null[str_replace('null_', '', $key)] = $value; + foreach ($null_custom_fields_inputs as $key => $value) { + $custom_fields_to_null[str_replace('null', '', $key)] = $value; } @@ -267,7 +268,7 @@ class BulkAssetsController extends Controller || ($request->filled('null_next_audit_date')) || ($request->filled('null_asset_eol_date')) || ($request->anyFilled($custom_field_columns)) - || ($request->anyFilled(array_keys($custom_fields_to_null))) + || ($request->anyFilled(array_keys($null_custom_fields_inputs))) ) { // Let's loop through those assets and build an update array @@ -295,7 +296,7 @@ class BulkAssetsController extends Controller $this->conditionallyAddItem($custom_field_column); } foreach ($custom_fields_to_null as $key => $custom_field_to_null) { - + //dd($key); $this->conditionallyAddItem($key); } @@ -451,6 +452,15 @@ class BulkAssetsController extends Controller if ($asset->model->fieldset) { foreach ($asset->model->fieldset->fields as $field) { + // null custom fields + if ($custom_fields_to_null) { + foreach ($custom_fields_to_null as $key => $custom_field_to_null) { + if ($field->db_column == $key) { + $this->update_array[$field->db_column] = null; + } + } + } + if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) { if (Gate::allows('admin')) { $decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column}); From 12d5e4f7d27c524a07843d1acdbc44f4fa287665 Mon Sep 17 00:00:00 2001 From: spencerrlongg Date: Tue, 3 Jun 2025 19:07:02 -0500 Subject: [PATCH 047/661] cleanup and test --- .../Assets/BulkAssetsController.php | 2 - .../Feature/Assets/Ui/BulkEditAssetsTest.php | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index f70d40e5d8..88ba51bf78 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -201,7 +201,6 @@ class BulkAssetsController extends Controller */ public function update(Request $request) : RedirectResponse { - //dd($request->all()); $this->authorize('update', Asset::class); $has_errors = 0; $error_array = array(); @@ -296,7 +295,6 @@ class BulkAssetsController extends Controller $this->conditionallyAddItem($custom_field_column); } foreach ($custom_fields_to_null as $key => $custom_field_to_null) { - //dd($key); $this->conditionallyAddItem($key); } diff --git a/tests/Feature/Assets/Ui/BulkEditAssetsTest.php b/tests/Feature/Assets/Ui/BulkEditAssetsTest.php index 44e9052482..58f7a11598 100644 --- a/tests/Feature/Assets/Ui/BulkEditAssetsTest.php +++ b/tests/Feature/Assets/Ui/BulkEditAssetsTest.php @@ -197,6 +197,47 @@ class BulkEditAssetsTest extends TestCase }); } + public function testBulkEditAssetsNullsCustomFieldsIfSelected() + { + $this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL'); + + CustomField::factory()->ram()->create(); + CustomField::factory()->cpu()->create(); + CustomField::factory()->phone()->create(); + + // when getting the custom field directly from the factory the field has not been fully created yet + // so we have to do a query afterwards to get the actual model :shrug: + + $ram = CustomField::where('name', 'RAM')->first(); + $cpu = CustomField::where('name', 'CPU')->first(); + $phone = CustomField::where('name', 'Phone Number')->first(); + + $assets = Asset::factory()->count(10)->hasMultipleCustomFields([$ram, $cpu])->create([ + $ram->db_column => 8, + $cpu->db_column => '2.1', + ]); + + $id_array = $assets->pluck('id')->toArray(); + + $this->actingAs(User::factory()->editAssets()->create())->post(route('hardware/bulksave'), [ + 'ids' => $id_array, + $ram->db_column => 16, + $cpu->db_column => '4.1', + 'null'.$phone->db_column => '8304997586', + ])->assertStatus(302); + + $this->actingAs(User::factory()->editAssets()->create())->post(route('hardware/bulksave'), [ + 'ids' => $id_array, + null.$phone->db_column => 1, + ]); + + Asset::findMany($id_array)->each(function (Asset $asset) use ($ram, $cpu, $phone) { + $this->assertEquals(16, $asset->{$ram->db_column}); + $this->assertEquals('4.1', $asset->{$cpu->db_column}); + $this->assertEquals(null, $asset->{$phone->db_column}); + }); + } + public function testBulkEditAssetsAcceptsAndUpdatesEncryptedCustomFields() { $this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL'); From 444c13c6ea3c2bbb9d6950bb399e88354eb87227 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 3 Jun 2025 17:10:15 -0700 Subject: [PATCH 048/661] Scaffold template --- .../views/livewire/category-edit-form.blade.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/resources/views/livewire/category-edit-form.blade.php b/resources/views/livewire/category-edit-form.blade.php index 91cb26da51..5ae814ca6c 100644 --- a/resources/views/livewire/category-edit-form.blade.php +++ b/resources/views/livewire/category-edit-form.blade.php @@ -64,6 +64,21 @@
    + @if ($requireAcceptance) +
    +
    + +
    +
    + @endif +
    From 84ec5aea26458e37b0919b13fa902e596323089e Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Wed, 4 Jun 2025 09:05:27 +0200 Subject: [PATCH 049/661] Manager View Feature --- app/Http/Controllers/SettingsController.php | 1 + app/Http/Controllers/ViewAssetsController.php | 91 +++++++++++++------ app/Models/Setting.php | 2 + app/Models/User.php | 65 +++++++++++++ ...manager_view_enabled_to_settings_table.php | 34 +++++++ .../lang/en-US/admin/settings/general.php | 3 + resources/lang/en-US/general.php | 3 + resources/views/account/view-assets.blade.php | 24 +++++ resources/views/settings/general.blade.php | 15 +++ 9 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 747f7b7284..2729b14a7e 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -352,6 +352,7 @@ class SettingsController extends Controller $setting->dash_chart_type = $request->input('dash_chart_type'); $setting->profile_edit = $request->input('profile_edit', 0); $setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0); + $setting->manager_view_enabled = $request->input('manager_view_enabled', 0); if ($request->input('per_page') != '') { diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index bbff6ba4f7..a54dff0881 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -27,50 +27,89 @@ use Exception; class ViewAssetsController extends Controller { /** - * Redirect to the profile page. + * Show user's assigned assets with optional manager view functionality. * */ - public function getIndex() : View | RedirectResponse + public function getIndex(Request $request) : View | RedirectResponse { - $user = User::with( + $authUser = auth()->user(); + $settings = Setting::getSettings(); + $subordinates = collect(); + $selectedUserId = $authUser->id; + + // Check if manager view is enabled and get subordinates if applicable + if ($settings->manager_view_enabled) { + // Get all subordinates including self, sorted for the dropdown + if ($authUser->isSuperUser()) { + // SuperAdmin sees all users + $subordinates = User::select('id', 'first_name', 'last_name', 'username') + ->where('activated', 1) + ->orderBy('last_name') + ->orderBy('first_name') + ->get(); + } else { + // Regular manager sees only their subordinates + self (recursive) + $managedUsers = $authUser->getAllSubordinates(false); // Don't include self yet + + // Only show dropdown if user actually has subordinates + if ($managedUsers->count() > 0) { + $subordinates = collect([$authUser])->merge($managedUsers) // Add self at beginning + ->sortBy('last_name') + ->sortBy('first_name'); + } else { + // User has no subordinates, so they only see themselves + $subordinates = collect([$authUser]); + } + } + + // If the user has subordinates and a user_id is provided in the request + if ($subordinates->count() > 1 && $request->filled('user_id')) { + $requestedUserId = (int) $request->input('user_id'); + + // Validate if the requested user is allowed (self or subordinate) + if ($subordinates->contains('id', $requestedUserId)) { + $selectedUserId = $requestedUserId; + } + // If invalid ID or not authorized, $selectedUserId remains $authUser->id (default) + } + } + + // Load the data for the user to be viewed (either auth user or selected subordinate) + $userToView = User::with([ 'assets', 'assets.model', 'assets.model.fieldset.fields', 'consumables', 'accessories', - 'licenses', - )->find(auth()->id()); + 'licenses' + ])->find($selectedUserId); - $field_array = array(); - - // Loop through all the custom fields that are applied to any model the user has assigned - foreach ($user->assets as $asset) { - - // Make sure the model has a custom fieldset before trying to loop through the associated fields - if ($asset->model->fieldset) { + // If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error + if (!$userToView) { + return redirect()->route('view-assets')->with('error', trans('admin/users/message.user_not_found')); + } + // Process custom fields for the user being viewed + $field_array = []; + foreach ($userToView->assets as $asset) { + if ($asset->model && $asset->model->fieldset) { foreach ($asset->model->fieldset->fields as $field) { - // check and make sure they're allowed to see the value of the custom field if ($field->display_in_user_view == '1') { $field_array[$field->db_column] = $field->name; } - } } - } + array_unique($field_array); // Remove duplicate field names - // Since some models may re-use the same fieldsets/fields, let's make the array unique so we don't repeat columns - array_unique($field_array); - - if (isset($user->id)) { - return view('account/view-assets', compact('user', 'field_array' )) - ->with('settings', Setting::getSettings()); - } - - // Redirect to the user management page - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', $user->id)); + // Pass the necessary data to the view + return view('account/view-assets', [ + 'user' => $userToView, // Use 'user' for compatibility with the existing view + 'field_array' => $field_array, + 'settings' => $settings, + 'subordinates' => $subordinates, + 'selectedUserId' => $selectedUserId + ]); } /** diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 199aee33dc..73fcada1e6 100755 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -67,11 +67,13 @@ class Setting extends Model 'google_login', 'google_client_id', 'google_client_secret', + 'manager_view_enabled', ]; protected $casts = [ 'label2_asset_logo' => 'boolean', 'require_checkinout_notes' => 'boolean', + 'manager_view_enabled' => 'boolean', ]; /** diff --git a/app/Models/User.php b/app/Models/User.php index 9b211e6a04..7856e5e53f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -956,5 +956,70 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo + } + + /** + * Get all direct and indirect subordinates for this user. + * + * @param bool $includeSelf Include the current user in the results + * @return \Illuminate\Support\Collection + */ + public function getAllSubordinates($includeSelf = false) + { + $subordinates = collect(); + if ($includeSelf) { + $subordinates->push($this); + } + + // Use a recursive helper function to avoid scope issues + $this->fetchSubordinatesRecursive($this, $subordinates); + + return $subordinates->unique('id'); // Ensure uniqueness + } + + /** + * Recursive helper function to fetch subordinates. + * + * @param User $manager + * @param \Illuminate\Support\Collection $subordinatesCollection + */ + protected function fetchSubordinatesRecursive(User $manager, \Illuminate\Support\Collection &$subordinatesCollection) + { + // Eager load 'managesUsers' to prevent N+1 queries in recursion + $directSubordinates = $manager->managesUsers()->with('managesUsers')->get(); + + foreach ($directSubordinates as $directSubordinate) { + // Add subordinate if not already in the collection + if (!$subordinatesCollection->contains('id', $directSubordinate->id)) { + $subordinatesCollection->push($directSubordinate); + // Recursive call for this subordinate's subordinates + $this->fetchSubordinatesRecursive($directSubordinate, $subordinatesCollection); + } + } + } + + /** + * Check if the current user is a direct or indirect manager of the given user. + * + * @param User $userToCheck + * @return bool + */ + public function isManagerOf(User $userToCheck): bool + { + // Optimization: If it's the same user, they are not their own manager + if ($this->id === $userToCheck->id) { + return false; + } + + // Eager load manager relationship to potentially reduce queries in the loop + $manager = $userToCheck->load('manager')->manager; + while ($manager) { + if ($manager->id === $this->id) { + return true; + } + // Move up the hierarchy (load relationship if not already loaded) + $manager = $manager->load('manager')->manager; + } + return false; } } diff --git a/database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php b/database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php new file mode 100644 index 0000000000..345c52a7dd --- /dev/null +++ b/database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php @@ -0,0 +1,34 @@ +boolean('manager_view_enabled') + ->default(false) + ->comment('Allow managers to view assets assigned to their subordinates'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasColumn('settings', 'manager_view_enabled')) { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('manager_view_enabled'); + }); + } + } +}; diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index 5da86537d6..fc45c61291 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -401,6 +401,9 @@ return [ 'due_checkin_days_help' => 'How many days before the expected checkin of an asset should it be listed in the "Due for checkin" page?', 'no_groups' => 'No groups have been created yet. Visit Admin Settings > Permission Groups to add one.', 'text' => 'Text', + 'manager_view' => 'Manager View', + 'manager_view_enabled_text' => 'Enable Manager View', + 'manager_view_enabled_help' => 'Allow managers to view assets assigned to their direct and indirect reports in their account view.', 'username_formats' => [ 'username_format' => 'Username Format', diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 599a15c139..e4adad7fd9 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -318,6 +318,9 @@ return [ 'viewall' => 'View All', 'viewassets' => 'View Assigned Assets', 'viewassetsfor' => 'View Assets for :name', + 'view_user_assets' => 'View User Assets', + 'select_user' => 'Select User', + 'me' => 'Me', 'website' => 'Website', 'welcome' => 'Welcome, :name', 'years' => 'years', diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 050f53263e..4c356778cf 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -9,6 +9,30 @@ {{-- Account page content --}} @section('content') +{{-- Manager View Dropdown --}} +@if (isset($settings) && $settings->manager_view_enabled && isset($subordinates) && $subordinates->count() > 1) +
    +
    +
    +
    + + +
    +
    +
    +
    +@endif @if ($acceptances = \App\Models\CheckoutAcceptance::forUser(Auth::user())->pending()->count())
    diff --git a/resources/views/settings/general.blade.php b/resources/views/settings/general.blade.php index 931fa47bcc..c9964b581f 100644 --- a/resources/views/settings/general.blade.php +++ b/resources/views/settings/general.blade.php @@ -325,6 +325,21 @@

    {{ trans('admin/settings/general.require_checkinout_notes_help_text') }}

    + + +
    +
    + {{ trans('admin/settings/general.manager_view') }} +
    +
    + +

    {{ trans('admin/settings/general.manager_view_enabled_help') }}

    + {!! $errors->first('manager_view_enabled', '') !!} +
    +
    From dc562d8c20916a97d2f248e2aacdad99417afbf6 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 4 Jun 2025 08:43:40 +0100 Subject: [PATCH 050/661] Remove error log Signed-off-by: snipe --- app/Http/Controllers/Assets/BulkAssetsController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 6fc25224f9..338f5b2763 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -613,7 +613,6 @@ class BulkAssetsController extends Controller // See if there is a status label passed if ($request->filled('status_id')) { - \Log::error('status id: ' . $request->get('status_id')); $asset->status_id = $request->get('status_id'); } From 3e1f71026c61970b1547545cb3cb6f3c4eed54bb Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 4 Jun 2025 10:37:04 +0100 Subject: [PATCH 051/661] Fixed #16240 - made additional strings translatable Signed-off-by: snipe --- resources/lang/en-US/general.php | 4 ++++ resources/views/layouts/default.blade.php | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 599a15c139..cda9efc221 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -581,6 +581,10 @@ return [ 'user_managed_passwords_allow' => 'Allow users to manage their own passwords', 'from' => 'From', 'by' => 'By', + 'by_lower' => 'by', + 'version' => 'Version', + 'build' => 'build', + 'footer_credit' => 'Snipe-IT is open source software, made with love by @snipeitapp.com.', // Add form placeholders here 'placeholders' => [ diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index 2dfe860bf1..4175cdf11a 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -921,15 +921,14 @@ dir="{{ Helper::determineLanguageDirection() }}">
    -
    - Snipe-IT is open source software, made with - love by @snipeitapp. +
    + {!! trans('general.footer_credit') !!}
    @if ($snipeSettings->version_footer!='off') @if (($snipeSettings->version_footer=='on') || (($snipeSettings->version_footer=='admin') && (Auth::user()->isSuperUser()=='1'))) -   Version {{ config('version.app_version') }} - - build {{ config('version.build_version') }} ({{ config('version.branch') }}) +   {{ trans('general.version') }} {{ config('version.app_version') }} - + {{ trans('general.build') }} {{ config('version.build_version') }} ({{ config('version.branch') }}) @endif @endif From c8c2867305e9ff4aeb7ba2b216ef337ffd569b77 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 4 Jun 2025 10:39:08 +0100 Subject: [PATCH 052/661] Remove unused translation Signed-off-by: snipe --- resources/lang/en-US/general.php | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index cda9efc221..83d49fecb9 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -581,7 +581,6 @@ return [ 'user_managed_passwords_allow' => 'Allow users to manage their own passwords', 'from' => 'From', 'by' => 'By', - 'by_lower' => 'by', 'version' => 'Version', 'build' => 'build', 'footer_credit' => 'Snipe-IT is open source software, made with love by @snipeitapp.com.', From 9aac183318e8d01cb2de281bc4d164e124f7b3f7 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 4 Jun 2025 10:40:05 +0100 Subject: [PATCH 053/661] Added aria tag for accessibility Signed-off-by: snipe --- resources/lang/en-US/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 83d49fecb9..657d31d407 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -583,7 +583,7 @@ return [ 'by' => 'By', 'version' => 'Version', 'build' => 'build', - 'footer_credit' => 'Snipe-IT is open source software, made with love by @snipeitapp.com.', + 'footer_credit' => 'Snipe-IT is open source software, made with love by @snipeitapp.com.', // Add form placeholders here 'placeholders' => [ From 092d9d1e42a94e495e8906dcd832ea2f1450f480 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Wed, 4 Jun 2025 11:23:09 +0100 Subject: [PATCH 054/661] Add deleted_at index to action_logs for people with many deleted action_logs --- ...36_add_deleted_at_index_to_action_logs.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_04_101736_add_deleted_at_index_to_action_logs.php diff --git a/database/migrations/2025_06_04_101736_add_deleted_at_index_to_action_logs.php b/database/migrations/2025_06_04_101736_add_deleted_at_index_to_action_logs.php new file mode 100644 index 0000000000..c9f853599b --- /dev/null +++ b/database/migrations/2025_06_04_101736_add_deleted_at_index_to_action_logs.php @@ -0,0 +1,28 @@ +index('deleted_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('action_logs', function (Blueprint $table) { + $table->dropIndex('deleted_at'); + }); + } +}; From fdb5ab229305a99db2d8e84051019da805f76709 Mon Sep 17 00:00:00 2001 From: snipe Date: Wed, 4 Jun 2025 11:46:38 +0100 Subject: [PATCH 055/661] Added advanced search to users Signed-off-by: snipe --- resources/views/users/index.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index cbb7e2bac9..c9b6924d91 100755 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -42,6 +42,7 @@ @include('partials.users-bulk-actions') Date: Wed, 4 Jun 2025 12:09:57 +0100 Subject: [PATCH 056/661] Fixed #17102 - added keywords to admin settings search for notifications Signed-off-by: snipe --- resources/lang/en-US/admin/settings/general.php | 1 + resources/views/settings/index.blade.php | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index 5da86537d6..9f0fa2c9e6 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -484,6 +484,7 @@ return [ 'php_overview' => 'phpinfo, system, info', 'purge' => 'permanently delete', 'security' => 'password, passwords, requirements, two factor, two-factor, common passwords, remote login, logout, authentication', + 'notifications' => 'alerts, email, notifications, audit, threshold, email alerts, cc', ], ]; diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index c580efdea9..c73df97f7a 100755 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -149,6 +149,7 @@

    {{ trans('admin/settings/general.notifications') }} + From 60989d67664a14c407d861e9b9b678a02fcf296a Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Wed, 4 Jun 2025 15:17:55 +0200 Subject: [PATCH 057/661] Fix Codacy warnings --- app/Http/Controllers/ViewAssetsController.php | 37 ++++++++++++------- app/Models/User.php | 14 +++---- resources/views/account/view-assets.blade.php | 1 + 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index a54dff0881..49503f370f 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -26,6 +26,27 @@ use Exception; */ class ViewAssetsController extends Controller { + /** + * Extract custom fields that should be displayed in user view. + * + * @param User $user + * @return array + */ + private function extractCustomFields(User $user): array + { + $fieldArray = []; + foreach ($user->assets as $asset) { + if ($asset->model && $asset->model->fieldset) { + foreach ($asset->model->fieldset->fields as $field) { + if ($field->display_in_user_view == '1') { + $fieldArray[$field->db_column] = $field->name; + } + } //end foreach + } + } //end foreach + return array_unique($fieldArray); + } + /** * Show user's assigned assets with optional manager view functionality. * @@ -59,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - } + } //end if } // If the user has subordinates and a user_id is provided in the request @@ -90,22 +111,12 @@ class ViewAssetsController extends Controller } // Process custom fields for the user being viewed - $field_array = []; - foreach ($userToView->assets as $asset) { - if ($asset->model && $asset->model->fieldset) { - foreach ($asset->model->fieldset->fields as $field) { - if ($field->display_in_user_view == '1') { - $field_array[$field->db_column] = $field->name; - } - } - } - } - array_unique($field_array); // Remove duplicate field names + $fieldArray = $this->extractCustomFields($userToView); // Pass the necessary data to the view return view('account/view-assets', [ 'user' => $userToView, // Use 'user' for compatibility with the existing view - 'field_array' => $field_array, + 'field_array' => $fieldArray, 'settings' => $settings, 'subordinates' => $subordinates, 'selectedUserId' => $selectedUserId diff --git a/app/Models/User.php b/app/Models/User.php index 7856e5e53f..43ff8fdf1f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -964,7 +964,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo * @param bool $includeSelf Include the current user in the results * @return \Illuminate\Support\Collection */ - public function getAllSubordinates($includeSelf = false) + public function getAllSubordinates($includeSelf=false) { $subordinates = collect(); if ($includeSelf) { @@ -981,21 +981,21 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo * Recursive helper function to fetch subordinates. * * @param User $manager - * @param \Illuminate\Support\Collection $subordinatesCollection + * @param \Illuminate\Support\Collection $subs */ - protected function fetchSubordinatesRecursive(User $manager, \Illuminate\Support\Collection &$subordinatesCollection) + protected function fetchSubordinatesRecursive(User $manager, \Illuminate\Support\Collection &$subs) { // Eager load 'managesUsers' to prevent N+1 queries in recursion $directSubordinates = $manager->managesUsers()->with('managesUsers')->get(); foreach ($directSubordinates as $directSubordinate) { // Add subordinate if not already in the collection - if (!$subordinatesCollection->contains('id', $directSubordinate->id)) { - $subordinatesCollection->push($directSubordinate); + if (!$subs->contains('id', $directSubordinate->id)) { + $subs->push($directSubordinate); // Recursive call for this subordinate's subordinates - $this->fetchSubordinatesRecursive($directSubordinate, $subordinatesCollection); + $this->fetchSubordinatesRecursive($directSubordinate, $subs); } - } + } //end foreach } /** diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 4c356778cf..742fab66b3 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -14,6 +14,7 @@
    + @csrf
    @stop +@section('moar_scripts') + +@endsection diff --git a/resources/views/models/custom_fields_form_bulk_edit.blade.php b/resources/views/models/custom_fields_form_bulk_edit.blade.php index 68a42a8006..3efca73258 100644 --- a/resources/views/models/custom_fields_form_bulk_edit.blade.php +++ b/resources/views/models/custom_fields_form_bulk_edit.blade.php @@ -30,17 +30,14 @@ :selected="old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id)))" class="format form-control" /> - @elseif ($field->element=='textarea') @if($field->is_unique) @endif - @if(!$field->is_unique) + @if(!$field->is_unique) - @endif + @endif @elseif ($field->element=='checkbox') @foreach ($field->formatFieldValuesAsArray() as $key => $value) @@ -48,9 +45,7 @@ {$field->db_column_name()}))) ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($key, array_map('trim', explode(',', $field->defaultValue($model->id)))) ? ' checked="checked"' : '')) }}> {{ $value }} - + @endforeach @elseif ($field->element=='radio') @foreach ($field->formatFieldValuesAsArray() as $value) @@ -59,11 +54,13 @@ {$field->db_column_name()} == $value ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($value, explode(', ', $field->defaultValue($model->id))) ? ' checked="checked"' : '')) }}> {{ $value }} - - @endforeach + @endforeach + @endif @else @@ -126,4 +123,4 @@
    @endforeach @endif - @endforeach + @endforeach From 6f4cee6334b7ed0d2db03a16b38162a9f6c6d510 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Jun 2025 11:38:50 -0700 Subject: [PATCH 061/661] adds Eula tab and count to user account --- app/Http/Controllers/ViewAssetsController.php | 1 + app/Models/User.php | 17 +++++ resources/lang/en-US/general.php | 1 + resources/views/account/view-assets.blade.php | 62 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index bbff6ba4f7..6c35ca2e05 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -39,6 +39,7 @@ class ViewAssetsController extends Controller 'consumables', 'accessories', 'licenses', + 'eulas', )->find(auth()->id()); $field_array = array(); diff --git a/app/Models/User.php b/app/Models/User.php index 9b211e6a04..5c6fc14ff0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -551,6 +551,23 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo ->orderBy('created_at', 'desc'); } + /** + * Establishes the user -> eula relationship + * + * @author A. Gianotto + * @since [v7.0.7] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function eulas() + { + return $this->hasMany(Actionlog::class, 'target_id') + ->where('target_type', self::class) + ->where('action_type', 'accepted') + ->whereNotNull('filename') + ->whereNotNull('accept_signature') + ->orderBy('created_at', 'desc'); + } + /** * Establishes the user -> requested assets relationship * diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 657d31d407..54fe8a7f31 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -287,6 +287,7 @@ return [ 'status_label' => 'Status Label', 'status' => 'Status', 'accept_eula' => 'Acceptance Agreement', + 'eula' => 'EULAs', 'show_or_hide_eulas' => 'Show/Hide EULAs', 'supplier' => 'Supplier', 'suppliers' => 'Suppliers', diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 050f53263e..830b05c08c 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -87,6 +87,17 @@ +
  • + + + + +
  • +
    @@ -703,7 +714,58 @@
    +
    + + + + + + + @can('self.view_purchase_cost') + + @endcan + + + + + + @foreach ($user->consumables as $consumable) + + + @can('self.view_purchase_cost') + + @endcan + + + + @endforeach + +
    + {{ trans('general.consumables') }} +
    {{ trans('general.name') }}{{ trans('general.purchase_cost') }}{{ trans('general.date') }}{{ trans('general.notes') }}
    {{ $consumable->name }} + {!! Helper::formatCurrencyOutput($consumable->purchase_cost) !!} + {{ Helper::getFormattedDateObject($consumable->pivot->created_at, 'datetime', false) }}{{ $consumable->pivot->note }}
    +
    From 8bc57f98a5a8ab25497e128038b9ee0ee1f43d6e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 4 Jun 2025 11:51:29 -0700 Subject: [PATCH 062/661] Split test case --- ...ationsToAdminAlertEmailUponCheckinTest.php | 85 +++++++++++++++++++ ...ailNotificationsToUserUponCheckinTest.php} | 55 +----------- 2 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php rename tests/Feature/Notifications/Email/{EmailNotificationsUponCheckinTest.php => EmailNotificationsToUserUponCheckinTest.php} (64%) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php new file mode 100644 index 0000000000..ec2481b59d --- /dev/null +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php @@ -0,0 +1,85 @@ +settings->enableAdminCC('cc@example.com'); + + $user = User::factory()->create(); + $asset = Asset::factory()->assignedToUser($user)->create(); + + $asset->model->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($asset, $user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) use ($user) { + return $mail->hasTo($user->email) && $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_category_email_is_not_set_to_send_email_to_user() + { + $this->settings->enableAdminCC('cc@example.com'); + + $category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'use_default_eula' => false, + ]); + $assetModel = AssetModel::factory()->create(['category_id' => $category->id]); + $asset = Asset::factory()->create(['model_id' => $assetModel->id]); + + $this->fireCheckInEvent($asset, User::factory()->create()); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_user_has_no_email_address() + { + $this->settings->enableAdminCC('cc@example.com'); + + $user = User::factory()->create(['email' => null]); + $asset = Asset::factory()->assignedToUser($user)->create(); + + $asset->model->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($asset, $user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + private function fireCheckInEvent($asset, $user): void + { + event(new CheckoutableCheckedIn( + $asset, + $user, + User::factory()->checkinAssets()->create(), + '' + )); + } +} diff --git a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckinTest.php similarity index 64% rename from tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php rename to tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckinTest.php index 254e2e09ca..432ce88b4b 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToUserUponCheckinTest.php @@ -4,8 +4,6 @@ namespace Tests\Feature\Notifications\Email; use App\Mail\CheckinAssetMail; use App\Models\Accessory; -use App\Models\AssetModel; -use App\Models\Category; use App\Models\Consumable; use App\Models\LicenseSeat; use Illuminate\Support\Facades\Mail; @@ -16,7 +14,7 @@ use App\Models\User; use Tests\TestCase; #[Group('notifications')] -class EmailNotificationsUponCheckinTest extends TestCase +class EmailNotificationsToUserUponCheckinTest extends TestCase { protected function setUp(): void { @@ -101,57 +99,6 @@ class EmailNotificationsUponCheckinTest extends TestCase Mail::assertNothingSent(); } - public function test_admin_alert_email_sends() - { - $this->settings->enableAdminCC('cc@example.com'); - - $user = User::factory()->create(); - $asset = Asset::factory()->assignedToUser($user)->create(); - - $asset->model->category->update(['checkin_email' => true]); - - $this->fireCheckInEvent($asset, $user); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && $mail->hasCc('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_category_email_is_not_set_to_send_email_to_user() - { - $this->settings->enableAdminCC('cc@example.com'); - - $category = Category::factory()->create([ - 'checkin_email' => false, - 'eula_text' => null, - 'use_default_eula' => false, - ]); - $assetModel = AssetModel::factory()->create(['category_id' => $category->id]); - $asset = Asset::factory()->create(['model_id' => $assetModel->id]); - - $this->fireCheckInEvent($asset, User::factory()->create()); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - - public function test_admin_alert_email_still_sent_when_user_has_no_email_address() - { - $this->settings->enableAdminCC('cc@example.com'); - - $user = User::factory()->create(['email' => null]); - $asset = Asset::factory()->assignedToUser($user)->create(); - - $asset->model->category->update(['checkin_email' => true]); - - $this->fireCheckInEvent($asset, $user); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) { - return $mail->hasTo('cc@example.com'); - }); - } - private function fireCheckInEvent($asset, $user): void { event(new CheckoutableCheckedIn( From 5ec52f7471095b05929372bb19d4f61c1fecb8d9 Mon Sep 17 00:00:00 2001 From: akemidx Date: Wed, 4 Jun 2025 15:10:16 -0400 Subject: [PATCH 063/661] beginning of everything. tried some stuff but yea. something is up --- app/Presenters/ConsumablePresenter.php | 16 ++++++++-------- resources/views/consumables/view.blade.php | 11 +++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/Presenters/ConsumablePresenter.php b/app/Presenters/ConsumablePresenter.php index fd4cee272c..e777cfe26b 100644 --- a/app/Presenters/ConsumablePresenter.php +++ b/app/Presenters/ConsumablePresenter.php @@ -72,14 +72,6 @@ class ConsumablePresenter extends Presenter 'searchable' => true, 'sortable' => true, 'title' => trans('admin/consumables/general.item_no'), - ], [ - 'field' => 'min_amt', - 'searchable' => false, - 'sortable' => true, - 'title' => trans('general.min_amt'), - 'visible' => true, - 'formatter' => 'minAmtFormatter', - 'class' => 'text-right text-padding-number-cell', ], [ 'field' => 'qty', 'searchable' => false, @@ -96,6 +88,14 @@ class ConsumablePresenter extends Presenter 'visible' => true, 'class' => 'text-right text-padding-number-cell', 'footerFormatter' => 'qtySumFormatter', + ], [ + 'field' => 'min_amt', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.min_amt'), + 'visible' => true, + 'formatter' => 'minAmtFormatter', + 'class' => 'text-right text-padding-number-cell', ], [ 'field' => 'location', 'searchable' => true, diff --git a/resources/views/consumables/view.blade.php b/resources/views/consumables/view.blade.php index 4ef82efc8e..ab235004ab 100644 --- a/resources/views/consumables/view.blade.php +++ b/resources/views/consumables/view.blade.php @@ -189,6 +189,17 @@ @endif + + @if ($consumable->qty) +
    +
    + {{ trans('admin/components/general.total') }} +
    +
    + {{ $consumable->qty }} +
    +
    + @endif @if ($consumable->numRemaining()) From 8e70ff135a65d52edf2034b8777af1ec727aa96b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 4 Jun 2025 12:44:55 -0700 Subject: [PATCH 064/661] Scaffold tests --- ...ilNotificationsToAdminAlertEmailUponCheckinTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php index ec2481b59d..47cfb2e492 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php @@ -73,6 +73,16 @@ class EmailNotificationsToAdminAlertEmailUponCheckinTest extends TestCase }); } + public function test_admin_alert_email_sent_when_always_send_is_true_and_asset_does_not_require_acceptance() + { + $this->markTestIncomplete(); + } + + public function test_admin_alert_email_not_sent_when_always_send_is_false_and_asset_does_not_require_acceptance() + { + $this->markTestIncomplete(); + } + private function fireCheckInEvent($asset, $user): void { event(new CheckoutableCheckedIn( From 31db86abd3aa45fcee6546003195f5be4659c4ee Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 4 Jun 2025 12:51:48 -0700 Subject: [PATCH 065/661] Simplify test --- ...ationsToAdminAlertEmailUponCheckinTest.php | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php index 47cfb2e492..a65baa1a5a 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php @@ -15,26 +15,45 @@ use Tests\TestCase; #[Group('notifications')] class EmailNotificationsToAdminAlertEmailUponCheckinTest extends TestCase { + private Asset $asset; + private AssetModel $assetModel; + private Category $category; + private User $user; + protected function setUp(): void { parent::setUp(); Mail::fake(); + + $this->user = User::factory()->create(); + + $this->category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + + $this->assetModel = AssetModel::factory()->for($this->category)->create(); + + $this->asset = Asset::factory() + ->for($this->assetModel, 'model') + ->assignedToUser($this->user) + ->create(); + } public function test_admin_alert_email_sends() { $this->settings->enableAdminCC('cc@example.com'); - $user = User::factory()->create(); - $asset = Asset::factory()->assignedToUser($user)->create(); + $this->category->update(['checkin_email' => true]); - $asset->model->category->update(['checkin_email' => true]); + $this->fireCheckInEvent($this->asset, $this->user); - $this->fireCheckInEvent($asset, $user); - - Mail::assertSent(CheckinAssetMail::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && $mail->hasCc('cc@example.com'); + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo($this->user->email) && $mail->hasCc('cc@example.com'); }); } @@ -42,15 +61,9 @@ class EmailNotificationsToAdminAlertEmailUponCheckinTest extends TestCase { $this->settings->enableAdminCC('cc@example.com'); - $category = Category::factory()->create([ - 'checkin_email' => false, - 'eula_text' => null, - 'use_default_eula' => false, - ]); - $assetModel = AssetModel::factory()->create(['category_id' => $category->id]); - $asset = Asset::factory()->create(['model_id' => $assetModel->id]); + $this->category->update(['checkin_email' => false]); - $this->fireCheckInEvent($asset, User::factory()->create()); + $this->fireCheckInEvent($this->asset, $this->user); Mail::assertSent(CheckinAssetMail::class, function ($mail) { return $mail->hasTo('cc@example.com'); @@ -61,12 +74,11 @@ class EmailNotificationsToAdminAlertEmailUponCheckinTest extends TestCase { $this->settings->enableAdminCC('cc@example.com'); - $user = User::factory()->create(['email' => null]); - $asset = Asset::factory()->assignedToUser($user)->create(); + $this->user->update(['email' => null]); - $asset->model->category->update(['checkin_email' => true]); + $this->category->update(['checkin_email' => true]); - $this->fireCheckInEvent($asset, $user); + $this->fireCheckInEvent($this->asset, $this->user); Mail::assertSent(CheckinAssetMail::class, function ($mail) { return $mail->hasTo('cc@example.com'); From 92e22eead55b1e9d3da970ed95ea139731ba820c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 4 Jun 2025 12:51:59 -0700 Subject: [PATCH 066/661] Formatting --- .../Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php index a65baa1a5a..f2ce26e712 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php @@ -41,7 +41,6 @@ class EmailNotificationsToAdminAlertEmailUponCheckinTest extends TestCase ->for($this->assetModel, 'model') ->assignedToUser($this->user) ->create(); - } public function test_admin_alert_email_sends() From 10be434c137fdfad4718fa2e6ce7dfd0d688009b Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 4 Jun 2025 12:58:49 -0700 Subject: [PATCH 067/661] Populate tests --- ...ationsToAdminAlertEmailUponCheckinTest.php | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php index f2ce26e712..1bdbf0b673 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsToAdminAlertEmailUponCheckinTest.php @@ -86,12 +86,32 @@ class EmailNotificationsToAdminAlertEmailUponCheckinTest extends TestCase public function test_admin_alert_email_sent_when_always_send_is_true_and_asset_does_not_require_acceptance() { - $this->markTestIncomplete(); + $this->settings + ->enableAdminCC('cc@example.com') + ->enableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); } public function test_admin_alert_email_not_sent_when_always_send_is_false_and_asset_does_not_require_acceptance() { - $this->markTestIncomplete(); + $this->settings + ->enableAdminCC('cc@example.com') + ->disableAdminCCAlways(); + + $this->category->update(['checkin_email' => false]); + + $this->fireCheckInEvent($this->asset, $this->user); + + Mail::assertNotSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com') || $mail->hasCc('cc@example.com'); + }); } private function fireCheckInEvent($asset, $user): void From c1505de8d6915623acf2545309040f4b0ca0769c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 4 Jun 2025 13:02:04 -0700 Subject: [PATCH 068/661] Update wording --- resources/views/settings/alerts.blade.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 3a472fbe20..59c1888d2f 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -107,7 +107,7 @@ value="1" @checked($setting->admin_cc_always == 1) > - Always send copy to CC email + Always send copy upon checkin/checkout -

    - "Always send copy to CC email" requires valid CC Email to be set. -

    From ee3deb9c63a285e7231926f722025fc4bb9fd14d Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Jun 2025 15:04:59 -0700 Subject: [PATCH 069/661] made eula api route, formatted table, cleaned up code --- app/Http/Controllers/Api/UsersController.php | 20 ++++++++++- app/Http/Controllers/ViewAssetsController.php | 1 - app/Models/User.php | 1 + resources/lang/en-US/general.php | 1 + resources/views/account/view-assets.blade.php | 33 ++++++------------- routes/api.php | 8 +++++ 6 files changed, 39 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 09dadbbd3c..7a83feef9e 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -6,6 +6,7 @@ use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Http\Requests\SaveUserRequest; use App\Http\Transformers\AccessoriesTransformer; +use App\Http\Transformers\ActionlogsTransformer; use App\Http\Transformers\AssetsTransformer; use App\Http\Transformers\ConsumablesTransformer; use App\Http\Transformers\LicensesTransformer; @@ -80,7 +81,7 @@ class UsersController extends Controller 'users.autoassign_licenses', 'users.website', - ])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations') + ])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations', 'eulas') ->withCount([ 'assets as assets_count' => function(Builder $query) { $query->withoutTrashed(); @@ -736,6 +737,23 @@ class UsersController extends Controller return (new UsersTransformer)->transformUser($request->user()); } + /** + * Display the EULAs accepted by the user. + * + * @param \App\Models\User $user + * * @param \App\Http\Transformers\ActionlogsTransformer $transformer + * * @return \Illuminate\Http\JsonResponse + *@since [v8.1.16] + * @author [Godfrey Martinez] [] + */ + public function eulas(User $user, ActionlogsTransformer $transformer) + { + $eulas = $user->eulas; + return response()->json( + $transformer->transformActionlogs($eulas, $eulas->count()) + ); + } + /** * Restore a soft-deleted user. * diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 6c35ca2e05..bbff6ba4f7 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -39,7 +39,6 @@ class ViewAssetsController extends Controller 'consumables', 'accessories', 'licenses', - 'eulas', )->find(auth()->id()); $field_array = array(); diff --git a/app/Models/User.php b/app/Models/User.php index 5c6fc14ff0..bd24a62370 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -561,6 +561,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo public function eulas() { return $this->hasMany(Actionlog::class, 'target_id') + ->with('item') ->where('target_type', self::class) ->where('action_type', 'accepted') ->whereNotNull('filename') diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 54fe8a7f31..628f338c83 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -288,6 +288,7 @@ return [ 'status' => 'Status', 'accept_eula' => 'Acceptance Agreement', 'eula' => 'EULAs', + 'eula_long' => 'End-User License Agreements', 'show_or_hide_eulas' => 'Show/Hide EULAs', 'supplier' => 'Supplier', 'suppliers' => 'Suppliers', diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 830b05c08c..d51e01f328 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -731,41 +731,28 @@ data-sort-order="asc" data-sort-name="name" class="table table-striped snipe-table table-hover" + data-url="{{route('api.user.eulas', $user->id)}}" data-export-options='{ "fileName": "export-eula-{{ str_slug($user->username) }}-{{ date('Y-m-d') }}", "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","delete","purchasecost", "icon"] }'> - - {{ trans('general.consumables') }} + + {{ trans('general.eula_long') }} - {{ trans('general.name') }} - @can('self.view_purchase_cost') - {{ trans('general.purchase_cost') }} - @endcan - {{ trans('general.date') }} - {{ trans('general.notes') }} + {{ trans('admin/hardware/table.icon') }} + {{ trans('general.item') }} + {{ trans('general.accepted_date') }} + {{ trans('general.notes') }} + {{ trans('general.signature') }} + {{ trans('general.download') }} - - @foreach ($user->consumables as $consumable) - - {{ $consumable->name }} - @can('self.view_purchase_cost') - - {!! Helper::formatCurrencyOutput($consumable->purchase_cost) !!} - - @endcan - {{ Helper::getFormattedDateObject($consumable->pivot->created_at, 'datetime', false) }} - {{ $consumable->pivot->note }} - - @endforeach - - + diff --git a/routes/api.php b/routes/api.php index 8d9391d2a5..06317410a9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1094,6 +1094,14 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu ] )->name('api.users.me'); + Route::get('/users/{user}/eulas', + [ + Api\UsersController::class, + 'eulas' + ] + )->name('api.user.eulas'); + + Route::get('list/{status?}', [ Api\UsersController::class, From 68c082e0dcc84120a42405b0c44e22adba0987ab Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Jun 2025 15:48:35 -0700 Subject: [PATCH 070/661] fix doc blocks --- app/Http/Controllers/Api/UsersController.php | 6 +++--- app/Models/User.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 7a83feef9e..831a943423 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -740,9 +740,9 @@ class UsersController extends Controller /** * Display the EULAs accepted by the user. * - * @param \App\Models\User $user - * * @param \App\Http\Transformers\ActionlogsTransformer $transformer - * * @return \Illuminate\Http\JsonResponse + * @param \App\Models\User $user + * @param \App\Http\Transformers\ActionlogsTransformer $transformer + * @return \Illuminate\Http\JsonResponse *@since [v8.1.16] * @author [Godfrey Martinez] [] */ diff --git a/app/Models/User.php b/app/Models/User.php index bd24a62370..6fcb8364dc 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -554,10 +554,10 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo /** * Establishes the user -> eula relationship * - * @author A. Gianotto - * @since [v7.0.7] * @return \Illuminate\Database\Eloquent\Relations\Relation - */ + * @since [v8.1.16] + * @author [Godfrey Martinez] [] + */ public function eulas() { return $this->hasMany(Actionlog::class, 'target_id') From fb18c1a0bea37d6575cf2a1fb48e0c083d534f12 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 08:59:07 +0200 Subject: [PATCH 071/661] Fix code style: Remove whitespace in end block comments --- app/Http/Controllers/ViewAssetsController.php | 6 +++--- app/Models/User.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index d73068afe7..ee4916f0a7 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -41,9 +41,9 @@ class ViewAssetsController extends Controller if ($field->display_in_user_view == '1') { $fieldArray[$field->db_column] = $field->name; } - }//end foreach + }//endforeach } - }//end foreach + }//endforeach return array_unique($fieldArray); } @@ -80,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - }//end if + }//endif } // If the user has subordinates and a user_id is provided in the request diff --git a/app/Models/User.php b/app/Models/User.php index affd76a729..825fba640b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1000,7 +1000,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo // Recursive call for this subordinate's subordinates $this->fetchSubordinatesRecursive($directSubordinate, $subs); } - }//end foreach + }//endforeach } /** From 61d3e2fb493f722384c4b61ff4dc629d00b614c5 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 09:23:58 +0200 Subject: [PATCH 072/661] Fix code style: Add required whitespace in end block comments --- app/Http/Controllers/ViewAssetsController.php | 6 +++--- app/Models/User.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index ee4916f0a7..d73068afe7 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -41,9 +41,9 @@ class ViewAssetsController extends Controller if ($field->display_in_user_view == '1') { $fieldArray[$field->db_column] = $field->name; } - }//endforeach + }//end foreach } - }//endforeach + }//end foreach return array_unique($fieldArray); } @@ -80,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - }//endif + }//end if } // If the user has subordinates and a user_id is provided in the request diff --git a/app/Models/User.php b/app/Models/User.php index 825fba640b..affd76a729 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1000,7 +1000,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo // Recursive call for this subordinate's subordinates $this->fetchSubordinatesRecursive($directSubordinate, $subs); } - }//endforeach + }//end foreach } /** From 7ff82e60433d0306fe2c093a094174eca8ca0430 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 11:07:42 +0200 Subject: [PATCH 073/661] Delete comments --- app/Http/Controllers/ViewAssetsController.php | 6 +++--- app/Models/User.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index d73068afe7..f27c75a169 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -41,9 +41,9 @@ class ViewAssetsController extends Controller if ($field->display_in_user_view == '1') { $fieldArray[$field->db_column] = $field->name; } - }//end foreach + } } - }//end foreach + } return array_unique($fieldArray); } @@ -80,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - }//end if + } } // If the user has subordinates and a user_id is provided in the request diff --git a/app/Models/User.php b/app/Models/User.php index affd76a729..b70fbc1ca8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1000,7 +1000,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo // Recursive call for this subordinate's subordinates $this->fetchSubordinatesRecursive($directSubordinate, $subs); } - }//end foreach + } } /** From 30c090ba2d945485655b1a30728f51779c926834 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 11:45:05 +0200 Subject: [PATCH 074/661] Fix code style: Add descriptive end comment for conditional block --- app/Http/Controllers/ViewAssetsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index f27c75a169..3fe8de03c8 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -80,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - } + }//end if managedUsers count > 0 } // If the user has subordinates and a user_id is provided in the request From 62e50dbe5297851c93edc1ab00ab857f99f1993b Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 11:56:13 +0200 Subject: [PATCH 075/661] Fix code style: Add semantically correct end comments --- app/Http/Controllers/ViewAssetsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 3fe8de03c8..2e370e1296 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -80,8 +80,8 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - }//end if managedUsers count > 0 - } + }//end if + }//end if // If the user has subordinates and a user_id is provided in the request if ($subordinates->count() > 1 && $request->filled('user_id')) { From 7494fa6bc92b81612f75b3decf78957f307d3bf5 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 12:03:42 +0200 Subject: [PATCH 076/661] Fix code style: Remove unnecessary end comment from short conditional block --- app/Http/Controllers/ViewAssetsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 2e370e1296..2cf2f37751 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -80,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - }//end if + } }//end if // If the user has subordinates and a user_id is provided in the request From 6291389df50f1da86307487636fe9d06981799c4 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 5 Jun 2025 11:53:57 +0100 Subject: [PATCH 077/661] Fixed #16934 - update asset by ID in importer Signed-off-by: snipe --- app/Importer/AssetImporter.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php index ad69028055..62938fe391 100644 --- a/app/Importer/AssetImporter.php +++ b/app/Importer/AssetImporter.php @@ -80,7 +80,16 @@ class AssetImporter extends ItemImporter $asset_tag = Asset::autoincrement_asset(); } - $asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first(); + + + if ($this->findCsvMatch($row, 'id')!='') { + // Override location if an ID was given + \Log::debug('Finding asset by ID: '.$this->findCsvMatch($row, 'id')); + $asset = Asset::find($this->findCsvMatch($row, 'id')); + } else { + $asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first(); + } + if ($asset) { if (! $this->updating) { $exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]); From e33f73fe9f872d68ce0ca56dc4e5075aa8c8474e Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 5 Jun 2025 11:56:14 +0100 Subject: [PATCH 078/661] Fixed comment text Signed-off-by: snipe --- app/Importer/AssetImporter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php index 62938fe391..1d03d6f95b 100644 --- a/app/Importer/AssetImporter.php +++ b/app/Importer/AssetImporter.php @@ -83,7 +83,7 @@ class AssetImporter extends ItemImporter if ($this->findCsvMatch($row, 'id')!='') { - // Override location if an ID was given + // Override asset if an ID was given \Log::debug('Finding asset by ID: '.$this->findCsvMatch($row, 'id')); $asset = Asset::find($this->findCsvMatch($row, 'id')); } else { From 7a93e94fa65dad6d5afed84fcddda2f7362afa15 Mon Sep 17 00:00:00 2001 From: snipe Date: Thu, 5 Jun 2025 12:35:30 +0100 Subject: [PATCH 079/661] Add ID to field list Signed-off-by: snipe --- app/Livewire/Importer.php | 1 + tests/Support/Importing/AssetsImportFileBuilder.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/Livewire/Importer.php b/app/Livewire/Importer.php index eef569e314..4802eb8125 100644 --- a/app/Livewire/Importer.php +++ b/app/Livewire/Importer.php @@ -203,6 +203,7 @@ class Importer extends Component ]; $this->assets_fields = [ + 'id' => trans('general.id'), 'asset_eol_date' => trans('admin/hardware/form.eol_date'), 'asset_model' => trans('general.model_name'), 'asset_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/general.asset')]), diff --git a/tests/Support/Importing/AssetsImportFileBuilder.php b/tests/Support/Importing/AssetsImportFileBuilder.php index df573536c5..381cf5dffc 100644 --- a/tests/Support/Importing/AssetsImportFileBuilder.php +++ b/tests/Support/Importing/AssetsImportFileBuilder.php @@ -40,6 +40,7 @@ class AssetsImportFileBuilder extends FileBuilder protected function getDictionary(): array { return [ + 'id' => 'ID', 'assigneeFullName' => 'Full Name', 'assigneeEmail' => 'Email', 'assigneeUsername' => 'Username', @@ -69,6 +70,7 @@ class AssetsImportFileBuilder extends FileBuilder $faker = fake(); return [ + 'asset_tag' => Str::random(), 'assigneeFullName' => $faker->name, 'assigneeEmail' => $faker->email, 'assigneeUsername' => $faker->userName, From 29989ac24e43d2315e68f58b724a78e46a52323a Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 13:50:57 +0200 Subject: [PATCH 080/661] Fix code style: Remove empty line after end comment --- app/Http/Controllers/ViewAssetsController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 2cf2f37751..78b45f26d6 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -82,7 +82,6 @@ class ViewAssetsController extends Controller $subordinates = collect([$authUser]); } }//end if - // If the user has subordinates and a user_id is provided in the request if ($subordinates->count() > 1 && $request->filled('user_id')) { $requestedUserId = (int) $request->input('user_id'); From 261f84d5f501f486428d307a975ffd9f61607277 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 17:55:46 +0200 Subject: [PATCH 081/661] Fix code style: Remove comment --- app/Http/Controllers/ViewAssetsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 78b45f26d6..a654334442 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -81,7 +81,7 @@ class ViewAssetsController extends Controller // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); } - }//end if + } // If the user has subordinates and a user_id is provided in the request if ($subordinates->count() > 1 && $request->filled('user_id')) { $requestedUserId = (int) $request->input('user_id'); From 8b98ae15f0f82f373d0b2b3e3e0522a832127dbc Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 18:08:34 +0200 Subject: [PATCH 082/661] Fix code style: Add comment --- app/Http/Controllers/ViewAssetsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index a654334442..8ac752e2b0 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -92,7 +92,7 @@ class ViewAssetsController extends Controller } // If invalid ID or not authorized, $selectedUserId remains $authUser->id (default) } - } + }//end if // Load the data for the user to be viewed (either auth user or selected subordinate) $userToView = User::with([ From c50c97d149db9b9b4c8f13a5cb150b07673db5e4 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 18:18:07 +0200 Subject: [PATCH 083/661] Fix code style: Change comments --- app/Http/Controllers/ViewAssetsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 8ac752e2b0..0b5ef71d14 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -81,7 +81,7 @@ class ViewAssetsController extends Controller // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); } - } + }//end foreach // If the user has subordinates and a user_id is provided in the request if ($subordinates->count() > 1 && $request->filled('user_id')) { $requestedUserId = (int) $request->input('user_id'); @@ -92,7 +92,7 @@ class ViewAssetsController extends Controller } // If invalid ID or not authorized, $selectedUserId remains $authUser->id (default) } - }//end if + } // Load the data for the user to be viewed (either auth user or selected subordinate) $userToView = User::with([ From ad6fe855a968ad5255c28ee10a8a005b81289c29 Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 18:53:36 +0200 Subject: [PATCH 084/661] Fix code style: Change comments --- app/Http/Controllers/ViewAssetsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 0b5ef71d14..38bf32eee2 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -81,7 +81,7 @@ class ViewAssetsController extends Controller // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); } - }//end foreach + }//end if // If the user has subordinates and a user_id is provided in the request if ($subordinates->count() > 1 && $request->filled('user_id')) { $requestedUserId = (int) $request->input('user_id'); @@ -92,7 +92,7 @@ class ViewAssetsController extends Controller } // If invalid ID or not authorized, $selectedUserId remains $authUser->id (default) } - } + }//end if // Load the data for the user to be viewed (either auth user or selected subordinate) $userToView = User::with([ From e2e54677ee5827f110e8321581085f8bc13f67ca Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Thu, 5 Jun 2025 10:59:16 -0700 Subject: [PATCH 085/661] add auth to api call, gave more specificity to the relationship --- app/Http/Controllers/Api/UsersController.php | 2 ++ app/Models/User.php | 1 + routes/api.php | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 831a943423..69ddfc54f6 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -748,6 +748,8 @@ class UsersController extends Controller */ public function eulas(User $user, ActionlogsTransformer $transformer) { + $this->authorize('view', Asset::class); + $eulas = $user->eulas; return response()->json( $transformer->transformActionlogs($eulas, $eulas->count()) diff --git a/app/Models/User.php b/app/Models/User.php index 6fcb8364dc..a3a66aef0e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -562,6 +562,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo { return $this->hasMany(Actionlog::class, 'target_id') ->with('item') + ->select(['id', 'target_id', 'target_type', 'action_type', 'filename', 'accept_signature', 'created_at']) ->where('target_type', self::class) ->where('action_type', 'accepted') ->whereNotNull('filename') diff --git a/routes/api.php b/routes/api.php index 06317410a9..2088dbe41b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1094,7 +1094,7 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu ] )->name('api.users.me'); - Route::get('/users/{user}/eulas', + Route::get('{user}/eulas', [ Api\UsersController::class, 'eulas' From 16fb1018a248c6e914ed5149924d02b41606a91a Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Thu, 5 Jun 2025 20:05:38 +0200 Subject: [PATCH 086/661] List users code refactoring --- app/Http/Controllers/ViewAssetsController.php | 95 ++++++++++++------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 38bf32eee2..c4e72971b4 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -47,6 +47,63 @@ class ViewAssetsController extends Controller return array_unique($fieldArray); } + /** + * Get list of users viewable by the current user. + * + * @param User $authUser + * @return \Illuminate\Support\Collection + */ + private function getViewableUsers(User $authUser): \Illuminate\Support\Collection + { + // SuperAdmin sees all users + if ($authUser->isSuperUser()) { + return User::select('id', 'first_name', 'last_name', 'username') + ->where('activated', 1) + ->orderBy('last_name') + ->orderBy('first_name') + ->get(); + } + + // Regular manager sees only their subordinates + self + $managedUsers = $authUser->getAllSubordinates(); + + // If user has subordinates, show them with self at beginning + if ($managedUsers->count() > 0) { + return collect([$authUser])->merge($managedUsers) + ->sortBy('last_name') + ->sortBy('first_name'); + } + + // User has no subordinates, only sees themselves + return collect([$authUser]); + } + + /** + * Get the selected user ID from request or default to current user. + * + * @param Request $request + * @param \Illuminate\Support\Collection $subordinates + * @param int $defaultUserId + * @return int + */ + private function getSelectedUserId(Request $request, \Illuminate\Support\Collection $subordinates, int $defaultUserId): int + { + // If no subordinates or no user_id in request, return default + if ($subordinates->count() <= 1 || !$request->filled('user_id')) { + return $defaultUserId; + } + + $requestedUserId = (int) $request->input('user_id'); + + // Validate if the requested user is allowed + if ($subordinates->contains('id', $requestedUserId)) { + return $requestedUserId; + } + + // If invalid ID or not authorized, return default + return $defaultUserId; + } + /** * Show user's assigned assets with optional manager view functionality. * @@ -58,41 +115,11 @@ class ViewAssetsController extends Controller $subordinates = collect(); $selectedUserId = $authUser->id; - // Check if manager view is enabled and get subordinates if applicable + // Process manager view if enabled if ($settings->manager_view_enabled) { - // Get all subordinates including self, sorted for the dropdown - if ($authUser->isSuperUser()) { - // SuperAdmin sees all users - $subordinates = User::select('id', 'first_name', 'last_name', 'username') - ->where('activated', 1) - ->orderBy('last_name') - ->orderBy('first_name') - ->get(); - } else { - // Regular manager sees only their subordinates + self (recursive) - $managedUsers = $authUser->getAllSubordinates(); - - // Only show dropdown if user actually has subordinates - if ($managedUsers->count() > 0) { - $subordinates = collect([$authUser])->merge($managedUsers) // Add self at beginning - ->sortBy('last_name') - ->sortBy('first_name'); - } else { - // User has no subordinates, so they only see themselves - $subordinates = collect([$authUser]); - } - }//end if - // If the user has subordinates and a user_id is provided in the request - if ($subordinates->count() > 1 && $request->filled('user_id')) { - $requestedUserId = (int) $request->input('user_id'); - - // Validate if the requested user is allowed (self or subordinate) - if ($subordinates->contains('id', $requestedUserId)) { - $selectedUserId = $requestedUserId; - } - // If invalid ID or not authorized, $selectedUserId remains $authUser->id (default) - } - }//end if + $subordinates = $this->getViewableUsers($authUser); + $selectedUserId = $this->getSelectedUserId($request, $subordinates, $authUser->id); + } // Load the data for the user to be viewed (either auth user or selected subordinate) $userToView = User::with([ From 088e6af0b5cd696966f889066c4cb18a3f17ab51 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 12:07:14 -0700 Subject: [PATCH 087/661] Remove admin_cc_email validation for admin_cc_always --- app/Http/Requests/StoreNotificationSettings.php | 6 +----- tests/Feature/Settings/AlertsSettingTest.php | 9 --------- tests/Support/Settings.php | 5 ----- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/app/Http/Requests/StoreNotificationSettings.php b/app/Http/Requests/StoreNotificationSettings.php index 6b2b1f5aec..f58d014c76 100644 --- a/app/Http/Requests/StoreNotificationSettings.php +++ b/app/Http/Requests/StoreNotificationSettings.php @@ -26,11 +26,7 @@ class StoreNotificationSettings extends FormRequest { return [ 'alert_email' => 'email_array|nullable', - 'admin_cc_email' => [ - 'email_array', - 'nullable', - 'required_if_accepted:admin_cc_always', - ], + 'admin_cc_email' => 'email_array|nullable', 'admin_cc_always' => [ Rule::in('0', '1'), ], diff --git a/tests/Feature/Settings/AlertsSettingTest.php b/tests/Feature/Settings/AlertsSettingTest.php index 7812d0eb33..f8829d3825 100644 --- a/tests/Feature/Settings/AlertsSettingTest.php +++ b/tests/Feature/Settings/AlertsSettingTest.php @@ -39,15 +39,6 @@ class AlertsSettingTest extends TestCase $this->assertDatabaseHas('settings', ['admin_cc_always' => '1']); } - public function test_cannot_update_admin_cc_always_without_admin_cc_email() - { - $this->settings->disableAdminCCAlways(); - - $this->actingAs(User::factory()->superuser()->create()) - ->post(route('settings.alerts.save', ['admin_cc_always' => '1'])) - ->assertSessionHasErrors('admin_cc_always'); - } - public function test_can_update_admin_cc_always_to_false() { $this->settings->enableAdminCC()->enableAdminCCAlways(); diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index d33970e683..8ff8ba6945 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -4,7 +4,6 @@ namespace Tests\Support; use App\Models\Setting; use Illuminate\Support\Facades\Crypt; -use RuntimeException; class Settings { @@ -63,10 +62,6 @@ class Settings public function enableAdminCCAlways(): Settings { - if (is_null($this->setting->admin_cc_email) || $this->setting->admin_cc_email == 0) { - throw new RuntimeException('admin_cc_email requires admin_cc_email to be set.'); - } - return $this->update([ 'admin_cc_always' => 1, ]); From 77234f6580215f6b7a4c75b8baca07cd742ec03f Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 12:24:46 -0700 Subject: [PATCH 088/661] Extract translation strings --- resources/lang/en-US/admin/settings/general.php | 2 ++ resources/views/settings/alerts.blade.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index 5da86537d6..9dafacd0e7 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -9,6 +9,8 @@ return [ 'ad_append_domain_help' => 'User isn\'t required to write "username@domain.local", they can just type "username".', 'admin_cc_email' => 'CC Email', 'admin_cc_email_help' => 'Send a copy of checkin/checkout emails to this address.', + 'admin_cc_always' => 'Always send copy upon checkin/checkout', + 'admin_cc_when_acceptance_required' => 'Only send copy upon checkout if acceptance is required', 'admin_settings' => 'Admin Settings', 'is_ad' => 'This is an Active Directory server', 'alerts' => 'Alerts', diff --git a/resources/views/settings/alerts.blade.php b/resources/views/settings/alerts.blade.php index 59c1888d2f..c861cc63bf 100644 --- a/resources/views/settings/alerts.blade.php +++ b/resources/views/settings/alerts.blade.php @@ -107,7 +107,7 @@ value="1" @checked($setting->admin_cc_always == 1) > - Always send copy upon checkin/checkout + {{ trans('admin/settings/general.admin_cc_always') }} From 360f5b7538804792bac297e7d30426a8512399e8 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 13:10:18 -0700 Subject: [PATCH 089/661] Add alert_on_response_id to CheckoutAcceptance --- app/Models/CheckoutAcceptance.php | 1 + ...ponse_id_to_checkout_acceptances_table.php | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php diff --git a/app/Models/CheckoutAcceptance.php b/app/Models/CheckoutAcceptance.php index e44a330ebc..2ca9c5b48b 100644 --- a/app/Models/CheckoutAcceptance.php +++ b/app/Models/CheckoutAcceptance.php @@ -15,6 +15,7 @@ class CheckoutAcceptance extends Model protected $casts = [ 'accepted_at' => 'datetime', 'declined_at' => 'datetime', + 'alert_on_response_id' => 'integer', ]; /** diff --git a/database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php b/database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php new file mode 100644 index 0000000000..ff9ad310f5 --- /dev/null +++ b/database/migrations/2025_06_05_200518_add_alert_on_response_id_to_checkout_acceptances_table.php @@ -0,0 +1,27 @@ +unsignedBigInteger('alert_on_response_id')->nullable()->after('note'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('checkout_acceptances', function (Blueprint $table) { + $table->dropColumn('alert_on_response_id'); + }); + } +}; From 96bce301a0ffde3bbb325a79082f429a56fc1be0 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 13:43:20 -0700 Subject: [PATCH 090/661] Add alert_on_response to Category --- app/Models/Category.php | 1 + ..._alert_on_response_to_categories_table.php | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php diff --git a/app/Models/Category.php b/app/Models/Category.php index 0321dfdb26..0fd896b35d 100755 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -32,6 +32,7 @@ class Category extends SnipeModel protected $hidden = ['created_by', 'deleted_at']; protected $casts = [ + 'alert_on_response' => 'boolean', 'created_by' => 'integer', ]; diff --git a/database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php b/database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php new file mode 100644 index 0000000000..dc2bdae1b2 --- /dev/null +++ b/database/migrations/2025_06_05_204139_add_alert_on_response_to_categories_table.php @@ -0,0 +1,27 @@ +boolean('alert_on_response')->default(0)->after('require_acceptance'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('categories', function (Blueprint $table) { + $table->dropColumn('alert_on_response'); + }); + } +}; From cb183d3645d6c9db3db7aedfab17aedd64472daa Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:06:22 -0700 Subject: [PATCH 091/661] Store alert_on_response_id on CheckoutAcceptance --- app/Listeners/CheckoutableListener.php | 6 +++ app/Models/Category.php | 1 + .../General/SettingAlertOnResponseTest.php | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index e647bcd9e0..52a30f4772 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -227,6 +227,7 @@ class CheckoutableListener if ($checkedOutToType != "App\Models\User") { return null; } + if (!$event->checkoutable->requireAcceptance()) { return null; } @@ -234,6 +235,11 @@ class CheckoutableListener $acceptance = new CheckoutAcceptance; $acceptance->checkoutable()->associate($event->checkoutable); $acceptance->assignedTo()->associate($event->checkedOutTo); + + if (data_get($event, 'checkoutable.model.category.alert_on_response')) { + $acceptance->alert_on_response_id = auth()->id(); + } + $acceptance->save(); return $acceptance; diff --git a/app/Models/Category.php b/app/Models/Category.php index 0fd896b35d..08bef4d3eb 100755 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -70,6 +70,7 @@ class Category extends SnipeModel 'eula_text', 'name', 'require_acceptance', + 'alert_on_response', 'use_default_eula', 'created_by', 'notes', diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php new file mode 100644 index 0000000000..ac3a445c6f --- /dev/null +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -0,0 +1,37 @@ +create(); + $asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => true, + ]); + + $actor = User::factory()->checkoutAssets()->create(); + $assignedUser = User::factory()->create(); + + $this->actingAs($actor) + ->post(route('hardware.checkout.store', $asset), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $assignedUser->id, + ]); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $asset->id, + 'assigned_to_id' => $assignedUser->id, + 'alert_on_response_id' => $actor->id, + ]); + } +} From 5e251505218ea1996953def2c5081f624d8f26bf Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:26:56 -0700 Subject: [PATCH 092/661] Add another test case --- .../General/SettingAlertOnResponseTest.php | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php index ac3a445c6f..c32f8d3472 100644 --- a/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php +++ b/tests/Feature/Checkouts/General/SettingAlertOnResponseTest.php @@ -9,29 +9,60 @@ use Tests\TestCase; class SettingAlertOnResponseTest extends TestCase { - public function testSetsAlertOnResponseCorrectly() + private Asset $asset; + private User $actor; + private User $assignedUser; + + protected function setUp(): void { - $asset = Asset::factory()->create(); - $asset->model->category->update([ + parent::setUp(); + + $this->asset = Asset::factory()->create(); + $this->actor = User::factory()->checkoutAssets()->create(); + $this->assignedUser = User::factory()->create(); + } + + public function test_sets_alert_on_response_if_enabled_by_category() + { + $this->asset->model->category->update([ 'require_acceptance' => true, 'alert_on_response' => true, ]); - $actor = User::factory()->checkoutAssets()->create(); - $assignedUser = User::factory()->create(); - - $this->actingAs($actor) - ->post(route('hardware.checkout.store', $asset), [ - 'checkout_to_type' => 'user', - 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, - 'assigned_user' => $assignedUser->id, - ]); + $this->postCheckout(); $this->assertDatabaseHas('checkout_acceptances', [ 'checkoutable_type' => Asset::class, - 'checkoutable_id' => $asset->id, - 'assigned_to_id' => $assignedUser->id, - 'alert_on_response_id' => $actor->id, + 'checkoutable_id' => $this->asset->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => $this->actor->id, ]); } + + public function test_does_not_set_alert_on_response_if_disabled_by_category() + { + $this->asset->model->category->update([ + 'require_acceptance' => true, + 'alert_on_response' => false, + ]); + + $this->postCheckout(); + + $this->assertDatabaseHas('checkout_acceptances', [ + 'checkoutable_type' => Asset::class, + 'checkoutable_id' => $this->asset->id, + 'assigned_to_id' => $this->assignedUser->id, + 'alert_on_response_id' => null, + ]); + } + + private function postCheckout(): void + { + $this->actingAs($this->actor) + ->post(route('hardware.checkout.store', $this->asset), [ + 'checkout_to_type' => 'user', + 'status_id' => (string) Statuslabel::factory()->readyToDeploy()->create()->id, + 'assigned_user' => $this->assignedUser->id, + ]); + } } From 2a68b4aeff04275b9604d3c20d6c695368f4671c Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:36:06 -0700 Subject: [PATCH 093/661] Update test for updating category with alert_on_response --- .../Categories/Ui/UpdateCategoriesTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/Feature/Categories/Ui/UpdateCategoriesTest.php b/tests/Feature/Categories/Ui/UpdateCategoriesTest.php index ea5cc63388..8ecfb2a527 100644 --- a/tests/Feature/Categories/Ui/UpdateCategoriesTest.php +++ b/tests/Feature/Categories/Ui/UpdateCategoriesTest.php @@ -43,20 +43,33 @@ class UpdateCategoriesTest extends TestCase public function testUserCanEditAssetCategory() { - $category = Category::factory()->forAssets()->create(['name' => 'Test Category']); + $category = Category::factory()->forAssets()->create([ + 'name' => 'Test Category', + 'require_acceptance' => false, + 'alert_on_response' => false, + ]); + $this->assertTrue(Category::where('name', 'Test Category')->exists()); $response = $this->actingAs(User::factory()->superuser()->create()) ->put(route('categories.update', $category), [ 'name' => 'Test Category Edited', 'notes' => 'Test Note Edited', + 'require_acceptance' => '1', + 'alert_on_response' => '1', ]) ->assertStatus(302) ->assertSessionHasNoErrors() ->assertRedirect(route('categories.index')); $this->followRedirects($response)->assertSee('Success'); - $this->assertTrue(Category::where('name', 'Test Category Edited')->where('notes', 'Test Note Edited')->exists()); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Test Category Edited', + 'notes' => 'Test Note Edited', + 'require_acceptance' => 1, + 'alert_on_response' => 1, + ]); } @@ -102,5 +115,4 @@ class UpdateCategoriesTest extends TestCase $this->assertFalse(Category::where('name', 'Test Category Edited')->where('notes', 'Test Note Edited')->exists()); } - } From 19b9e50281431652d3d2f39fc3d713b8efc5c747 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:40:19 -0700 Subject: [PATCH 094/661] Update livewire component for alert_on_response --- app/Livewire/CategoryEditForm.php | 2 ++ resources/views/categories/edit.blade.php | 1 + resources/views/livewire/category-edit-form.blade.php | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Livewire/CategoryEditForm.php b/app/Livewire/CategoryEditForm.php index fd8bef6489..98e505c8df 100644 --- a/app/Livewire/CategoryEditForm.php +++ b/app/Livewire/CategoryEditForm.php @@ -6,6 +6,8 @@ use Livewire\Component; class CategoryEditForm extends Component { + public bool $alertOnResponse; + public $defaultEulaText; public $eulaText; diff --git a/resources/views/categories/edit.blade.php b/resources/views/categories/edit.blade.php index 7c12f677fa..fd9efcc49f 100755 --- a/resources/views/categories/edit.blade.php +++ b/resources/views/categories/edit.blade.php @@ -31,6 +31,7 @@ Send email to you when user accepts or declines checkout. From 063553d4f72dab8a8fe287d53e4c18197d629bea Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 14:57:15 -0700 Subject: [PATCH 095/661] Scaffold scenarios --- .../Email/CheckoutResponseEmailTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php new file mode 100644 index 0000000000..e6627b9225 --- /dev/null +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -0,0 +1,21 @@ +markTestIncomplete(); + } +} From 333501fe55cee37b1552d0bf31aafb4344814875 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 15:07:19 -0700 Subject: [PATCH 096/661] WIP: create mail class --- app/Mail/CheckoutAcceptanceResponseMail.php | 53 +++++++++++++++++++ .../checkout-acceptance-response.blade.php | 8 +++ .../Email/CheckoutResponseEmailTest.php | 33 ++++++++++-- 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 app/Mail/CheckoutAcceptanceResponseMail.php create mode 100644 resources/views/mail/markdown/checkout-acceptance-response.blade.php diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php new file mode 100644 index 0000000000..440f63467a --- /dev/null +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -0,0 +1,53 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php new file mode 100644 index 0000000000..5fe2b643e2 --- /dev/null +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -0,0 +1,8 @@ +@component('mail::message') +# {{ trans('mail.hello') }} {{ $target->present()->fullName() }}, + +{{ trans('mail.best_regards') }} + +{{ $snipeSettings->site_name }} + +@endcomponent diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index e6627b9225..cd40f55b18 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -2,20 +2,45 @@ namespace Tests\Feature\Notifications\Email; +use App\Mail\CheckoutAcceptanceResponseMail; +use App\Models\CheckoutAcceptance; +use App\Models\User; +use Illuminate\Support\Facades\Mail; use Tests\TestCase; class CheckoutResponseEmailTest extends TestCase { public static function scenarios() { - yield 'Accepting checkout acceptance configured to send alert'; - yield 'Declining checkout acceptance configured to send alert'; - yield 'Accepting checkout acceptance not configured to send alert'; - yield 'Declining checkout acceptance not configured to send alert'; + yield 'Accepting checkout acceptance configured to send alert' => []; + yield 'Declining checkout acceptance configured to send alert' => []; + yield 'Accepting checkout acceptance not configured to send alert' => []; + yield 'Declining checkout acceptance not configured to send alert' => []; } public function test_checkout_response_alert() { $this->markTestIncomplete(); + + Mail::fake(); + + $user = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => $user->id, + ]); + + $this->actingAs($checkoutAcceptance->assignedTo) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'accepted', + 'note' => null, + ]); + + Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { + // @todo: better assertions? accepted vs declined? + return $mail->hasTo($user->email); + }); } } From bec80b443ceeeb414fb307e3a932be91e21485b2 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 15:15:22 -0700 Subject: [PATCH 097/661] WIP: begin to send email --- .../Controllers/Account/AcceptanceController.php | 13 +++++++++++++ app/Mail/CheckoutAcceptanceResponseMail.php | 6 ++++-- .../Email/CheckoutResponseEmailTest.php | 2 -- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index b20f7d97e8..583b710887 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -7,6 +7,7 @@ use App\Events\CheckoutDeclined; use App\Events\ItemAccepted; use App\Events\ItemDeclined; use App\Http\Controllers\Controller; +use App\Mail\CheckoutAcceptanceResponseMail; use App\Models\Actionlog; use App\Models\Asset; use App\Models\CheckoutAcceptance; @@ -21,8 +22,10 @@ use App\Models\Component; use App\Models\Consumable; use App\Notifications\AcceptanceAssetAcceptedNotification; use App\Notifications\AcceptanceAssetDeclinedNotification; +use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use App\Http\Controllers\SettingsController; @@ -337,6 +340,16 @@ class AcceptanceController extends Controller $return_msg = trans('admin/users/message.declined'); } + if ($acceptance->alert_on_response_id) { + try { + Mail::to(User::findOrFail($acceptance->alert_on_response_id)) + ->send(new CheckoutAcceptanceResponseMail( + (bool) $request->input('asset_acceptance') === 'accepted' + )); + } catch (Exception $e) { + Log::warning($e); + } + } return redirect()->to('account/accept')->with('success', $return_msg); diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index 440f63467a..3eed73ed0a 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -13,12 +13,14 @@ class CheckoutAcceptanceResponseMail extends Mailable { use Queueable, SerializesModels; + public bool $accepted; + /** * Create a new message instance. */ - public function __construct() + public function __construct(bool $accepted) { - // + $this->accepted = $accepted; } /** diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index cd40f55b18..9b4546af81 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -20,8 +20,6 @@ class CheckoutResponseEmailTest extends TestCase public function test_checkout_response_alert() { - $this->markTestIncomplete(); - Mail::fake(); $user = User::factory()->create(); From 6db04c86dfa44f216791362720687b0d1ff0bd78 Mon Sep 17 00:00:00 2001 From: akemidx Date: Thu, 5 Jun 2025 18:46:33 -0400 Subject: [PATCH 098/661] making fields active. first pass --- resources/views/layouts/default.blade.php | 76 ++++++++++++----------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index 4175cdf11a..fb65a72f2b 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -455,7 +455,8 @@ dir="{{ Helper::determineLanguageDirection() }}">
      -
    • +
    • +{{-- this makes list all ALWAYS active for the dropdown submenu. need this but NOT that--}} {{ trans('general.list_all') }} @@ -573,21 +574,22 @@ dir="{{ Helper::determineLanguageDirection() }}"> {{ trans('general.deleted') }}
    • -
    • +
    • +{{-- dropdown menu still closes when selecting this--}} {{ trans('general.asset_maintenances') }}
    • @endcan @can('admin') -
    • +
    • {{ trans('general.import-history') }}
    • @endcan @can('audit', \App\Models\Asset::class) -
    • +
    • {{ trans('general.bulkaudit') }} @@ -646,7 +648,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
    • @endcan @can('import') -
    • +
    • {{ trans('general.import') }} @@ -680,64 +682,64 @@ dir="{{ Helper::determineLanguageDirection() }}"> @endcan @can('view', \App\Models\AssetModel::class) -
    • - +
    • + {{ trans('general.asset_models') }}
    • @endcan @can('view', \App\Models\Category::class) -
    • - +
    • + {{ trans('general.categories') }}
    • @endcan @can('view', \App\Models\Manufacturer::class) -
    • - +
    • + {{ trans('general.manufacturers') }}
    • @endcan @can('view', \App\Models\Supplier::class) -
    • - +
    • + {{ trans('general.suppliers') }}
    • @endcan @can('view', \App\Models\Department::class) -
    • - +
    • + {{ trans('general.departments') }}
    • @endcan @can('view', \App\Models\Location::class) -
    • - +
    • + {{ trans('general.locations') }}
    • @endcan @can('view', \App\Models\Company::class) -
    • - +
    • + {{ trans('general.companies') }}
    • @endcan @can('view', \App\Models\Depreciation::class) -
    • - +
    • + {{ trans('general.depreciation') }}
    • @@ -755,42 +757,42 @@ dir="{{ Helper::determineLanguageDirection() }}">
        -
      • - +
      • + {{ trans('general.activity_report') }}
      • -
      • - +
      • + {{ trans('general.custom_report') }}
      • -
      • - +
      • + {{ trans('general.audit_report') }}
      • -
      • - +
      • + {{ trans('general.depreciation_report') }}
      • -
      • - +
      • + {{ trans('general.license_report') }}
      • -
      • - +
      • + {{ trans('general.asset_maintenance_report') }}
      • -
      • - +
      • + {{ trans('general.unaccepted_asset_report') }}
      • -
      • - +
      • + {{ trans('general.accessory_report') }}
      • From df1361aa43ad198585a348723eafb8b2dcdacd54 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:42:31 -0700 Subject: [PATCH 099/661] Scaffold test --- .../Email/CheckoutResponseEmailTest.php | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 9b4546af81..d43f2c6422 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -10,15 +10,7 @@ use Tests\TestCase; class CheckoutResponseEmailTest extends TestCase { - public static function scenarios() - { - yield 'Accepting checkout acceptance configured to send alert' => []; - yield 'Declining checkout acceptance configured to send alert' => []; - yield 'Accepting checkout acceptance not configured to send alert' => []; - yield 'Declining checkout acceptance not configured to send alert' => []; - } - - public function test_checkout_response_alert() + public function test_accepting_checkout_acceptance_configured_to_send_alert() { Mail::fake(); @@ -41,4 +33,19 @@ class CheckoutResponseEmailTest extends TestCase return $mail->hasTo($user->email); }); } + + public function test_declining_checkout_acceptance_configured_to_send_alert() + { + $this->markTestIncomplete(); + } + + public function test_accepting_checkout_acceptance_not_configured_to_send_alert() + { + $this->markTestIncomplete(); + } + + public function test_declining_checkout_acceptance_not_configured_to_send_alert() + { + $this->markTestIncomplete(); + } } From 7ec0925c69f4ffb0e85998b8e1caea9f080d11c4 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:44:44 -0700 Subject: [PATCH 100/661] Scaffold out tests --- .../Email/CheckoutResponseEmailTest.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index d43f2c6422..408535ccd0 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -10,10 +10,15 @@ use Tests\TestCase; class CheckoutResponseEmailTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + Mail::fake(); + } + public function test_accepting_checkout_acceptance_configured_to_send_alert() { - Mail::fake(); - $user = User::factory()->create(); $checkoutAcceptance = CheckoutAcceptance::factory() @@ -28,10 +33,7 @@ class CheckoutResponseEmailTest extends TestCase 'note' => null, ]); - Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { - // @todo: better assertions? accepted vs declined? - return $mail->hasTo($user->email); - }); + $this->assertEmailSentTo($user); } public function test_declining_checkout_acceptance_configured_to_send_alert() @@ -48,4 +50,12 @@ class CheckoutResponseEmailTest extends TestCase { $this->markTestIncomplete(); } + + private function assertEmailSentTo(User $user): void + { + Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { + // @todo: better assertions? accepted vs declined? + return $mail->hasTo($user->email); + }); + } } From db50e98ae383e7ae682bb6daaef52c325fc29f23 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:51:44 -0700 Subject: [PATCH 101/661] Populate tests --- .../Email/CheckoutResponseEmailTest.php | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 408535ccd0..73dcd2cfb5 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -19,36 +19,62 @@ class CheckoutResponseEmailTest extends TestCase public function test_accepting_checkout_acceptance_configured_to_send_alert() { - $user = User::factory()->create(); + $initiator = User::factory()->create(); $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() ->create([ - 'alert_on_response_id' => $user->id, + 'alert_on_response_id' => $initiator->id, ]); - $this->actingAs($checkoutAcceptance->assignedTo) - ->post(route('account.store-acceptance', $checkoutAcceptance), [ - 'asset_acceptance' => 'accepted', - 'note' => null, - ]); + $this->acceptCheckout($checkoutAcceptance); - $this->assertEmailSentTo($user); + $this->assertEmailSentTo($initiator); } public function test_declining_checkout_acceptance_configured_to_send_alert() { - $this->markTestIncomplete(); + $initiator = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => $initiator->id, + ]); + + $this->declineCheckout($checkoutAcceptance); + + $this->assertEmailSentTo($initiator); } public function test_accepting_checkout_acceptance_not_configured_to_send_alert() { - $this->markTestIncomplete(); + $initiator = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => null, + ]); + + $this->acceptCheckout($checkoutAcceptance); + + $this->assertEmailNotSentTo($initiator); } public function test_declining_checkout_acceptance_not_configured_to_send_alert() { - $this->markTestIncomplete(); + $initiator = User::factory()->create(); + + $checkoutAcceptance = CheckoutAcceptance::factory() + ->pending() + ->create([ + 'alert_on_response_id' => null, + ]); + + $this->declineCheckout($checkoutAcceptance); + + $this->assertEmailNotSentTo($initiator); } private function assertEmailSentTo(User $user): void @@ -58,4 +84,29 @@ class CheckoutResponseEmailTest extends TestCase return $mail->hasTo($user->email); }); } + + private function assertEmailNotSentTo(User $user): void + { + Mail::assertNotSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { + return $mail->hasTo($user->email); + }); + } + + private function acceptCheckout(CheckoutAcceptance $checkoutAcceptance): void + { + $this->actingAs($checkoutAcceptance->assignedTo) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'accepted', + 'note' => null, + ]); + } + + private function declineCheckout(CheckoutAcceptance $checkoutAcceptance): void + { + $this->actingAs($checkoutAcceptance->assignedTo) + ->post(route('account.store-acceptance', $checkoutAcceptance), [ + 'asset_acceptance' => 'declined', + 'note' => null, + ]); + } } From 2d0874920711adfb36da1f36fc171508a28c4550 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 16:56:11 -0700 Subject: [PATCH 102/661] Improve readability --- .../factories/CheckoutAcceptanceFactory.php | 18 +++++++++++++++++ .../Email/CheckoutResponseEmailTest.php | 20 ++++++++----------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index a15e942a84..bb56ab2b4a 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -64,6 +64,24 @@ class CheckoutAcceptanceFactory extends Factory ]); } + public function withoutAlerting() + { + return $this->state(function () { + return [ + 'alert_on_response_id' => null, + ]; + }); + } + + public function withAlertingTo(User $user) + { + return $this->state(function () use ($user) { + return [ + 'alert_on_response_id' => $user->id, + ]; + }); + } + private function createdAssociatedActionLogEntry(CheckoutAcceptance $acceptance): void { $acceptance->checkoutable->assetlog()->create([ diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 73dcd2cfb5..a761b77ce1 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -23,9 +23,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => $initiator->id, - ]); + ->withAlertingTo($initiator) + ->create(); $this->acceptCheckout($checkoutAcceptance); @@ -38,9 +37,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => $initiator->id, - ]); + ->withAlertingTo($initiator) + ->create(); $this->declineCheckout($checkoutAcceptance); @@ -53,9 +51,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => null, - ]); + ->withoutAlerting() + ->create(); $this->acceptCheckout($checkoutAcceptance); @@ -68,9 +65,8 @@ class CheckoutResponseEmailTest extends TestCase $checkoutAcceptance = CheckoutAcceptance::factory() ->pending() - ->create([ - 'alert_on_response_id' => null, - ]); + ->withoutAlerting() + ->create(); $this->declineCheckout($checkoutAcceptance); From 7424a5987bc6f6da9a007b048f1c0a04de9d3413 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 17:22:31 -0700 Subject: [PATCH 103/661] Add subject --- .../Controllers/Account/AcceptanceController.php | 9 ++++++--- app/Mail/CheckoutAcceptanceResponseMail.php | 15 +++++++++++---- .../checkout-acceptance-response.blade.php | 2 +- .../Email/CheckoutResponseEmailTest.php | 13 ++++++------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php index 583b710887..adb423581b 100644 --- a/app/Http/Controllers/Account/AcceptanceController.php +++ b/app/Http/Controllers/Account/AcceptanceController.php @@ -342,10 +342,13 @@ class AcceptanceController extends Controller if ($acceptance->alert_on_response_id) { try { - Mail::to(User::findOrFail($acceptance->alert_on_response_id)) + $recipient = User::findOrFail($acceptance->alert_on_response_id); + + Mail::to($recipient) ->send(new CheckoutAcceptanceResponseMail( - (bool) $request->input('asset_acceptance') === 'accepted' - )); + $recipient, + $request->input('asset_acceptance') === 'accepted') + ); } catch (Exception $e) { Log::warning($e); } diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index 3eed73ed0a..bf4aad5e8e 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -2,6 +2,7 @@ namespace App\Mail; +use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; @@ -13,14 +14,16 @@ class CheckoutAcceptanceResponseMail extends Mailable { use Queueable, SerializesModels; - public bool $accepted; + public bool $wasAccepted; + public User $recipient; /** * Create a new message instance. */ - public function __construct(bool $accepted) + public function __construct(User $recipient, bool $wasAccepted) { - $this->accepted = $accepted; + $this->recipient = $recipient; + $this->wasAccepted = $wasAccepted; } /** @@ -29,7 +32,7 @@ class CheckoutAcceptanceResponseMail extends Mailable public function envelope(): Envelope { return new Envelope( - subject: 'Checkout Acceptance Response Mail', + subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'rejected'), ); } @@ -40,6 +43,10 @@ class CheckoutAcceptanceResponseMail extends Mailable { return new Content( markdown: 'mail.markdown.checkout-acceptance-response', + with: [ + 'recipient' => $this->recipient, + 'wasAccepted' => $this->wasAccepted, + ] ); } diff --git a/resources/views/mail/markdown/checkout-acceptance-response.blade.php b/resources/views/mail/markdown/checkout-acceptance-response.blade.php index 5fe2b643e2..472e39b308 100644 --- a/resources/views/mail/markdown/checkout-acceptance-response.blade.php +++ b/resources/views/mail/markdown/checkout-acceptance-response.blade.php @@ -1,5 +1,5 @@ @component('mail::message') -# {{ trans('mail.hello') }} {{ $target->present()->fullName() }}, +# {{ trans('mail.hello') }} {{ $recipient->present()->fullName() }}, {{ trans('mail.best_regards') }} diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index a761b77ce1..9174e5656d 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -28,7 +28,7 @@ class CheckoutResponseEmailTest extends TestCase $this->acceptCheckout($checkoutAcceptance); - $this->assertEmailSentTo($initiator); + $this->assertEmailSentTo($initiator, 'accepted'); } public function test_declining_checkout_acceptance_configured_to_send_alert() @@ -42,7 +42,7 @@ class CheckoutResponseEmailTest extends TestCase $this->declineCheckout($checkoutAcceptance); - $this->assertEmailSentTo($initiator); + $this->assertEmailSentTo($initiator, 'rejected'); } public function test_accepting_checkout_acceptance_not_configured_to_send_alert() @@ -73,11 +73,10 @@ class CheckoutResponseEmailTest extends TestCase $this->assertEmailNotSentTo($initiator); } - private function assertEmailSentTo(User $user): void + private function assertEmailSentTo(User $user, string $type): void { - Mail::assertSent(CheckoutAcceptanceResponseMail::class, function ($mail) use ($user) { - // @todo: better assertions? accepted vs declined? - return $mail->hasTo($user->email); + Mail::assertSent(CheckoutAcceptanceResponseMail::class, function (CheckoutAcceptanceResponseMail $mail) use ($type, $user) { + return $mail->hasTo($user->email) && $mail->assertHasSubject('A checkout you initiated was ' . $type); }); } @@ -101,7 +100,7 @@ class CheckoutResponseEmailTest extends TestCase { $this->actingAs($checkoutAcceptance->assignedTo) ->post(route('account.store-acceptance', $checkoutAcceptance), [ - 'asset_acceptance' => 'declined', + 'asset_acceptance' => 'rejected', 'note' => null, ]); } From ae98f6276e4edc60dc4c2749f07c31024a6625d7 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Thu, 5 Jun 2025 17:24:57 -0700 Subject: [PATCH 104/661] Replace reject with declined --- app/Mail/CheckoutAcceptanceResponseMail.php | 2 +- .../Feature/Notifications/Email/CheckoutResponseEmailTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php index bf4aad5e8e..4e75d3e7e5 100644 --- a/app/Mail/CheckoutAcceptanceResponseMail.php +++ b/app/Mail/CheckoutAcceptanceResponseMail.php @@ -32,7 +32,7 @@ class CheckoutAcceptanceResponseMail extends Mailable public function envelope(): Envelope { return new Envelope( - subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'rejected'), + subject: 'A checkout you initiated was ' . ($this->wasAccepted ? 'accepted' : 'declined'), ); } diff --git a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php index 9174e5656d..4fe996461a 100644 --- a/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php +++ b/tests/Feature/Notifications/Email/CheckoutResponseEmailTest.php @@ -42,7 +42,7 @@ class CheckoutResponseEmailTest extends TestCase $this->declineCheckout($checkoutAcceptance); - $this->assertEmailSentTo($initiator, 'rejected'); + $this->assertEmailSentTo($initiator, 'declined'); } public function test_accepting_checkout_acceptance_not_configured_to_send_alert() @@ -100,7 +100,7 @@ class CheckoutResponseEmailTest extends TestCase { $this->actingAs($checkoutAcceptance->assignedTo) ->post(route('account.store-acceptance', $checkoutAcceptance), [ - 'asset_acceptance' => 'rejected', + 'asset_acceptance' => 'declined', 'note' => null, ]); } From cc9209d2deaf7f75fdc9f2803c4c06ae2d6ac0fe Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 6 Jun 2025 11:32:13 +0100 Subject: [PATCH 105/661] Removed EOL date and purchase date partials Signed-off-by: snipe --- .../views/partials/forms/edit/eol_date.blade.php | 11 ----------- .../views/partials/forms/edit/purchase_date.blade.php | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 resources/views/partials/forms/edit/eol_date.blade.php delete mode 100644 resources/views/partials/forms/edit/purchase_date.blade.php diff --git a/resources/views/partials/forms/edit/eol_date.blade.php b/resources/views/partials/forms/edit/eol_date.blade.php deleted file mode 100644 index 5efba06a85..0000000000 --- a/resources/views/partials/forms/edit/eol_date.blade.php +++ /dev/null @@ -1,11 +0,0 @@ - -
        - -
        -
        - - -
        - {!! $errors->first('asset_eol_date', '') !!} -
        -
        diff --git a/resources/views/partials/forms/edit/purchase_date.blade.php b/resources/views/partials/forms/edit/purchase_date.blade.php deleted file mode 100644 index f07387c309..0000000000 --- a/resources/views/partials/forms/edit/purchase_date.blade.php +++ /dev/null @@ -1,11 +0,0 @@ - -
        - -
        -
        - - -
        - {!! $errors->first('purchase_date', '') !!} -
        -
        From e18e9f699e199cda6af81f04c750750bbeb3fd35 Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 6 Jun 2025 11:32:37 +0100 Subject: [PATCH 106/661] Use blade component in datepicker partial Signed-off-by: snipe --- .../partials/forms/edit/datepicker.blade.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/resources/views/partials/forms/edit/datepicker.blade.php b/resources/views/partials/forms/edit/datepicker.blade.php index d91659134e..74ec021e4b 100644 --- a/resources/views/partials/forms/edit/datepicker.blade.php +++ b/resources/views/partials/forms/edit/datepicker.blade.php @@ -2,11 +2,21 @@
        -
        - - -
        + {!! $errors->first($fieldname, '') !!}
        + @if (isset($help_text)) +
        +

        + {!! $help_text !!} +

        +
        + @endif
        + \ No newline at end of file From 974627849bddeb065088e1dbca089589f9e55f85 Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 6 Jun 2025 11:32:47 +0100 Subject: [PATCH 107/661] Added datepicker partial Signed-off-by: snipe --- resources/views/blade/input/datepicker.blade.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 resources/views/blade/input/datepicker.blade.php diff --git a/resources/views/blade/input/datepicker.blade.php b/resources/views/blade/input/datepicker.blade.php new file mode 100644 index 0000000000..111aa8dbf7 --- /dev/null +++ b/resources/views/blade/input/datepicker.blade.php @@ -0,0 +1,13 @@ +@props([ + 'value' => '', + 'required' => '', + 'end_date' => null, + 'col_size_class' => null, +]) + + +
        + merge(['class' => 'form-control']) }} {{ $required=='1' ? 'required' : '' }}> + + +
        \ No newline at end of file From b162aba445bd319fe1539aaa593b16a29b7430d5 Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 6 Jun 2025 11:33:02 +0100 Subject: [PATCH 108/661] Added localization for datepicker Signed-off-by: snipe --- resources/lang/en-US/datepicker.php | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 resources/lang/en-US/datepicker.php diff --git a/resources/lang/en-US/datepicker.php b/resources/lang/en-US/datepicker.php new file mode 100644 index 0000000000..1abad03a81 --- /dev/null +++ b/resources/lang/en-US/datepicker.php @@ -0,0 +1,78 @@ + 'Today', + 'clear' => 'Clear', + 'days' => [ + 'sunday' => 'Sunday', + 'monday' => 'Monday', + 'tuesday' => 'Tuesday', + 'wednesday' => 'Wednesday', + 'thursday' => 'Thursday', + 'friday' => 'Friday', + 'saturday' => 'Saturday', + ], + + 'short_days' => [ + 'sunday' => 'Sun', + 'monday' => 'Mon', + 'tuesday' => 'Tue', + 'wednesday' => 'Wed', + 'thursday' => 'Thu', + 'friday' => 'Fri', + 'saturday' => 'Sat', + ], + + 'min_days' => [ + 'sunday' => 'Su', + 'monday' => 'Mo', + 'tuesday' => 'Tu', + 'wednesday' => 'We', + 'thursday' => 'Th', + 'friday' => 'Fr', + 'saturday' => 'Sa', + ], + + 'months' => [ + 'january' => 'January', + 'february' => 'February', + 'march' => 'March', + 'april' => 'April', + 'may' => 'May', + 'june' => 'June', + 'july' => 'July', + 'august' => 'August', + 'september' => 'September', + 'october' => 'October', + 'november' => 'November', + 'december' => 'December', + ], + + 'months_short' => [ + 'january' => 'Jan', + 'february' => 'Feb', + 'march' => 'Mar', + 'april' => 'Apr', + 'may' => 'May', + 'june' => 'Jun', + 'july' => 'Jul', + 'august' => 'Aug', + 'september' => 'Sep', + 'october' => 'Oct', + 'november' => 'Nov', + 'december' => 'Dec', + ], + +); From 048d910d5b844e4cc485af924a4df33b4b6ab39e Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 6 Jun 2025 11:33:17 +0100 Subject: [PATCH 109/661] Use partial Signed-off-by: snipe --- resources/views/accessories/edit.blade.php | 2 +- .../views/asset_maintenances/edit.blade.php | 22 ++++--- resources/views/components/edit.blade.php | 2 +- resources/views/consumables/edit.blade.php | 2 +- resources/views/hardware/checkout.blade.php | 43 +++++-------- resources/views/hardware/edit.blade.php | 28 ++------- resources/views/layouts/default.blade.php | 62 +++++++++++++++++++ resources/views/licenses/edit.blade.php | 2 +- 8 files changed, 97 insertions(+), 66 deletions(-) diff --git a/resources/views/accessories/edit.blade.php b/resources/views/accessories/edit.blade.php index 8e0503400f..827e117b51 100644 --- a/resources/views/accessories/edit.blade.php +++ b/resources/views/accessories/edit.blade.php @@ -22,7 +22,7 @@ @include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id']) @include ('partials.forms.edit.model_number') @include ('partials.forms.edit.order_number') -@include ('partials.forms.edit.purchase_date') +@include ('partials.forms.edit.datepicker', ['translated_name' => trans('general.purchase_date'),'fieldname' => 'purchase_date']) @include ('partials.forms.edit.purchase_cost', ['currency_type' => $item->location->currency ?? null]) @include ('partials.forms.edit.quantity') @include ('partials.forms.edit.minimum_quantity') diff --git a/resources/views/asset_maintenances/edit.blade.php b/resources/views/asset_maintenances/edit.blade.php index 4dd390ddfa..8e41701c79 100644 --- a/resources/views/asset_maintenances/edit.blade.php +++ b/resources/views/asset_maintenances/edit.blade.php @@ -65,10 +65,12 @@
        -
        - - -
        + {!! $errors->first('start_date', '') !!}
        @@ -80,12 +82,12 @@
        -
        - - - - -
        + {!! $errors->first('completion_date', '') !!}
        diff --git a/resources/views/components/edit.blade.php b/resources/views/components/edit.blade.php index 206418f894..979d68248c 100644 --- a/resources/views/components/edit.blade.php +++ b/resources/views/components/edit.blade.php @@ -26,7 +26,7 @@ @include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id']) @include ('partials.forms.edit.supplier-select', ['translated_name' => trans('general.supplier'), 'fieldname' => 'supplier_id']) @include ('partials.forms.edit.order_number') -@include ('partials.forms.edit.purchase_date') +@include ('partials.forms.edit.datepicker', ['translated_name' => trans('general.purchase_date'),'fieldname' => 'purchase_date']) @include ('partials.forms.edit.purchase_cost') @include ('partials.forms.edit.notes') @include ('partials.forms.edit.image-upload', ['image_path' => app('components_upload_path')]) diff --git a/resources/views/consumables/edit.blade.php b/resources/views/consumables/edit.blade.php index 300a7114d9..e844338fa7 100644 --- a/resources/views/consumables/edit.blade.php +++ b/resources/views/consumables/edit.blade.php @@ -22,7 +22,7 @@ @include ('partials.forms.edit.model_number') @include ('partials.forms.edit.item_number') @include ('partials.forms.edit.order_number') -@include ('partials.forms.edit.purchase_date') +@include ('partials.forms.edit.datepicker', ['translated_name' => trans('general.purchase_date'),'fieldname' => 'purchase_date']) @include ('partials.forms.edit.purchase_cost') @include ('partials.forms.edit.quantity') @include ('partials.forms.edit.minimum_quantity') diff --git a/resources/views/hardware/checkout.blade.php b/resources/views/hardware/checkout.blade.php index ac5c8022ec..60d64221e2 100755 --- a/resources/views/hardware/checkout.blade.php +++ b/resources/views/hardware/checkout.blade.php @@ -110,14 +110,15 @@ {{ trans('admin/hardware/form.checkout_date') }}
        -
        - - - -
        + + {!! $errors->first('checkout_at', '') !!}
        @@ -129,15 +130,12 @@
        -
        - - - - -
        + {!! $errors->first('expected_checkin', '') !!}
        @@ -224,15 +222,4 @@ @section('moar_scripts') @include('partials/assets-assigned') - - @stop diff --git a/resources/views/hardware/edit.blade.php b/resources/views/hardware/edit.blade.php index 28b9a8438a..921a4542fd 100755 --- a/resources/views/hardware/edit.blade.php +++ b/resources/views/hardware/edit.blade.php @@ -114,28 +114,8 @@