From 58eac619eabc6f224a6147c66da6454cc37cca1d Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 16 Sep 2025 15:52:41 -0700 Subject: [PATCH 01/22] add checkoutable class, rework blade for unaccepted items --- app/Http/Controllers/ReportsController.php | 35 +++--- .../views/reports/unaccepted_assets.blade.php | 104 ++++++++++++------ 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 1eccf4886ff7..61d772809bcb 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -5,10 +5,14 @@ use App\Helpers\Helper; use App\Mail\CheckoutAssetMail; use App\Models\Accessory; +use App\Models\AccessoryCheckout; use App\Models\Actionlog; use App\Models\Asset; use App\Models\AssetModel; use App\Models\Category; +use App\Models\Checkoutable; +use App\Models\Component; +use App\Models\LicenseSeat; use App\Models\Maintenance; use App\Models\CheckoutAcceptance; use App\Models\Company; @@ -1111,14 +1115,14 @@ public function getAssetAcceptanceReport($deleted = false) : View $showDeleted = $deleted == 'deleted'; $query = CheckoutAcceptance::pending() - ->where('checkoutable_type', 'App\Models\Asset') ->with([ 'checkoutable' => function (MorphTo $query) { $query->morphWith([ - AssetModel::class => ['model'], - Company::class => ['company'], - Asset::class => ['assignedTo'], - ])->with('model.category'); + Asset::class => ['model.category', 'assignedTo', 'company'], + Accessory::class => ['category','checkouts', 'company'], + LicenseSeat::class => ['user', 'license'], + Component::class => ['assignedTo', 'company'], + ]); }, 'assignedTo' => function($query){ $query->withTrashed(); @@ -1129,15 +1133,20 @@ public function getAssetAcceptanceReport($deleted = false) : View $query->withTrashed(); } - $assetsForReport = $query->get() - ->map(function ($acceptance) { - return [ - 'assetItem' => $acceptance->checkoutable, - 'acceptance' => $acceptance, - ]; - }); +// $assetsForReport = $query->get() +// ->map(function ($acceptance) { +// return [ +// 'assetItem' => $acceptance->checkoutable, +// 'acceptance' => $acceptance, +// ]; +// }); +// dd($assetsForReport); +// $assetsForReport = $query->get()->map(function ($unaccepted) {}) + $itemsForReport = $query->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted)); + + - return view('reports/unaccepted_assets', compact('assetsForReport','showDeleted' )); + return view('reports/unaccepted_assets', compact('itemsForReport','showDeleted' )); } /** diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index f0d93c0bed47..ccffb10cf3ec 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -49,6 +49,7 @@ class="table table-striped snipe-table" {{ trans('general.date') }} + {{ trans('general.type') }} {{ trans('admin/companies/table.title') }} {{ trans('general.category') }} {{ trans('admin/hardware/form.model') }} @@ -59,43 +60,74 @@ class="table table-striped snipe-table" - @if ($assetsForReport) - @foreach ($assetsForReport as $item) - @if ($item['assetItem']) - trashed()) style="text-decoration: line-through" @endif> - {{ Helper::getFormattedDateObject($item['acceptance']->created_at, 'datetime', false) }} - {{ ($item['assetItem']->company) ? $item['assetItem']->company->name : '' }} - {!! $item['assetItem']->model->category->present()->nameUrl() !!} - {!! $item['assetItem']->present()->modelUrl() !!} - {!! $item['assetItem']->present()->nameUrl() !!} - {{ $item['assetItem']->asset_tag }} - assignedTo === null || $item['acceptance']->assignedTo->trashed()) style="text-decoration: line-through" @endif>{!! ($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->present()->nameUrl() : trans('admin/reports/general.deleted_user') !!} - - - @if(!$item['acceptance']->trashed()) -
- @if (($item['acceptance']->assignedTo) && ($item['acceptance']->assignedTo->email)) - @csrf - - - @else - - - - - - @endif - -
- @endif + @if ($itemsForReport) + @foreach ($itemsForReport as $item) + acceptance->trashed()) style="text-decoration: line-through" @endif> + {{-- Created date (already formatted) --}} + + {{ Helper::getFormattedDateObject($item->acceptance->created_at, 'datetime', false) }} + + {{-- Item Type --}} + {{ $item->type }} + {{-- Company name --}} + {{ $item->company }} + + {{-- Category --}} + {!! $item->category !!} + + {{-- Model --}} + {!! $item->model !!} + + {{-- Name --}} + {!! $item->name !!} -
- - - @endif - @endforeach + {{-- Asset tag or blank --}} + {{ $item->asset_tag }} + + {{-- Assigned To (with soft-delete strike if needed) --}} + @php + $assignee = $item->acceptance->assignedTo; + $assigneeStruck = !$assignee || (method_exists($assignee, 'trashed') && $assignee->trashed()); + @endphp + + {!! $assignee + ? optional($assignee->present())->nameUrl() ?? e($assignee->name) + : trans('admin/reports/general.deleted_user') !!} + + + {{-- Actions: send reminder / delete --}} + + + @unless($item->acceptance->trashed()) +
+ @csrf + + @if ($assignee && $assignee->email) + + @else + + + + + + @endif + + + +
+ @endunless +
+ + + @endforeach @endif From dcbb09bbd7f22ab12ebe248e59444e3de2dce6d0 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 16 Sep 2025 15:59:53 -0700 Subject: [PATCH 02/22] added checkoutable class --- app/Http/Controllers/ReportsController.php | 11 ---- app/Models/Checkoutable.php | 73 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 app/Models/Checkoutable.php diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 61d772809bcb..5a350a0b9fc6 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1133,19 +1133,8 @@ public function getAssetAcceptanceReport($deleted = false) : View $query->withTrashed(); } -// $assetsForReport = $query->get() -// ->map(function ($acceptance) { -// return [ -// 'assetItem' => $acceptance->checkoutable, -// 'acceptance' => $acceptance, -// ]; -// }); -// dd($assetsForReport); -// $assetsForReport = $query->get()->map(function ($unaccepted) {}) $itemsForReport = $query->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted)); - - return view('reports/unaccepted_assets', compact('itemsForReport','showDeleted' )); } diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php new file mode 100644 index 000000000000..5c18a028aa9c --- /dev/null +++ b/app/Models/Checkoutable.php @@ -0,0 +1,73 @@ +checkoutable; + $acceptance = $unaccepted; + + $company = optional($unaccepted_row->company)->name ?? ''; + $category = $model = $name = $tag = ''; + $type = $acceptance->checkoutable_item_type ?? ''; + + if($unaccepted_row instanceof Asset){ + $category = optional($unaccepted_row->model?->category?->present())->nameUrl() ?? ''; + $model = optional($unaccepted_row->present())->modelUrl() ?? ''; + $name = optional($unaccepted_row->present())->nameUrl() ?? ''; + $tag = (string) ($unaccepted_row->asset_tag ?? ''); + } + elseif($unaccepted_row instanceof Accessory){ + $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; + $model = $unaccepted_row->model_number ?? ''; + $name = optional($unaccepted_row->present())->nameUrl() ?? ''; + $tag = ''; + + } + if($unaccepted_row instanceof LicenseSeat){ + $category = ''; + $model = ''; + $name = $unaccepted_row->license->name ?? ''; + $tag = ''; + } + if($unaccepted_row instanceof Component){ + $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; + $model = $unaccepted_row->model_number ?? ''; + $name = $unaccepted_row->present()->nameUrl() ?? ''; + $tag = ''; + } + return new self( + acceptance_id: $acceptance->id, + created_at: Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false), + company: $company, + category: $category, + model: $model, + asset_tag: $tag, + name: $name, + type: $type, + acceptance: $acceptance, + ); + } +} From 51ce570eb3b7c5ba714bdaa6e1f6edd505fc85be Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Thu, 18 Sep 2025 11:01:40 -0700 Subject: [PATCH 03/22] attempt to sort chronologically, can not resort still --- app/Http/Controllers/ReportsController.php | 3 ++- app/Models/Checkoutable.php | 11 +++++---- .../views/partials/bootstrap-table.blade.php | 11 +++++++++ .../views/reports/unaccepted_assets.blade.php | 24 ++++++++++--------- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 5a350a0b9fc6..946e810b8a1a 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1127,7 +1127,8 @@ public function getAssetAcceptanceReport($deleted = false) : View 'assignedTo' => function($query){ $query->withTrashed(); } - ]); + ])->orderByDesc('checkout_acceptances.created_at'); + if ($showDeleted) { $query->withTrashed(); diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php index 5c18a028aa9c..1648b5cae77d 100644 --- a/app/Models/Checkoutable.php +++ b/app/Models/Checkoutable.php @@ -9,7 +9,6 @@ class Checkoutable { public function __construct( public int $acceptance_id, - public string $created_at, public string $company, public string $category, public string $model, @@ -17,6 +16,7 @@ public function __construct( public string $name, public string $type, public object $acceptance, + public object $assignee, ){} // public static function fromCheckoutable(Asset|Accessory|etc..) @@ -29,10 +29,12 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $unaccepted_row = $unaccepted->checkoutable; $acceptance = $unaccepted; + $assignee = $acceptance->assignedTo; $company = optional($unaccepted_row->company)->name ?? ''; $category = $model = $name = $tag = ''; $type = $acceptance->checkoutable_item_type ?? ''; + if($unaccepted_row instanceof Asset){ $category = optional($unaccepted_row->model?->category?->present())->nameUrl() ?? ''; $model = optional($unaccepted_row->present())->modelUrl() ?? ''; @@ -43,24 +45,22 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; $model = $unaccepted_row->model_number ?? ''; $name = optional($unaccepted_row->present())->nameUrl() ?? ''; - $tag = ''; } if($unaccepted_row instanceof LicenseSeat){ $category = ''; $model = ''; $name = $unaccepted_row->license->name ?? ''; - $tag = ''; } if($unaccepted_row instanceof Component){ $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; $model = $unaccepted_row->model_number ?? ''; $name = $unaccepted_row->present()->nameUrl() ?? ''; - $tag = ''; } + $created = $acceptance->created_at; + return new self( acceptance_id: $acceptance->id, - created_at: Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false), company: $company, category: $category, model: $model, @@ -68,6 +68,7 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self name: $name, type: $type, acceptance: $acceptance, + assignee: $assignee, ); } } diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index 8ebbc9fb16d2..687927719c8e 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -870,6 +870,17 @@ function dateDisplayFormatter(value) { return value.formatted; } } + function createdAtSorter(a, b, rowA, rowB) { + const CREATED_AT_COL_INDEX = 0; + const ts = row => Number(row?._$el?.find('td').eq(CREATED_AT_COL_INDEX).attr('data-timestamp') || 0); + return ts(rowA) - ts(rowB); + } + + function createdAtSorter(a, b, rowA, rowB) { + const ts = row => + Number(row?._$el?.find('td').eq(CREATED_AT_COL_INDEX).attr('data-timestamp') || 0); + return ts(rowA) - ts(rowB); + } function iconFormatter(value) { if (value) { diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index ccffb10cf3ec..01081ce5aa39 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -48,7 +48,13 @@ class="table table-striped snipe-table" }'> - {{ trans('general.date') }} + + {{ trans('general.date') }} + {{ trans('general.type') }} {{ trans('admin/companies/table.title') }} {{ trans('general.category') }} @@ -63,8 +69,8 @@ class="table table-striped snipe-table" @if ($itemsForReport) @foreach ($itemsForReport as $item) acceptance->trashed()) style="text-decoration: line-through" @endif> - {{-- Created date (already formatted) --}} - + {{-- Created date --}} + {{ Helper::getFormattedDateObject($item->acceptance->created_at, 'datetime', false) }} {{-- Item Type --}} @@ -85,13 +91,9 @@ class="table table-striped snipe-table" {{ $item->asset_tag }} {{-- Assigned To (with soft-delete strike if needed) --}} - @php - $assignee = $item->acceptance->assignedTo; - $assigneeStruck = !$assignee || (method_exists($assignee, 'trashed') && $assignee->trashed()); - @endphp - - {!! $assignee - ? optional($assignee->present())->nameUrl() ?? e($assignee->name) + assignee || (method_exists($item->assignee, 'trashed') && $item->assignee->trashed())) style="text-decoration: line-through" @endif> + {!! $item->assignee + ? optional($item->assignee->present())->nameUrl() ?? e($item->assignee->name) : trans('admin/reports/general.deleted_user') !!} @@ -102,7 +104,7 @@ class="table table-striped snipe-table"
@csrf - @if ($assignee && $assignee->email) + @if ($item->assignee && $item->assignee->email) From a6cb75c481c2a6e719c9a8af12856e15cbe20f20 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Mon, 22 Sep 2025 12:19:34 -0700 Subject: [PATCH 04/22] remove sorter, didnt work --- resources/views/partials/bootstrap-table.blade.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index 687927719c8e..8ebbc9fb16d2 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -870,17 +870,6 @@ function dateDisplayFormatter(value) { return value.formatted; } } - function createdAtSorter(a, b, rowA, rowB) { - const CREATED_AT_COL_INDEX = 0; - const ts = row => Number(row?._$el?.find('td').eq(CREATED_AT_COL_INDEX).attr('data-timestamp') || 0); - return ts(rowA) - ts(rowB); - } - - function createdAtSorter(a, b, rowA, rowB) { - const ts = row => - Number(row?._$el?.find('td').eq(CREATED_AT_COL_INDEX).attr('data-timestamp') || 0); - return ts(rowA) - ts(rowB); - } function iconFormatter(value) { if (value) { From ab30df10ff9f2d53b4400e718cd445a05df0a99d Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Mon, 22 Sep 2025 12:20:32 -0700 Subject: [PATCH 05/22] remove sorter from blade --- resources/views/reports/unaccepted_assets.blade.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index 01081ce5aa39..a70652855fd3 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -48,13 +48,7 @@ class="table table-striped snipe-table" }'> - - {{ trans('general.date') }} - + {{ trans('general.date') }} {{ trans('general.type') }} {{ trans('admin/companies/table.title') }} {{ trans('general.category') }} From 7077faaf4ac42619e6e487e831e88e2cc1f5da7a Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 11:08:37 -0700 Subject: [PATCH 06/22] updated the postassetAcceptanceReport query and rows --- app/Http/Controllers/ReportsController.php | 57 ++++++++++++++++------ app/Models/Checkoutable.php | 6 +++ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 946e810b8a1a..d36944e973d3 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1220,22 +1220,44 @@ public function postAssetAcceptanceReport($deleted = false) : Response * Get all assets with pending checkout acceptances */ if($showDeleted) { - $acceptances = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')->withTrashed()->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model'])->get(); + $acceptances = CheckoutAcceptance::pending() + ->with([ + 'checkoutable' => function (MorphTo $acceptance) { + $acceptance->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'company'], + Accessory::class => ['category','checkouts', 'company'], + LicenseSeat::class => ['user', 'license'], + Component::class => ['assignedTo', 'company'], + ]); + }, + 'assignedTo' => function($acceptance){ + $acceptance->withTrashed(); + } + ])->orderByDesc('checkout_acceptances.created_at'); } else { - $acceptances = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model'])->get(); + $acceptances = CheckoutAcceptance::pending() + ->with([ + 'checkoutable' => function (MorphTo $acceptance) { + $acceptance->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'company'], + Accessory::class => ['category','checkouts', 'company'], + LicenseSeat::class => ['user', 'license'], + Component::class => ['assignedTo', 'company'], + ]); + }, + 'assignedTo' => function($acceptances){ + } + ])->orderByDesc('checkout_acceptances.created_at'); } - $assetsForReport = $acceptances - ->filter(function($acceptance) { - return $acceptance->checkoutable_type == 'App\Models\Asset'; - }) - ->map(function($acceptance) { - return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance]; - }); + $itemsForReport = $acceptances->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted)); $rows = []; $header = [ + trans('general.date'), + trans('general.type'), + trans('admin/companies/table.title'), trans('general.category'), trans('admin/hardware/form.model'), trans('admin/hardware/form.name'), @@ -1246,16 +1268,19 @@ public function postAssetAcceptanceReport($deleted = false) : Response $header = array_map('trim', $header); $rows[] = implode(',', $header); - foreach ($assetsForReport as $item) { + foreach ($itemsForReport as $item) { - if ($item['assetItem'] != null){ + if ($item != null){ $row = [ ]; - $row[] = str_replace(',', '', e($item['assetItem']->model->category->name)); - $row[] = str_replace(',', '', e($item['assetItem']->model->name)); - $row[] = str_replace(',', '', e($item['assetItem']->name)); - $row[] = str_replace(',', '', e($item['assetItem']->asset_tag)); - $row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->display_name : trans('admin/reports/general.deleted_user'))); + $row[] = str_replace(',', '', $item->acceptance->created_at); + $row[] = str_replace(',', '', $item->type); + $row[] = str_replace(',', '', $item->company); + $row[] = str_replace(',', '', $item->category_plain); + $row[] = str_replace(',', '', $item->model_plain); + $row[] = str_replace(',', '', $item->name_plain); + $row[] = str_replace(',', '', $item->asset_tag); + $row[] = str_replace(',', '', ($item->acceptance->assignedto) ? $item->acceptance->assignedto->display_name : trans('admin/reports/general.deleted_user')); $rows[] = implode(',', $row); } } diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php index 1648b5cae77d..21420fdd86ad 100644 --- a/app/Models/Checkoutable.php +++ b/app/Models/Checkoutable.php @@ -17,6 +17,9 @@ public function __construct( public string $type, public object $acceptance, public object $assignee, + public readonly string $category_plain, + public readonly string $model_plain, + public readonly string $name_plain, ){} // public static function fromCheckoutable(Asset|Accessory|etc..) @@ -69,6 +72,9 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self type: $type, acceptance: $acceptance, assignee: $assignee, + category_plain: optional($unaccepted_row->model?->category)->name ?? '', + model_plain: optional($unaccepted_row->model)->name ?? '', + name_plain: (string) ($unaccepted_row->name ?? ''), ); } } From ca8eae4064b2492d27e5447dcf6c7665e6d90798 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 11:40:29 -0700 Subject: [PATCH 07/22] updated the csv values to be plain --- app/Http/Controllers/ReportsController.php | 28 ++++++------------- app/Models/Checkoutable.php | 5 +++- .../views/reports/unaccepted_assets.blade.php | 2 +- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index d36944e973d3..689e5c38f3ab 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1219,25 +1219,13 @@ public function postAssetAcceptanceReport($deleted = false) : Response /** * Get all assets with pending checkout acceptances */ - if($showDeleted) { + $acceptances = CheckoutAcceptance::pending() ->with([ - 'checkoutable' => function (MorphTo $acceptance) { - $acceptance->morphWith([ - Asset::class => ['model.category', 'assignedTo', 'company'], - Accessory::class => ['category','checkouts', 'company'], - LicenseSeat::class => ['user', 'license'], - Component::class => ['assignedTo', 'company'], - ]); - }, - 'assignedTo' => function($acceptance){ + 'checkoutable' => function (MorphTo $acceptance) use ($showDeleted) { + if ($showDeleted) { $acceptance->withTrashed(); } - ])->orderByDesc('checkout_acceptances.created_at'); - } else { - $acceptances = CheckoutAcceptance::pending() - ->with([ - 'checkoutable' => function (MorphTo $acceptance) { $acceptance->morphWith([ Asset::class => ['model.category', 'assignedTo', 'company'], Accessory::class => ['category','checkouts', 'company'], @@ -1245,10 +1233,12 @@ public function postAssetAcceptanceReport($deleted = false) : Response Component::class => ['assignedTo', 'company'], ]); }, - 'assignedTo' => function($acceptances){ - } + 'assignedTo', ])->orderByDesc('checkout_acceptances.created_at'); - } + + if ($showDeleted) { + $acceptances->withTrashed(); + } $itemsForReport = $acceptances->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted)); @@ -1275,7 +1265,7 @@ public function postAssetAcceptanceReport($deleted = false) : Response $row = [ ]; $row[] = str_replace(',', '', $item->acceptance->created_at); $row[] = str_replace(',', '', $item->type); - $row[] = str_replace(',', '', $item->company); + $row[] = str_replace(',', '', $item->company_plain); $row[] = str_replace(',', '', $item->category_plain); $row[] = str_replace(',', '', $item->model_plain); $row[] = str_replace(',', '', $item->name_plain); diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php index 21420fdd86ad..b629aa7d0d23 100644 --- a/app/Models/Checkoutable.php +++ b/app/Models/Checkoutable.php @@ -20,6 +20,7 @@ public function __construct( public readonly string $category_plain, public readonly string $model_plain, public readonly string $name_plain, + public readonly string $company_plain, ){} // public static function fromCheckoutable(Asset|Accessory|etc..) @@ -33,7 +34,7 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $acceptance = $unaccepted; $assignee = $acceptance->assignedTo; - $company = optional($unaccepted_row->company)->name ?? ''; + $company = optional($unaccepted_row->company)->present()?->nameUrl() ?? ''; $category = $model = $name = $tag = ''; $type = $acceptance->checkoutable_item_type ?? ''; @@ -72,9 +73,11 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self type: $type, acceptance: $acceptance, assignee: $assignee, + //plain text for CSVs category_plain: optional($unaccepted_row->model?->category)->name ?? '', model_plain: optional($unaccepted_row->model)->name ?? '', name_plain: (string) ($unaccepted_row->name ?? ''), + company_plain: optional($unaccepted_row->company)->name ?? '', ); } } diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index a70652855fd3..2b78a756ef31 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -70,7 +70,7 @@ class="table table-striped snipe-table" {{-- Item Type --}} {{ $item->type }} {{-- Company name --}} - {{ $item->company }} + {!! $item->company !!} {{-- Category --}} {!! $item->category !!} From 20adad3c6b64e038dfa12b9ac96c0f88d007339b Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 12:09:21 -0700 Subject: [PATCH 08/22] fix company name link, clean up query --- app/Http/Controllers/ReportsController.php | 2 +- resources/views/reports/unaccepted_assets.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 689e5c38f3ab..6351121b51a3 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1214,7 +1214,7 @@ public function deleteAssetAcceptance($acceptanceId = null) : RedirectResponse public function postAssetAcceptanceReport($deleted = false) : Response { $this->authorize('reports.view'); - $showDeleted = $deleted == 'deleted'; + $showDeleted = request('deleted') === 'deleted';; /** * Get all assets with pending checkout acceptances diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index 2b78a756ef31..47ad6908b626 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -70,7 +70,7 @@ class="table table-striped snipe-table" {{-- Item Type --}} {{ $item->type }} {{-- Company name --}} - {!! $item->company !!} + {!! $item->company !!}/td> {{-- Category --}} {!! $item->category !!} From 5af85bfe7d11865959b4ab39e5930f7fa8f76b30 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 12:10:50 -0700 Subject: [PATCH 09/22] further clean up --- app/Http/Controllers/ReportsController.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 6351121b51a3..5bb6a7d0db3a 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1222,10 +1222,7 @@ public function postAssetAcceptanceReport($deleted = false) : Response $acceptances = CheckoutAcceptance::pending() ->with([ - 'checkoutable' => function (MorphTo $acceptance) use ($showDeleted) { - if ($showDeleted) { - $acceptance->withTrashed(); - } + 'checkoutable' => function (MorphTo $acceptance) { $acceptance->morphWith([ Asset::class => ['model.category', 'assignedTo', 'company'], Accessory::class => ['category','checkouts', 'company'], From 6f3323c195e12dd35ab100b5d54a7e415e1e2905 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 12:50:26 -0700 Subject: [PATCH 10/22] fix data in view model --- app/Models/Checkoutable.php | 20 +++++++++++++------ .../views/reports/unaccepted_assets.blade.php | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php index b629aa7d0d23..d73de4d4b345 100644 --- a/app/Models/Checkoutable.php +++ b/app/Models/Checkoutable.php @@ -52,14 +52,22 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self } if($unaccepted_row instanceof LicenseSeat){ - $category = ''; + $category = optional($unaccepted_row->license->category?->present())->nameUrl() ?? ''; + $company = optional($unaccepted_row->license->company?->present())?->nameUrl() ?? ''; $model = ''; $name = $unaccepted_row->license->name ?? ''; } + if($unaccepted_row instanceof Consumable){ + $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; + $model = $unaccepted_row->model_number ?? ''; + $name = $unaccepted_row?->present()?->nameUrl() ?? ''; + + } if($unaccepted_row instanceof Component){ $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; $model = $unaccepted_row->model_number ?? ''; - $name = $unaccepted_row->present()->nameUrl() ?? ''; + $name = $unaccepted_row?->present()?->nameUrl() ?? ''; + } $created = $acceptance->created_at; @@ -74,10 +82,10 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self acceptance: $acceptance, assignee: $assignee, //plain text for CSVs - category_plain: optional($unaccepted_row->model?->category)->name ?? '', - model_plain: optional($unaccepted_row->model)->name ?? '', - name_plain: (string) ($unaccepted_row->name ?? ''), - company_plain: optional($unaccepted_row->company)->name ?? '', + category_plain: ($unaccepted_row->model?->category?->name ?? $unaccepted_row->license->category?->name ?? $unaccepted_row->category?->name ?? ''), + model_plain: ($unaccepted_row->model?->name ?? $unaccepted_row->model_number ?? ''), + name_plain: ($unaccepted_row->name ?? $unaccepted_row->license?->name ?? ''), + company_plain: ($unaccepted_row->company)->name ?? $unaccepted_row->license->company?->name ?? '', ); } } diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index 47ad6908b626..2b78a756ef31 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -70,7 +70,7 @@ class="table table-striped snipe-table" {{-- Item Type --}} {{ $item->type }} {{-- Company name --}} - {!! $item->company !!}/td> + {!! $item->company !!} {{-- Category --}} {!! $item->category !!} From 2b5254e68f05173521b213780df87672520b0485 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 16:55:10 -0700 Subject: [PATCH 11/22] fixed, eager loading, variable names, clean up --- app/Http/Controllers/ReportsController.php | 13 +++++---- app/Models/Checkoutable.php | 27 +++++++------------ .../views/reports/unaccepted_assets.blade.php | 15 +++++------ 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 5bb6a7d0db3a..10c0a2e943f1 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -12,6 +12,7 @@ use App\Models\Category; use App\Models\Checkoutable; use App\Models\Component; +use App\Models\Consumable; use App\Models\LicenseSeat; use App\Models\Maintenance; use App\Models\CheckoutAcceptance; @@ -22,6 +23,7 @@ use App\Models\ReportTemplate; use App\Models\Setting; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -1228,6 +1230,7 @@ public function postAssetAcceptanceReport($deleted = false) : Response Accessory::class => ['category','checkouts', 'company'], LicenseSeat::class => ['user', 'license'], Component::class => ['assignedTo', 'company'], + Consumable::class => ['company'], ]); }, 'assignedTo', @@ -1247,7 +1250,7 @@ public function postAssetAcceptanceReport($deleted = false) : Response trans('admin/companies/table.title'), trans('general.category'), trans('admin/hardware/form.model'), - trans('admin/hardware/form.name'), + trans('general.name'), trans('admin/hardware/table.asset_tag'), trans('admin/hardware/table.checkoutto'), ]; @@ -1262,10 +1265,10 @@ public function postAssetAcceptanceReport($deleted = false) : Response $row = [ ]; $row[] = str_replace(',', '', $item->acceptance->created_at); $row[] = str_replace(',', '', $item->type); - $row[] = str_replace(',', '', $item->company_plain); - $row[] = str_replace(',', '', $item->category_plain); - $row[] = str_replace(',', '', $item->model_plain); - $row[] = str_replace(',', '', $item->name_plain); + $row[] = str_replace(',', '', $item->plain_text_company); + $row[] = str_replace(',', '', $item->plain_text_category); + $row[] = str_replace(',', '', $item->plain_text_model); + $row[] = str_replace(',', '', $item->plain_text_name); $row[] = str_replace(',', '', $item->asset_tag); $row[] = str_replace(',', '', ($item->acceptance->assignedto) ? $item->acceptance->assignedto->display_name : trans('admin/reports/general.deleted_user')); $rows[] = implode(',', $row); diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php index d73de4d4b345..bf169eb04885 100644 --- a/app/Models/Checkoutable.php +++ b/app/Models/Checkoutable.php @@ -2,9 +2,6 @@ namespace App\Models; -use App\Helpers\Helper; - - class Checkoutable { public function __construct( @@ -17,17 +14,12 @@ public function __construct( public string $type, public object $acceptance, public object $assignee, - public readonly string $category_plain, - public readonly string $model_plain, - public readonly string $name_plain, - public readonly string $company_plain, + public readonly string $plain_text_category, + public readonly string $plain_text_model, + public readonly string $plain_text_name, + public readonly string $plain_text_company, ){} -// public static function fromCheckoutable(Asset|Accessory|etc..) -// { -// -// } - public static function fromAcceptance(CheckoutAcceptance $unaccepted): self { $unaccepted_row = $unaccepted->checkoutable; @@ -55,7 +47,7 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $category = optional($unaccepted_row->license->category?->present())->nameUrl() ?? ''; $company = optional($unaccepted_row->license->company?->present())?->nameUrl() ?? ''; $model = ''; - $name = $unaccepted_row->license->name ?? ''; + $name = $unaccepted_row->license->present()->nameUrl() ?? ''; } if($unaccepted_row instanceof Consumable){ $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; @@ -69,7 +61,6 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $name = $unaccepted_row?->present()?->nameUrl() ?? ''; } - $created = $acceptance->created_at; return new self( acceptance_id: $acceptance->id, @@ -82,10 +73,10 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self acceptance: $acceptance, assignee: $assignee, //plain text for CSVs - category_plain: ($unaccepted_row->model?->category?->name ?? $unaccepted_row->license->category?->name ?? $unaccepted_row->category?->name ?? ''), - model_plain: ($unaccepted_row->model?->name ?? $unaccepted_row->model_number ?? ''), - name_plain: ($unaccepted_row->name ?? $unaccepted_row->license?->name ?? ''), - company_plain: ($unaccepted_row->company)->name ?? $unaccepted_row->license->company?->name ?? '', + plain_text_category: ($unaccepted_row->model?->category?->name ?? $unaccepted_row->license->category?->name ?? $unaccepted_row->category?->name ?? ''), + plain_text_model: ($unaccepted_row->model?->name ?? $unaccepted_row->model_number ?? ''), + plain_text_name: ($unaccepted_row->name ?? $unaccepted_row->license?->name ?? ''), + plain_text_company: ($unaccepted_row->company)->name ?? $unaccepted_row->license->company?->name ?? '', ); } } diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index 2b78a756ef31..5dbefbc8e157 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -53,7 +53,7 @@ class="table table-striped snipe-table" {{ trans('admin/companies/table.title') }} {{ trans('general.category') }} {{ trans('admin/hardware/form.model') }} - {{ trans('admin/hardware/form.name') }} + {{ trans('general.name') }} {{ trans('admin/hardware/table.asset_tag') }} {{ trans('admin/hardware/table.checkoutto') }} {{ trans('table.actions') }} @@ -92,8 +92,8 @@ class="table table-striped snipe-table" {{-- Actions: send reminder / delete --}} - - + + @unless($item->acceptance->trashed()) @csrf @@ -104,10 +104,10 @@ class="table table-striped snipe-table" @else - - - - + + + + @endif @endunless - @endforeach From 3527c357cc8e6a73b5941dbe3770039ebeca7485 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 16:57:06 -0700 Subject: [PATCH 12/22] whoops --- app/Http/Controllers/ReportsController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 10c0a2e943f1..be00c2c0e185 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1124,6 +1124,7 @@ public function getAssetAcceptanceReport($deleted = false) : View Accessory::class => ['category','checkouts', 'company'], LicenseSeat::class => ['user', 'license'], Component::class => ['assignedTo', 'company'], + Consumable::class => ['company'], ]); }, 'assignedTo' => function($query){ From d31d99b40f3dd56e697d2e285e94f0c88b6798e2 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 17:00:08 -0700 Subject: [PATCH 13/22] remove else and blank lines --- app/Models/Checkoutable.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php index bf169eb04885..9bf091c15f58 100644 --- a/app/Models/Checkoutable.php +++ b/app/Models/Checkoutable.php @@ -37,11 +37,10 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $name = optional($unaccepted_row->present())->nameUrl() ?? ''; $tag = (string) ($unaccepted_row->asset_tag ?? ''); } - elseif($unaccepted_row instanceof Accessory){ + if($unaccepted_row instanceof Accessory){ $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; $model = $unaccepted_row->model_number ?? ''; $name = optional($unaccepted_row->present())->nameUrl() ?? ''; - } if($unaccepted_row instanceof LicenseSeat){ $category = optional($unaccepted_row->license->category?->present())->nameUrl() ?? ''; @@ -53,13 +52,11 @@ public static function fromAcceptance(CheckoutAcceptance $unaccepted): self $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; $model = $unaccepted_row->model_number ?? ''; $name = $unaccepted_row?->present()?->nameUrl() ?? ''; - } if($unaccepted_row instanceof Component){ $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; $model = $unaccepted_row->model_number ?? ''; $name = $unaccepted_row?->present()?->nameUrl() ?? ''; - } return new self( From 0ce20c1eddff6e1f9f6a2f75aadbbc0d61971ed5 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 23 Sep 2025 18:40:03 -0700 Subject: [PATCH 14/22] fix send reminder method to handle other types --- app/Http/Controllers/ReportsController.php | 63 ++++++++++++++++++---- app/Models/Component.php | 14 ++++- app/Models/Consumable.php | 14 +++++ app/Models/License.php | 7 ++- app/Models/LicenseSeat.php | 24 +++++++++ 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index be00c2c0e185..6d79f80893ba 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -3,7 +3,11 @@ namespace App\Http\Controllers; use App\Helpers\Helper; +use App\Mail\CheckoutAccessoryMail; use App\Mail\CheckoutAssetMail; +use App\Mail\CheckoutComponentMail; +use App\Mail\CheckoutConsumableMail; +use App\Mail\CheckoutLicenseMail; use App\Models\Accessory; use App\Models\AccessoryCheckout; use App\Models\Actionlog; @@ -27,6 +31,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Mail\Mailable; use Illuminate\Support\Facades\Mail; use \Illuminate\Contracts\View\View; use League\Csv\Reader; @@ -1151,41 +1156,77 @@ public function getAssetAcceptanceReport($deleted = false) : View public function sentAssetAcceptanceReminder(Request $request) : RedirectResponse { $this->authorize('reports.view'); - - if (!$acceptance = CheckoutAcceptance::pending()->find($request->input('acceptance_id'))) { + $id = $request->input('acceptance_id'); + $query = CheckoutAcceptance::query() + ->with([ + 'checkoutable' => function (MorphTo $query) { + $query->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'company', 'checkouts'], + Accessory::class => ['category', 'company', 'checkouts'], + LicenseSeat::class => ['user', 'license', 'checkouts'], + Component::class => ['assignedTo', 'company', 'checkouts'], + Consumable::class => ['company', 'checkouts'], + ]); + }, + 'assignedTo' => fn ($q) => $q->withTrashed(), + ]) + ->pending(); + $acceptance = $query->find($id); + if (!$acceptance) { Log::debug('No pending acceptances'); // Redirect to the unaccepted assets report page with error return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } + $item = $acceptance->checkoutable; + $assignee = $acceptance->assignedTo ?? $item->assignedTo ?? null; + $email = $assignee?->email; + $locale = $assignee?->locale; - $assetItem = $acceptance->checkoutable; - - Log::debug(print_r($assetItem, true)); + Log::debug(print_r($acceptance, true)); if (is_null($acceptance->created_at)){ Log::debug('No acceptance created_at'); return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } else { - $logItem_res = $assetItem->checkouts()->where('created_at', '=', $acceptance->created_at)->get(); - + if($item instanceof LicenseSeat){ + $logItem_res = $item->license->checkouts()->where('created_at', '=', $acceptance->created_at)->get(); + } + else{ + $logItem_res = $item->checkouts()->where('created_at', '=', $acceptance->created_at)->get(); + } if ($logItem_res->isEmpty()){ Log::debug('Acceptance date mismatch'); return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } $logItem = $logItem_res[0]; } - $email = $assetItem->assignedTo?->email; - $locale = $assetItem->assignedTo?->locale; if (is_null($email) || $email === '') { return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.no_email')); } - - Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note, firstTimeSending: false))->locale($locale)); + $mailable = $this->getCheckoutMailType($acceptance, $logItem); + Mail::to($email)->send($mailable->locale($locale)); return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent')); } + private function getCheckoutMailType(CheckoutAcceptance $acceptance, $logItem) : Mailable + { + $lookup = [ + Accessory::class => CheckoutAccessoryMail::class, + Asset::class => CheckoutAssetMail::class, + LicenseSeat::class => CheckoutLicenseMail::class, + Consumable::class => CheckoutConsumableMail::class, + Component::class => CheckoutComponentMail::class, + ]; + $mailable= $lookup[get_class($acceptance->checkoutable)]; + + return new $mailable($acceptance->checkoutable, + $acceptance->checkedOutTo ?? $acceptance->assignedTo, + $logItem->adminuser, + $acceptance, + $acceptance->note); + } /** * sentAssetAcceptanceReminder * diff --git a/app/Models/Component.php b/app/Models/Component.php index ddcb3ad575c1..2ce67c5ec787 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -274,7 +274,19 @@ public function checkin_email() { return $this->category?->checkin_email; } - + /** + * Get the list of checkouts for this License + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } /** * Check how many items within a component are remaining diff --git a/app/Models/Consumable.php b/app/Models/Consumable.php index 8c6e43c490da..27b7a5581f2a 100644 --- a/app/Models/Consumable.php +++ b/app/Models/Consumable.php @@ -313,6 +313,20 @@ public function numRemaining() return $remaining; } + /** + * Get the list of checkouts for this consumable + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } + /** * ----------------------------------------------- * BEGIN MUTATORS diff --git a/app/Models/License.php b/app/Models/License.php index d84f42f0a190..2ee10cddba4a 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -391,7 +391,12 @@ public function checkin_email() } return false; } - + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } /** * Determine whether the user should be required to accept the license * diff --git a/app/Models/LicenseSeat.php b/app/Models/LicenseSeat.php index 9ddd3fb4319e..9ad51901e7fe 100755 --- a/app/Models/LicenseSeat.php +++ b/app/Models/LicenseSeat.php @@ -118,7 +118,31 @@ public function location() return false; } + /** + * Get the list of checkouts for this License + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } + /** + * Establishes the license -> action logs relationship + * + * @author [A. Gianotto] [] + * @since [v3.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function assetlog() + { + return $this->hasMany(Actionlog::class, 'item_id')->where('item_type', self::class)->orderBy('created_at', 'desc')->withTrashed(); + } /** * Query builder scope to order on department * From 9ac2ea2a52423c9524f332a4c59691eda0fd5328 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 13:22:33 -0700 Subject: [PATCH 15/22] fix test to check all mailable types for reminders --- app/Http/Controllers/ReportsController.php | 4 +- .../Email/AssetAcceptanceReminderTest.php | 61 ++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 6d79f80893ba..d06e779a40e7 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -1189,10 +1189,10 @@ public function sentAssetAcceptanceReminder(Request $request) : RedirectResponse return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } else { if($item instanceof LicenseSeat){ - $logItem_res = $item->license->checkouts()->where('created_at', '=', $acceptance->created_at)->get(); + $logItem_res = $item->license->checkouts()->with('adminuser')->where('created_at', '=', $acceptance->created_at)->get(); } else{ - $logItem_res = $item->checkouts()->where('created_at', '=', $acceptance->created_at)->get(); + $logItem_res = $item->checkouts()->with('adminuser')->where('created_at', '=', $acceptance->created_at)->get(); } if ($logItem_res->isEmpty()){ Log::debug('Acceptance date mismatch'); diff --git a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php index deb3e07d2c01..246d52366cd0 100644 --- a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php +++ b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php @@ -2,8 +2,19 @@ namespace Tests\Feature\Notifications\Email; +use App\Mail\CheckoutAccessoryMail; use App\Mail\CheckoutAssetMail; +use App\Mail\CheckoutComponentMail; +use App\Mail\CheckoutConsumableMail; +use App\Mail\CheckoutLicenseMail; +use App\Models\Accessory; +use App\Models\Actionlog; +use App\Models\Asset; use App\Models\CheckoutAcceptance; +use App\Models\Component; +use App\Models\Consumable; +use App\Models\License; +use App\Models\LicenseSeat; use App\Models\User; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\DataProvider; @@ -86,16 +97,52 @@ public function testUserWithoutEmailAddressHandledGracefully($callback) public function testReminderIsSentToUser() { - $checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create(); + $checkedOutBy = User::factory()->canViewReports()->create(); + + $checkoutTypes = [ + Asset::class => CheckoutAssetMail::class, + Accessory::class => CheckoutAccessoryMail::class, + LicenseSeat::class => CheckoutLicenseMail::class, + Consumable::class => CheckoutConsumableMail::class, + //for the future its setup for components, but we dont send reminders for components at the moment. +// Component::class => CheckoutComponentMail::class, + ]; - $this->actingAs(User::factory()->canViewReports()->create()) - ->post($this->routeFor($checkoutAcceptance)) + $assignee = User::factory()->create(['email' => 'test@example.com']); + foreach ($checkoutTypes as $modelClass => $mailable) { + + $item = $modelClass::factory()->create(); + $acceptance = CheckoutAcceptance::factory()->pending()->create([ + 'checkoutable_id' => $item->id, + 'checkoutable_type' => $modelClass, + 'assigned_to_id' => $assignee->id, + ]); + + if ($modelClass === LicenseSeat::class) { + $logType = License::class; + $logId = $item->license->id; + } else { + $logType = $modelClass; + $logId = $item->id; + } + + Actionlog::factory()->create([ + 'action_type' => 'checkout', + 'created_by' => $checkedOutBy->id, + 'target_id' => $assignee->id, + 'item_type' => $logType, + 'item_id' => $logId, + 'created_at' => $acceptance->created_at, + ]); + + $this->actingAs($checkedOutBy) + ->post($this->routeFor($acceptance)) ->assertRedirect(route('reports/unaccepted_assets')); + } - Mail::assertSent(CheckoutAssetMail::class, 1); - Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) use ($checkoutAcceptance) { - return $mail->hasTo($checkoutAcceptance->assignedTo->email) - && $mail->hasSubject(trans('mail.unaccepted_asset_reminder')); + Mail::assertSent($mailable, 1); + Mail::assertSent($mailable, function ($mail) use ($assignee) { + return $mail->hasTo($assignee->email); }); } From bf6964ee621185b5a704f229760a74f0339b3f4d Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 13:41:04 -0700 Subject: [PATCH 16/22] tests passing, CheckoutAcceptance Factory needs eyes though --- .../factories/CheckoutAcceptanceFactory.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index bb56ab2b4a1f..4f8f3fe46950 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -27,16 +27,16 @@ public function definition() public function configure(): static { return $this->afterCreating(function (CheckoutAcceptance $acceptance) { - if ($acceptance->checkoutable instanceof Asset) { - $this->createdAssociatedActionLogEntry($acceptance); - } - - if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) { - $acceptance->checkoutable->update([ - 'assigned_to' => $acceptance->assigned_to_id, - 'assigned_type' => get_class($acceptance->assignedTo), - ]); - } +// if ($acceptance->checkoutable instanceof Asset) { +// $this->createdAssociatedActionLogEntry($acceptance); +// } +// +// if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) { +// $acceptance->checkoutable->update([ +// 'assigned_to' => $acceptance->assigned_to_id, +// 'assigned_type' => get_class($acceptance->assignedTo), +// ]); +// } }); } From 6ca0e19819323407d8e57bea2d910804d57f52dc Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 13:50:41 -0700 Subject: [PATCH 17/22] uncomment code --- .../factories/CheckoutAcceptanceFactory.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index 4f8f3fe46950..bb56ab2b4a1f 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -27,16 +27,16 @@ public function definition() public function configure(): static { return $this->afterCreating(function (CheckoutAcceptance $acceptance) { -// if ($acceptance->checkoutable instanceof Asset) { -// $this->createdAssociatedActionLogEntry($acceptance); -// } -// -// if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) { -// $acceptance->checkoutable->update([ -// 'assigned_to' => $acceptance->assigned_to_id, -// 'assigned_type' => get_class($acceptance->assignedTo), -// ]); -// } + if ($acceptance->checkoutable instanceof Asset) { + $this->createdAssociatedActionLogEntry($acceptance); + } + + if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) { + $acceptance->checkoutable->update([ + 'assigned_to' => $acceptance->assigned_to_id, + 'assigned_type' => get_class($acceptance->assignedTo), + ]); + } }); } From 6f990dd1de5d8a9f4ba4ac582da626805eca3d02 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 15:27:16 -0700 Subject: [PATCH 18/22] adds an option to disable Auto assigned an actionlogs in factories --- .../factories/CheckoutAcceptanceFactory.php | 20 ++++++++++-- .../Email/AssetAcceptanceReminderTest.php | 31 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index bb56ab2b4a1f..5e0016763dbb 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -23,23 +23,39 @@ public function definition() 'assigned_to_id' => User::factory(), ]; } + protected static bool $skipAutoAssign = false; + + public function withoutAutoAssign(): static + { + // turn off for this create() call + static::$skipAutoAssign = true; + + // ensure it turns back on AFTER creating + return $this->afterCreating(function () { + static::$skipAutoAssign = false; + }); + } public function configure(): static { return $this->afterCreating(function (CheckoutAcceptance $acceptance) { + if (static::$skipAutoAssign) { + return; // short-circuit + } if ($acceptance->checkoutable instanceof Asset) { $this->createdAssociatedActionLogEntry($acceptance); } if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) { $acceptance->checkoutable->update([ - 'assigned_to' => $acceptance->assigned_to_id, - 'assigned_type' => get_class($acceptance->assignedTo), + 'assigned_to' => $acceptance->assigned_to_id, + 'assigned_type'=> get_class($acceptance->assignedTo), ]); } }); } + public function forAccessory() { return $this->state([ diff --git a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php index deb3e07d2c01..11afdaba4b73 100644 --- a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php +++ b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php @@ -86,7 +86,38 @@ public function testUserWithoutEmailAddressHandledGracefully($callback) public function testReminderIsSentToUser() { +<<<<<<< Updated upstream $checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create(); +======= + $checkedOutBy = User::factory()->canViewReports()->create(); + + $checkoutTypes = [ + Asset::class => CheckoutAssetMail::class, + Accessory::class => CheckoutAccessoryMail::class, + LicenseSeat::class => CheckoutLicenseMail::class, + Consumable::class => CheckoutConsumableMail::class, + //for the future its setup for components, but we dont send reminders for components at the moment. +// Component::class => CheckoutComponentMail::class, + ]; + + $assignee = User::factory()->create(['email' => 'test@example.com']); + foreach ($checkoutTypes as $modelClass => $mailable) { + + $item = $modelClass::factory()->create(); + $acceptance = CheckoutAcceptance::factory()->withoutAutoAssign()->pending()->create([ + 'checkoutable_id' => $item->id, + 'checkoutable_type' => $modelClass, + 'assigned_to_id' => $assignee->id, + ]); + + if ($modelClass === LicenseSeat::class) { + $logType = License::class; + $logId = $item->license->id; + } else { + $logType = $modelClass; + $logId = $item->id; + } +>>>>>>> Stashed changes $this->actingAs(User::factory()->canViewReports()->create()) ->post($this->routeFor($checkoutAcceptance)) From 533d82d4d8f0cf8611c8beb7267687075ec33295 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 15:34:02 -0700 Subject: [PATCH 19/22] remove unnecessary changes --- .../factories/CheckoutAcceptanceFactory.php | 2 +- .../Email/AssetAcceptanceReminderTest.php | 31 ------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index 5e0016763dbb..b53885807f10 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -25,7 +25,7 @@ public function definition() } protected static bool $skipAutoAssign = false; - public function withoutAutoAssign(): static + public function withoutActionLog(): static { // turn off for this create() call static::$skipAutoAssign = true; diff --git a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php index 11afdaba4b73..deb3e07d2c01 100644 --- a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php +++ b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php @@ -86,38 +86,7 @@ public function testUserWithoutEmailAddressHandledGracefully($callback) public function testReminderIsSentToUser() { -<<<<<<< Updated upstream $checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create(); -======= - $checkedOutBy = User::factory()->canViewReports()->create(); - - $checkoutTypes = [ - Asset::class => CheckoutAssetMail::class, - Accessory::class => CheckoutAccessoryMail::class, - LicenseSeat::class => CheckoutLicenseMail::class, - Consumable::class => CheckoutConsumableMail::class, - //for the future its setup for components, but we dont send reminders for components at the moment. -// Component::class => CheckoutComponentMail::class, - ]; - - $assignee = User::factory()->create(['email' => 'test@example.com']); - foreach ($checkoutTypes as $modelClass => $mailable) { - - $item = $modelClass::factory()->create(); - $acceptance = CheckoutAcceptance::factory()->withoutAutoAssign()->pending()->create([ - 'checkoutable_id' => $item->id, - 'checkoutable_type' => $modelClass, - 'assigned_to_id' => $assignee->id, - ]); - - if ($modelClass === LicenseSeat::class) { - $logType = License::class; - $logId = $item->license->id; - } else { - $logType = $modelClass; - $logId = $item->id; - } ->>>>>>> Stashed changes $this->actingAs(User::factory()->canViewReports()->create()) ->post($this->routeFor($checkoutAcceptance)) From 82bdd43168e548396d95038d081f330e4060d3d6 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 15:38:30 -0700 Subject: [PATCH 20/22] renamed variable --- database/factories/CheckoutAcceptanceFactory.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index b53885807f10..1cd139176e56 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -23,23 +23,23 @@ public function definition() 'assigned_to_id' => User::factory(), ]; } - protected static bool $skipAutoAssign = false; + protected static bool $skipActionLog = false; public function withoutActionLog(): static { // turn off for this create() call - static::$skipAutoAssign = true; + static::$skipActionLog = true; // ensure it turns back on AFTER creating return $this->afterCreating(function () { - static::$skipAutoAssign = false; + static::$skipActionLog = false; }); } public function configure(): static { return $this->afterCreating(function (CheckoutAcceptance $acceptance) { - if (static::$skipAutoAssign) { + if (static::$skipActionLog) { return; // short-circuit } if ($acceptance->checkoutable instanceof Asset) { From 881c789a75e8d4313a272d8089bf71162a3021f6 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 24 Sep 2025 15:45:01 -0700 Subject: [PATCH 21/22] add usuage of withoutactionlog --- .../Feature/Notifications/Email/AssetAcceptanceReminderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php index 246d52366cd0..04081241c90a 100644 --- a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php +++ b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php @@ -112,7 +112,7 @@ public function testReminderIsSentToUser() foreach ($checkoutTypes as $modelClass => $mailable) { $item = $modelClass::factory()->create(); - $acceptance = CheckoutAcceptance::factory()->pending()->create([ + $acceptance = CheckoutAcceptance::factory()->withoutActionLog()->pending()->create([ 'checkoutable_id' => $item->id, 'checkoutable_type' => $modelClass, 'assigned_to_id' => $assignee->id, From 3ae7a7703260b1f8cdc15ddcfc8c7f4ecca7d113 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Thu, 25 Sep 2025 10:43:41 -0700 Subject: [PATCH 22/22] update the snipeit reminder command --- .../Commands/SendAcceptanceReminder.php | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/app/Console/Commands/SendAcceptanceReminder.php b/app/Console/Commands/SendAcceptanceReminder.php index 67efecbb3453..4eb4a5114fe6 100644 --- a/app/Console/Commands/SendAcceptanceReminder.php +++ b/app/Console/Commands/SendAcceptanceReminder.php @@ -3,13 +3,18 @@ namespace App\Console\Commands; use App\Mail\UnacceptedAssetReminderMail; +use App\Models\Accessory; use App\Models\Asset; use App\Models\CheckoutAcceptance; +use App\Models\Component; +use App\Models\Consumable; +use App\Models\LicenseSeat; use App\Models\Setting; use App\Models\User; use App\Notifications\CheckoutAssetNotification; use App\Notifications\CurrentInventory; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Facades\Mail; class SendAcceptanceReminder extends Command @@ -45,19 +50,30 @@ public function __construct() */ public function handle() { - $pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset') - ->whereHas('checkoutable', function($query) { - $query->where('accepted_at', null) - ->where('declined_at', null); - }) - ->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser']) - ->get(); + $pending = CheckoutAcceptance::query() + ->with([ + 'checkoutable' => function (MorphTo $morph) { + $morph->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'adminuser', 'company', 'checkouts'], + Accessory::class => ['category', 'company', 'checkouts'], + LicenseSeat::class => ['user', 'license', 'checkouts'], + Component::class => ['assignedTo', 'company', 'checkouts'], + Consumable::class => ['company', 'checkouts'], + ]); + }, + 'assignedTo', + ]) + ->whereHasMorph( + 'checkoutable', + [Asset::class, Accessory::class, LicenseSeat::class, Component::class, Consumable::class], + fn ($q) => $q->whereNull('accepted_at') + ->whereNull('declined_at') + ) + ->pending() + ->get(); $count = 0; $unacceptedAssetGroups = $pending - ->filter(function($acceptance) { - return $acceptance->checkoutable_type == 'App\Models\Asset'; - }) ->map(function($acceptance) { return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance]; })