From 16ccb1bd4221dbb3f7218229da6879bb6d4f8762 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Feb 2026 16:08:32 -0800 Subject: [PATCH 1/8] adds action_log index and denorms first check out to assets table --- app/Http/Controllers/Api/AssetsController.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 58043f127be8..2afc58605b9f 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -154,15 +154,6 @@ public function index(FilterRequest $request, $action = null, $upcoming_status = } $assets = Asset::select('assets.*') - ->addSelect([ - 'first_checkout_at' => Actionlog::query() - ->select('created_at') - ->whereColumn('item_id', 'assets.id') - ->where('item_type', Asset::class) - ->where('action_type', 'checkout') - ->orderBy('created_at') - ->limit(1), - ]) ->with( 'model', 'location', From 2af20587feb73d8af079dfcd7d70f2ae861235af Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Feb 2026 16:19:27 -0800 Subject: [PATCH 2/8] adds migrations --- ...e_item_id_action_type_created_at_index.php | 29 +++++++++ ...fill_first_checkout_at_on_assets_table.php | 61 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 database/migrations/2026_02_04_222600_add_action_logs_item_type_item_id_action_type_created_at_index.php create mode 100644 database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php diff --git a/database/migrations/2026_02_04_222600_add_action_logs_item_type_item_id_action_type_created_at_index.php b/database/migrations/2026_02_04_222600_add_action_logs_item_type_item_id_action_type_created_at_index.php new file mode 100644 index 000000000000..4ef606464f5d --- /dev/null +++ b/database/migrations/2026_02_04_222600_add_action_logs_item_type_item_id_action_type_created_at_index.php @@ -0,0 +1,29 @@ +index(['item_type','item_id','action_type', 'created_at'], + 'action_logs_item_type_item_id_action_type_created_at_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('action_logs', function (Blueprint $table) { + $table->dropIndex('action_logs_item_type_item_id_action_type_created_at_index'); + }); + } +}; diff --git a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php new file mode 100644 index 000000000000..7a8b4a08c706 --- /dev/null +++ b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php @@ -0,0 +1,61 @@ +timestamp('first_checkout_at')->after('next_audit_date')->nullable(); + }); + $batchSize = 5000; + $minId = (int)DB::table('assets')->min('id'); + $maxId = (int)DB::table('assets')->max('id'); + $assetModel = App\Models\Asset::class; + + if (!$minId || !$maxId) { + return; + } + + for ($start = $minId; $start <= $maxId; $start += $batchSize) { + $end = $start + $batchSize - 1; + + DB::update(" + UPDATE assets a + SET a.first_checkout_at = ( + SELECT MIN(al.created_at) + FROM action_logs al + WHERE al.item_type = ? + AND al.action_type = 'checkout' + AND al.item_id = a.id + ) + WHERE a.id BETWEEN {$start} AND {$end} + AND a.first_checkout_at IS NULL + AND EXISTS( + SELECT 1 + FROM action_logs al + WHERE al.item_type = ? + AND al.action_type = 'checkout' + AND al.item_id = a.id + ) + ", [$assetModel, $assetModel]); + } + } + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table("assets", function (Blueprint $table) { + $table->dropColumn('first_checkout_at'); + }); + } +}; From 5f6ecb167ee6d8fc26d39a7c8ac829aeb71e15c8 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Feb 2026 16:23:35 -0800 Subject: [PATCH 3/8] variable rename --- ...fill_first_checkout_at_on_assets_table.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php index 7a8b4a08c706..201c3c3e057a 100644 --- a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php +++ b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php @@ -29,22 +29,22 @@ public function up(): void $end = $start + $batchSize - 1; DB::update(" - UPDATE assets a - SET a.first_checkout_at = ( - SELECT MIN(al.created_at) - FROM action_logs al - WHERE al.item_type = ? - AND al.action_type = 'checkout' - AND al.item_id = a.id + UPDATE assets asset + SET asset.first_checkout_at = ( + SELECT MIN(log.created_at) + FROM action_logs log + WHERE log.item_type = ? + AND log.action_type = 'checkout' + AND log.item_id = asset.id ) - WHERE a.id BETWEEN {$start} AND {$end} - AND a.first_checkout_at IS NULL + WHERE asset.id BETWEEN {$start} AND {$end} + AND asset.first_checkout_at IS NULL AND EXISTS( SELECT 1 - FROM action_logs al - WHERE al.item_type = ? - AND al.action_type = 'checkout' - AND al.item_id = a.id + FROM action_logs log + WHERE log.item_type = ? + AND log.action_type = 'checkout' + AND log.item_id = asset.id ) ", [$assetModel, $assetModel]); } From eb5b8aee87430741c740c4909a3b4d9c49177049 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 4 Feb 2026 16:44:32 -0800 Subject: [PATCH 4/8] reduce batchsize --- ..._02_04_222603_backfill_first_checkout_at_on_assets_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php index 201c3c3e057a..71a8cb786be4 100644 --- a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php +++ b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php @@ -16,7 +16,7 @@ public function up(): void Schema::table("assets", function (Blueprint $table) { $table->timestamp('first_checkout_at')->after('next_audit_date')->nullable(); }); - $batchSize = 5000; + $batchSize = 500; $minId = (int)DB::table('assets')->min('id'); $maxId = (int)DB::table('assets')->max('id'); $assetModel = App\Models\Asset::class; From 31578ed197d1ba03e82a7d32c89abc7dee5a8311 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Thu, 5 Feb 2026 09:48:03 -0800 Subject: [PATCH 5/8] apply first checkout in checkout controller --- app/Http/Controllers/Assets/AssetCheckoutController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Http/Controllers/Assets/AssetCheckoutController.php b/app/Http/Controllers/Assets/AssetCheckoutController.php index 4c260552578c..5e30b37b6096 100644 --- a/app/Http/Controllers/Assets/AssetCheckoutController.php +++ b/app/Http/Controllers/Assets/AssetCheckoutController.php @@ -92,6 +92,10 @@ public function store(AssetCheckoutRequest $request, $assetId) : RedirectRespons $checkout_at = $request->input('checkout_at'); } + if (empty($asset->first_checkout_at)){ + $asset->first_checkout_at = $checkout_at; + } + $expected_checkin = ''; if ($request->filled('expected_checkin')) { $expected_checkin = $request->input('expected_checkin'); @@ -102,6 +106,7 @@ public function store(AssetCheckoutRequest $request, $assetId) : RedirectRespons } + if(!empty($asset->licenseseats->all())){ if(request('checkout_to_type') == 'user') { foreach ($asset->licenseseats as $seat){ From 100430c54806bb4b9eeb216d31485e416305e9a1 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Thu, 5 Feb 2026 09:53:09 -0800 Subject: [PATCH 6/8] apply first checkout in bulk checkout controller --- app/Http/Controllers/Assets/BulkAssetsController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 9461799a03d7..20e45e306297 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -711,7 +711,9 @@ public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse if ($request->filled('status_id')) { $asset->status_id = $request->input('status_id'); } - + if (empty($asset->first_checkout_at)){ + $asset->first_checkout_at = $checkout_at; + } $checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->input('note')), $asset->name, null); //TODO - I think this logic is duplicated in the checkOut method? From 307fedef94201ec3cc58a4f5c151f399262741cb Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 18 Feb 2026 16:10:11 -0800 Subject: [PATCH 7/8] redo backfill query --- ..._add_first_checkout_at_to_assets_table.php | 28 ++++++++++++++ ...fill_first_checkout_at_on_assets_table.php | 37 ++++++------------- 2 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 database/migrations/2026_02_04_222602_add_first_checkout_at_to_assets_table.php diff --git a/database/migrations/2026_02_04_222602_add_first_checkout_at_to_assets_table.php b/database/migrations/2026_02_04_222602_add_first_checkout_at_to_assets_table.php new file mode 100644 index 000000000000..38921dbc0fa0 --- /dev/null +++ b/database/migrations/2026_02_04_222602_add_first_checkout_at_to_assets_table.php @@ -0,0 +1,28 @@ +timestamp('first_checkout_at')->after('next_audit_date')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table("assets", function (Blueprint $table) { + $table->dropColumn('first_checkout_at'); + }); + } +}; diff --git a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php index 71a8cb786be4..41747e28fc5b 100644 --- a/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php +++ b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php @@ -13,13 +13,9 @@ */ public function up(): void { - Schema::table("assets", function (Blueprint $table) { - $table->timestamp('first_checkout_at')->after('next_audit_date')->nullable(); - }); $batchSize = 500; $minId = (int)DB::table('assets')->min('id'); $maxId = (int)DB::table('assets')->max('id'); - $assetModel = App\Models\Asset::class; if (!$minId || !$maxId) { return; @@ -29,24 +25,17 @@ public function up(): void $end = $start + $batchSize - 1; DB::update(" - UPDATE assets asset - SET asset.first_checkout_at = ( - SELECT MIN(log.created_at) - FROM action_logs log - WHERE log.item_type = ? - AND log.action_type = 'checkout' - AND log.item_id = asset.id - ) - WHERE asset.id BETWEEN {$start} AND {$end} - AND asset.first_checkout_at IS NULL - AND EXISTS( - SELECT 1 - FROM action_logs log - WHERE log.item_type = ? - AND log.action_type = 'checkout' - AND log.item_id = asset.id - ) - ", [$assetModel, $assetModel]); + UPDATE assets asset + SET asset.first_checkout_at = ( + SELECT MIN(log.created_at) + FROM action_logs log + WHERE log.item_type = 'App\\\\Models\\\\Asset' + AND log.action_type = 'checkout' + AND log.item_id = asset.id + ) + WHERE asset.id BETWEEN {$start} AND {$end} + AND asset.first_checkout_at IS NULL + "); } } /** @@ -54,8 +43,6 @@ public function up(): void */ public function down(): void { - Schema::table("assets", function (Blueprint $table) { - $table->dropColumn('first_checkout_at'); - }); + DB::table('assets')->update(['first_checkout_at' => null]); } }; From 7fa0ae9fe7deb307d8d8c0e39c8787c1967ba6df Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 18 Feb 2026 16:27:03 -0800 Subject: [PATCH 8/8] add cast and attribute accessor --- app/Http/Controllers/Assets/AssetCheckoutController.php | 2 +- app/Http/Controllers/Assets/BulkAssetsController.php | 2 +- app/Models/Asset.php | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Assets/AssetCheckoutController.php b/app/Http/Controllers/Assets/AssetCheckoutController.php index 5e30b37b6096..4ba24cec7eb1 100644 --- a/app/Http/Controllers/Assets/AssetCheckoutController.php +++ b/app/Http/Controllers/Assets/AssetCheckoutController.php @@ -92,7 +92,7 @@ public function store(AssetCheckoutRequest $request, $assetId) : RedirectRespons $checkout_at = $request->input('checkout_at'); } - if (empty($asset->first_checkout_at)){ + if (is_null($asset->first_checkout_at)){ $asset->first_checkout_at = $checkout_at; } diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 20e45e306297..e25e339a3aa7 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -711,7 +711,7 @@ public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse if ($request->filled('status_id')) { $asset->status_id = $request->input('status_id'); } - if (empty($asset->first_checkout_at)){ + if (is_null($asset->first_checkout_at)){ $asset->first_checkout_at = $checkout_at; } $checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->input('note')), $asset->name, null); diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 0985e8c25f5e..3857e9810f9e 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -87,6 +87,7 @@ public function declinedCheckout(User $declinedBy, $signature) 'last_checkout' => 'datetime', 'last_checkin' => 'datetime', 'expected_checkin' => 'datetime:m-d-Y', + 'first_checkout_at' => 'datetime:m-d-Y', 'last_audit_date' => 'datetime', 'next_audit_date' => 'datetime:m-d-Y', 'model_id' => 'integer', @@ -1212,7 +1213,13 @@ protected function lastCheckin(): Attribute set: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null, ); } - + protected function firstCheckoutAt(): Attribute + { + return Attribute::make( + get: fn ($value) => $value ? Carbon::parse($value) : null, + set: fn ($value) => $value ? Carbon::parse($value) : null, + ); + } protected function assetEolDate(): Attribute { return Attribute::make(