diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 7386807a2b37..d8b41e5935e1 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', diff --git a/app/Http/Controllers/Assets/AssetCheckoutController.php b/app/Http/Controllers/Assets/AssetCheckoutController.php index 4c260552578c..4ba24cec7eb1 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 (is_null($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){ diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 9461799a03d7..e25e339a3aa7 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 (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); //TODO - I think this logic is duplicated in the checkOut method? 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( 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_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 new file mode 100644 index 000000000000..41747e28fc5b --- /dev/null +++ b/database/migrations/2026_02_04_222603_backfill_first_checkout_at_on_assets_table.php @@ -0,0 +1,48 @@ +min('id'); + $maxId = (int)DB::table('assets')->max('id'); + + if (!$minId || !$maxId) { + return; + } + + for ($start = $minId; $start <= $maxId; $start += $batchSize) { + $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 = '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 + "); + } + } + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('assets')->update(['first_checkout_at' => null]); + } +};