3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/app/Http/Controllers/Users/UserItemTransferController.php
2026-07-23 15:08:58 +01:00

263 lines
9.0 KiB
PHP

<?php
namespace App\Http\Controllers\Users;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutableCheckedOut;
use App\Http\Controllers\Controller;
use App\Http\Requests\TransferUserItemsRequest;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
class UserItemTransferController extends Controller
{
public function show(User $user): View|RedirectResponse
{
$this->authorize('view', $user);
$this->authorize('checkin', Asset::class);
$this->authorize('checkout', Asset::class);
$assets = $user->assets()
->with(['model.category', 'company'])
->whereNull('deleted_at')
->get();
$accessoryCheckouts = AccessoryCheckout::with(['accessory.category', 'accessory.company'])
->where('assigned_to', $user->id)
->where('assigned_type', User::class)
->get();
$licenseSeats = LicenseSeat::with(['license.category', 'license.company'])
->where('assigned_to', $user->id)
->whereNull('asset_id')
->get();
if ($assets->isEmpty() && $accessoryCheckouts->isEmpty() && $licenseSeats->isEmpty()) {
return redirect()->route('users.show', $user)
->with('error', trans('admin/users/general.transfer.nothing_to_transfer'));
}
return view('users.transfer', [
'sourceUser' => $user,
'assets' => $assets,
'accessoryCheckouts' => $accessoryCheckouts,
'licenseSeats' => $licenseSeats,
]);
}
public function store(TransferUserItemsRequest $request, User $user): RedirectResponse
{
$validated = $request->validated();
$target = User::findOrFail($validated['target_user_id']);
$note = $validated['note'];
$result = DB::transaction(fn () => [
'assets' => $this->transferAssets($validated['asset_ids'] ?? [], $user, $target, $note),
'accessories' => $this->transferAccessories($validated['accessory_checkout_ids'] ?? [], $user, $target, $note),
'licenses' => $this->transferLicenseSeats($validated['license_seat_ids'] ?? [], $user, $target, $note),
]);
$flash = trans('admin/users/general.transfer.success', [
'assets' => $result['assets']['count'],
'accessories' => $result['accessories']['count'],
'licenses' => $result['licenses']['count'],
'target' => $target->display_name,
]);
$redirect = redirect()->route('users.show', $target)->with('success', $flash);
$skipped = array_merge(
$result['assets']['skipped'],
$result['accessories']['skipped'],
$result['licenses']['skipped'],
);
if (! empty($skipped)) {
$redirect->with('warning', trans('admin/users/general.transfer.some_skipped', [
'count' => count($skipped),
]));
}
return $redirect;
}
/**
* @return array{count: int, skipped: array<int, string>}
*/
private function transferAssets(array $ids, User $source, User $target, ?string $note): array
{
$count = 0;
$skipped = [];
foreach ($ids as $assetId) {
$asset = Asset::find($assetId);
if (! $this->assetBelongsToSource($asset, $source) || ! $asset->canCheckoutTo($target)) {
$skipped[] = 'asset:'.$assetId;
continue;
}
$this->checkInAsset($asset, $source, $note);
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), null, $note);
$count++;
}
return ['count' => $count, 'skipped' => $skipped];
}
/**
* @return array{count: int, skipped: array<int, string>}
*/
private function transferAccessories(array $ids, User $source, User $target, ?string $note): array
{
$count = 0;
$skipped = [];
foreach ($ids as $checkoutId) {
$checkout = AccessoryCheckout::with('accessory')->find($checkoutId);
$accessory = $checkout?->accessory;
if (! $this->accessoryCheckoutBelongsToSource($checkout, $source) || ! $accessory?->canCheckoutTo($target)) {
$skipped[] = 'accessory:'.$checkoutId;
continue;
}
$this->checkInAccessory($checkout, $accessory, $note);
$this->checkOutAccessory($accessory, $target, $note);
$count++;
}
return ['count' => $count, 'skipped' => $skipped];
}
/**
* Non-reassignable licenses stay with the original assignee. Transferring
* one would defeat the whole point of the flag, so we drop it into the
* skipped bucket and surface a warning.
*
* @return array{count: int, skipped: array<int, string>}
*/
private function transferLicenseSeats(array $ids, User $source, User $target, ?string $note): array
{
$count = 0;
$skipped = [];
foreach ($ids as $seatId) {
$seat = LicenseSeat::with('license')->find($seatId);
$license = $seat?->license;
if (! $this->licenseSeatBelongsToSource($seat, $source) || ! $license?->reassignable || ! $license->canCheckoutTo($target)) {
$skipped[] = 'license:'.$seatId;
continue;
}
$this->transferLicenseSeat($seat, $source, $target, $note);
$count++;
}
return ['count' => $count, 'skipped' => $skipped];
}
private function assetBelongsToSource(?Asset $asset, User $source): bool
{
return $asset !== null
&& (int) $asset->assigned_to === (int) $source->id
&& $asset->assigned_type === User::class;
}
private function accessoryCheckoutBelongsToSource(?AccessoryCheckout $checkout, User $source): bool
{
return $checkout !== null
&& (int) $checkout->assigned_to === (int) $source->id
&& $checkout->assigned_type === User::class;
}
private function licenseSeatBelongsToSource(?LicenseSeat $seat, User $source): bool
{
return $seat !== null
&& (int) $seat->assigned_to === (int) $source->id
&& $seat->asset_id === null;
}
private function checkInAsset(Asset $asset, User $source, ?string $note): void
{
$originalValues = $asset->getRawOriginal();
$checkinAt = date('Y-m-d H:i:s');
$asset->expected_checkin = null;
$asset->last_checkin = $checkinAt;
$asset->accepted = null;
$asset->assignedTo()->dissociate();
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
});
CheckoutAcceptance::pending()
->where('checkoutable_type', Asset::class)
->where('checkoutable_id', $asset->id)
->get()
->each(fn ($a) => $a->delete());
$asset->save();
event(new CheckoutableCheckedIn($asset, $source, auth()->user(), $note, $checkinAt, $originalValues));
}
private function checkInAccessory(AccessoryCheckout $checkout, Accessory $accessory, ?string $note): void
{
$source = $checkout->assignedTo;
CheckoutAcceptance::pending()
->where('checkoutable_type', Accessory::class)
->where('checkoutable_id', $accessory->id)
->where('assigned_to_id', $checkout->assigned_to)
->get()
->each(fn ($a) => $a->delete());
$checkout->delete();
event(new CheckoutableCheckedIn($accessory, $source, auth()->user(), $note, date('Y-m-d H:i:s')));
}
private function checkOutAccessory(Accessory $accessory, User $target, ?string $note): void
{
$newCheckout = new AccessoryCheckout([
'accessory_id' => $accessory->id,
'assigned_to' => $target->id,
'assigned_type' => User::class,
'note' => $note,
]);
$newCheckout->created_by = auth()->id();
$newCheckout->save();
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $note, [], 1, false));
}
private function transferLicenseSeat(LicenseSeat $seat, User $source, User $target, ?string $note): void
{
CheckoutAcceptance::pending()
->where('checkoutable_type', License::class)
->where('checkoutable_id', $seat->license_id)
->where('assigned_to_id', $source->id)
->get()
->each(fn ($a) => $a->delete());
$seat->assigned_to = null;
$seat->save();
event(new CheckoutableCheckedIn($seat, $source, auth()->user(), $note));
$seat->assigned_to = $target->id;
$seat->save();
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1, false));
}
}