diff --git a/app/Events/CheckoutableCheckedIn.php b/app/Events/CheckoutableCheckedIn.php index f6a20fd3d265..4a1d46d2f479 100644 --- a/app/Events/CheckoutableCheckedIn.php +++ b/app/Events/CheckoutableCheckedIn.php @@ -22,12 +22,14 @@ class CheckoutableCheckedIn public $originalValues; + public ?string $filename; + /** * Create a new event instance. * * @return void */ - public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null, $originalValues = []) + public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null, $originalValues = [], ?string $filename = null) { $this->checkoutable = $checkoutable; $this->checkedOutTo = $checkedOutTo; @@ -35,5 +37,6 @@ public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $no $this->note = $note; $this->action_date = $action_date ?? date('Y-m-d H:i:s'); $this->originalValues = $originalValues; + $this->filename = $filename; } } diff --git a/app/Events/CheckoutableCheckedOut.php b/app/Events/CheckoutableCheckedOut.php index 0d47f64552d6..958a519aa63f 100644 --- a/app/Events/CheckoutableCheckedOut.php +++ b/app/Events/CheckoutableCheckedOut.php @@ -24,12 +24,14 @@ class CheckoutableCheckedOut public bool $signInPlace; + public ?string $filename; + /** * Create a new event instance. * * @return void */ - public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [], $quantity = 1, bool $signInPlace = false) + public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [], $quantity = 1, bool $signInPlace = false, ?string $filename = null) { $this->checkoutable = $checkoutable; $this->checkedOutTo = $checkedOutTo; @@ -38,5 +40,6 @@ public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $n $this->originalValues = $originalValues; $this->quantity = $quantity; $this->signInPlace = $signInPlace; + $this->filename = $filename; } } diff --git a/app/Http/Controllers/Accessories/AccessoryCheckinController.php b/app/Http/Controllers/Accessories/AccessoryCheckinController.php index 5a7dc55bcf9a..3856c659c031 100644 --- a/app/Http/Controllers/Accessories/AccessoryCheckinController.php +++ b/app/Http/Controllers/Accessories/AccessoryCheckinController.php @@ -5,11 +5,11 @@ use App\Events\CheckoutableCheckedIn; use App\Helpers\Helper; use App\Http\Controllers\Controller; +use App\Http\Requests\UploadFileRequest; use App\Models\Accessory; use App\Models\AccessoryCheckout; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class AccessoryCheckinController extends Controller @@ -53,7 +53,7 @@ public function create($accessoryUserId = null, $backto = null): View|RedirectRe * @param null $accessoryCheckoutId * @param string $backto */ - public function store(Request $request, $accessoryCheckoutId = null, $backto = null): RedirectResponse + public function store(UploadFileRequest $request, $accessoryCheckoutId = null, $backto = null): RedirectResponse { if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryCheckoutId))) { return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist')); @@ -75,9 +75,14 @@ public function store(Request $request, $accessoryCheckoutId = null, $backto = n $checkin_at = $request->input('checkin_at').' '.$checkin_hours; } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/accessories/', 'checkin-'.$accessory->id, $request->file('file')); + } + // Was the accessory updated? if ($accessory_checkout->delete()) { - event(new CheckoutableCheckedIn($accessory, $accessory_checkout->assignedTo, auth()->user(), $request->input('note'), $checkin_at)); + event(new CheckoutableCheckedIn($accessory, $accessory_checkout->assignedTo, auth()->user(), $request->input('note'), $checkin_at, [], $file_name)); session()->put(['redirect_option' => $request->input('redirect_option')]); diff --git a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php index f1e3502f1185..2c220020f0af 100644 --- a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php +++ b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php @@ -96,6 +96,11 @@ public function store(AccessoryCheckoutRequest $request, Accessory $accessory): $accessory_checkout->save(); } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/accessories/', 'checkout-'.$accessory->id, $request->file('file')); + } + event(new CheckoutableCheckedOut( $accessory, $target, @@ -104,6 +109,7 @@ public function store(AccessoryCheckoutRequest $request, Accessory $accessory): [], $accessory->checkout_qty, $request->boolean('sign_in_place'), + $file_name, )); $request->request->add(['checkout_to_type' => request('checkout_to_type')]); diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php index 5f9998f596bd..f36b868e6a32 100644 --- a/app/Http/Controllers/Api/AccessoriesController.php +++ b/app/Http/Controllers/Api/AccessoriesController.php @@ -8,6 +8,7 @@ use App\Http\Requests\AccessoryCheckoutRequest; use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\StoreAccessoryRequest; +use App\Http\Requests\UploadFileRequest; use App\Http\Traits\CheckInOutTrait; use App\Http\Transformers\AccessoriesTransformer; use App\Http\Transformers\ActionlogsTransformer; @@ -22,6 +23,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Gate; class AccessoriesController extends Controller { @@ -314,8 +316,13 @@ public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory $accessory->checkout_qty = $request->input('checkout_qty', 1); $payload = null; + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/accessories/', 'checkout-'.$accessory->id, $request->file('file')); + } + // Keep checkout rows and checkout log/event atomic to avoid ghost assignments. - DB::transaction(function () use ($accessory, $request, $target, &$payload): void { + DB::transaction(function () use ($accessory, $request, $target, $file_name, &$payload): void { for ($i = 0; $i < $accessory->checkout_qty; $i++) { $accessory_checkout = new AccessoryCheckout([ @@ -347,6 +354,8 @@ public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory $request->input('note'), [], $accessory->checkout_qty, + false, + $file_name, )); }); @@ -367,7 +376,7 @@ public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory * * @internal param int $accessoryId */ - public function checkin(Request $request, $accessoryUserId = null) + public function checkin(UploadFileRequest $request, $accessoryUserId = null) { if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) { return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId]))); @@ -376,7 +385,16 @@ public function checkin(Request $request, $accessoryUserId = null) $accessory = Accessory::find($accessory_checkout->accessory_id); $this->authorize('checkin', $accessory); - $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note')); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/accessories/', 'checkin-'.$accessory->id, $request->file('file')); + } + + $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note'), null, []); + + if ($file_name && Gate::allows('files', $accessory)) { + $accessory->logUpload($file_name, null); + } // Was the accessory updated? if ($accessory_checkout->delete()) { diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index b2b1a4042cfb..49fc96fcf39c 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -10,6 +10,7 @@ use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\StoreAssetRequest; use App\Http\Requests\UpdateAssetRequest; +use App\Http\Requests\UploadFileRequest; use App\Http\Traits\MigratesLegacyAssetLocations; use App\Http\Transformers\ActionlogsTransformer; use App\Http\Transformers\AssetsTransformer; @@ -1085,9 +1086,14 @@ public function checkout(AssetCheckoutRequest $request, $asset_id): JsonResponse // $asset->location_id = $target->rtd_location_id; // } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/assets/', 'checkout-'.$asset->id, $request->file('file')); + } + // Keep checkout mutation + checkout logging/event side effects atomic. - $wasCheckedOut = DB::transaction(function () use ($asset, $target, $checkout_at, $expected_checkin, $note, $asset_name): bool { - return $asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id); + $wasCheckedOut = DB::transaction(function () use ($asset, $target, $checkout_at, $expected_checkin, $note, $asset_name, $file_name): bool { + return $asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id, false, $file_name); }); if ($wasCheckedOut) { @@ -1106,7 +1112,7 @@ public function checkout(AssetCheckoutRequest $request, $asset_id): JsonResponse * * @since [v4.0] */ - public function checkin(Request $request, $asset_id): JsonResponse + public function checkin(UploadFileRequest $request, $asset_id): JsonResponse { $asset = Asset::with('model')->findOrFail($asset_id); $this->authorize('checkin', $asset); @@ -1173,6 +1179,11 @@ function (Builder $query) use ($asset) { $acceptance->delete(); }); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/assets/', 'checkin-'.$asset->id, $request->file('file')); + } + if ($asset->save()) { // Update the location of any child assets @@ -1180,7 +1191,7 @@ function (Builder $query) use ($asset) { ->where('assigned_to', $asset->id) ->update(['location_id' => $asset->location_id]); - event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues)); + event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues, $file_name)); return response()->json(Helper::formatStandardApiResponse('success', [ 'asset_tag' => e($asset->asset_tag), @@ -1199,7 +1210,7 @@ function (Builder $query) use ($asset) { * * @since [v6.0] */ - public function checkinByTag(Request $request, $tag = null): JsonResponse + public function checkinByTag(UploadFileRequest $request, $tag = null): JsonResponse { $this->authorize('checkin', Asset::class); if ($tag == null && null !== ($request->input('asset_tag'))) { diff --git a/app/Http/Controllers/Api/ComponentsController.php b/app/Http/Controllers/Api/ComponentsController.php index bf754ba1a55d..ac527b3b5645 100644 --- a/app/Http/Controllers/Api/ComponentsController.php +++ b/app/Http/Controllers/Api/ComponentsController.php @@ -6,6 +6,7 @@ use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Http\Requests\ImageUploadRequest; +use App\Http\Requests\UploadFileRequest; use App\Http\Transformers\ActionlogsTransformer; use App\Http\Transformers\ComponentsTransformer; use App\Models\Asset; @@ -17,6 +18,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Validator; @@ -290,7 +292,7 @@ public function getAssets(Component $component, Request $request): array * * @param int $componentId */ - public function checkout(Request $request, $componentId): JsonResponse + public function checkout(UploadFileRequest $request, $componentId): JsonResponse { // Check if the component exists if (! $component = Component::find($componentId)) { @@ -327,6 +329,11 @@ public function checkout(Request $request, $componentId): JsonResponse return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company'))); } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/components/', 'checkout-'.$component->id, $request->file('file')); + } + // Keep pivot + action log in one transaction so checkout is all-or-nothing. DB::transaction(function () use ($component, $request, $asset): void { $component->assigned_to = $request->input('assigned_to'); @@ -343,6 +350,10 @@ public function checkout(Request $request, $componentId): JsonResponse $component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1)); }); + if ($file_name && Gate::allows('files', $component)) { + $component->logUpload($file_name, null); + } + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success'))); } @@ -356,7 +367,7 @@ public function checkout(Request $request, $componentId): JsonResponse * * @since [v5.1.8] */ - public function checkin(Request $request, $component_asset_id): JsonResponse + public function checkin(UploadFileRequest $request, $component_asset_id): JsonResponse { if ($component_assets = DB::table('components_assets')->find($component_asset_id)) { if (is_null($component = Component::find($component_assets->component_id))) { @@ -394,7 +405,12 @@ public function checkin(Request $request, $component_asset_id): JsonResponse $asset = Asset::find($component_assets->asset_id); - event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->input('note'), Carbon::now())); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/components/', 'checkin-'.$component->id, $request->file('file')); + } + + event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->input('note'), Carbon::now(), [], $file_name)); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkin.success'))); } diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index 83758d47b930..44d33d8a41d2 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -8,6 +8,7 @@ use App\Http\Requests\FilterRequest; use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\StoreConsumableRequest; +use App\Http\Requests\UploadFileRequest; use App\Http\Transformers\ActionlogsTransformer; use App\Http\Transformers\ConsumablesTransformer; use App\Http\Transformers\SelectlistTransformer; @@ -282,7 +283,7 @@ public function getDataView($consumableId): array * * @since [v4.9.5] */ - public function checkout(Request $request, $id): JsonResponse + public function checkout(UploadFileRequest $request, $id): JsonResponse { // Check if the consumable exists if (! $consumable = Consumable::with('users')->find($id)) { @@ -322,8 +323,13 @@ public function checkout(Request $request, $id): JsonResponse // Update the consumable data $consumable->assigned_to = $request->input('assigned_to'); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/consumables/', 'checkout-'.$consumable->id, $request->file('file')); + } + // Keep pivot writes and checkout log/event atomic to avoid partial checkout state. - DB::transaction(function () use ($consumable, $request, $user): void { + DB::transaction(function () use ($consumable, $request, $user, $file_name): void { for ($i = 0; $i < $consumable->checkout_qty; $i++) { $consumable->users()->attach($consumable->id, [ @@ -342,6 +348,8 @@ public function checkout(Request $request, $id): JsonResponse $request->input('note'), [], $consumable->checkout_qty, + false, + $file_name, )); }); diff --git a/app/Http/Controllers/Api/LicensesController.php b/app/Http/Controllers/Api/LicensesController.php index 458f900dd159..a551faa97307 100644 --- a/app/Http/Controllers/Api/LicensesController.php +++ b/app/Http/Controllers/Api/LicensesController.php @@ -7,6 +7,7 @@ use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Http\Requests\FilterRequest; +use App\Http\Requests\UploadFileRequest; use App\Http\Transformers\ActionlogsTransformer; use App\Http\Transformers\LicenseSeatsTransformer; use App\Http\Transformers\LicensesTransformer; @@ -278,7 +279,7 @@ public function destroy($id): JsonResponse * * @param int $licenseId */ - public function checkout(Request $request, $licenseId): JsonResponse + public function checkout(UploadFileRequest $request, $licenseId): JsonResponse { $license = License::findOrFail($licenseId); $this->authorize('checkout', $license); @@ -295,11 +296,16 @@ public function checkout(Request $request, $licenseId): JsonResponse return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.license_is_inactive'))); } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/licenses/', 'checkout-'.$license->id, $request->file('file')); + } + $errorResponse = null; $updatedSeat = null; $target = null; - DB::transaction(function () use ($license, $validated, &$errorResponse, &$updatedSeat, &$target): void { + DB::transaction(function () use ($license, $validated, $file_name, &$errorResponse, &$updatedSeat, &$target): void { $seatId = $validated['seat_id'] ?? null; $licenseSeat = $seatId @@ -365,7 +371,7 @@ public function checkout(Request $request, $licenseId): JsonResponse return; } - event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), $validated['notes'] ?? null)); + event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), $validated['notes'] ?? null, [], 1, false, $file_name)); $updatedSeat = $licenseSeat->load('license', 'user', 'asset'); }); @@ -387,7 +393,7 @@ public function checkout(Request $request, $licenseId): JsonResponse * * @param int $licenseId */ - public function checkin(Request $request, $licenseId): JsonResponse + public function checkin(UploadFileRequest $request, $licenseId): JsonResponse { $license = License::findOrFail($licenseId); $this->authorize('checkin', $license); @@ -423,7 +429,12 @@ public function checkin(Request $request, $licenseId): JsonResponse return response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors())); } - event(new CheckoutableCheckedIn($licenseSeat, $target, auth()->user(), $licenseSeat->notes)); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/licenses/', 'checkin-'.$license->id, $request->file('file')); + } + + event(new CheckoutableCheckedIn($licenseSeat, $target, auth()->user(), $licenseSeat->notes, null, [], $file_name)); return response()->json(Helper::formatStandardApiResponse('success', (new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat->load('license', 'user', 'asset')), trans('admin/licenses/message.checkin.success'))); } diff --git a/app/Http/Controllers/Assets/AssetCheckinController.php b/app/Http/Controllers/Assets/AssetCheckinController.php index fb3b81eacb16..a197b7e8bb29 100644 --- a/app/Http/Controllers/Assets/AssetCheckinController.php +++ b/app/Http/Controllers/Assets/AssetCheckinController.php @@ -178,13 +178,18 @@ function (Builder $query) use ($asset) { // Add any custom fields that should be included in the checkout $asset->customFieldsForCheckinCheckout('display_checkin'); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/assets/', 'checkin-'.$asset->id, $request->file('file')); + } + if ($asset->save()) { // Update the location of any child assets Asset::where('assigned_type', Asset::class) ->where('assigned_to', $asset->id) ->update(['location_id' => $asset->location_id]); - event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues)); + event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues, $file_name)); return Helper::getRedirectOption($request, $asset->id, 'Assets') ->with('success', trans('admin/hardware/message.checkin.success')); diff --git a/app/Http/Controllers/Assets/AssetCheckoutController.php b/app/Http/Controllers/Assets/AssetCheckoutController.php index 0c8e0d7f2ee3..e0ae289ea91b 100644 --- a/app/Http/Controllers/Assets/AssetCheckoutController.php +++ b/app/Http/Controllers/Assets/AssetCheckoutController.php @@ -138,7 +138,12 @@ public function store(AssetCheckoutRequest $request, $assetId): RedirectResponse 'sign_in_place' => $request->boolean('sign_in_place'), ]); - if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->input('note'), $request->input('name'), null, $request->boolean('sign_in_place'))) { + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/assets/', 'checkout-'.$asset->id, $request->file('file')); + } + + if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->input('note'), $request->input('name'), null, $request->boolean('sign_in_place'), $file_name)) { // When sign_in_place is requested and the target is a user, redirect to the // acceptance/signature page so the user can sign in person. The signature is diff --git a/app/Http/Controllers/Components/ComponentCheckinController.php b/app/Http/Controllers/Components/ComponentCheckinController.php index 4980b4e997ec..a506e2b9b4de 100644 --- a/app/Http/Controllers/Components/ComponentCheckinController.php +++ b/app/Http/Controllers/Components/ComponentCheckinController.php @@ -5,12 +5,12 @@ use App\Events\CheckoutableCheckedIn; use App\Helpers\Helper; use App\Http\Controllers\Controller; +use App\Http\Requests\UploadFileRequest; use App\Models\Asset; use App\Models\Component; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; @@ -62,7 +62,7 @@ public function create($component_asset_id) * * @throws AuthorizationException */ - public function store(Request $request, $component_asset_id, $backto = null) + public function store(UploadFileRequest $request, $component_asset_id, $backto = null) { if ($component_assets = DB::table('components_assets')->find($component_asset_id)) { if (is_null($component = Component::find($component_assets->component_id))) { @@ -101,7 +101,12 @@ public function store(Request $request, $component_asset_id, $backto = null) $asset = Asset::find($component_assets->asset_id); - event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->input('note'), Carbon::now())); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/components/', 'checkin-'.$component->id, $request->file('file')); + } + + event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->input('note'), Carbon::now(), [], $file_name)); session()->put(['redirect_option' => $request->input('redirect_option')]); diff --git a/app/Http/Controllers/Components/ComponentCheckoutController.php b/app/Http/Controllers/Components/ComponentCheckoutController.php index 90c0bc309be8..945f91037c60 100644 --- a/app/Http/Controllers/Components/ComponentCheckoutController.php +++ b/app/Http/Controllers/Components/ComponentCheckoutController.php @@ -5,12 +5,12 @@ use App\Events\CheckoutableCheckedOut; use App\Helpers\Helper; use App\Http\Controllers\Controller; +use App\Http\Requests\UploadFileRequest; use App\Models\Asset; use App\Models\Component; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; class ComponentCheckoutController extends Controller @@ -72,7 +72,7 @@ public function create($id) * * @throws AuthorizationException */ - public function store(Request $request, $componentId) + public function store(UploadFileRequest $request, $componentId) { // Check if the component exists if (! $component = Component::find($componentId)) { @@ -124,6 +124,11 @@ public function store(Request $request, $componentId) 'note' => $request->input('note'), ]); + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/components/', 'checkout-'.$component->id, $request->file('file')); + } + event(new CheckoutableCheckedOut( $component, $asset, @@ -131,6 +136,8 @@ public function store(Request $request, $componentId) $request->input('note'), [], $component->checkout_qty, + false, + $file_name, )); $request->request->add(['checkout_to_type' => 'asset']); diff --git a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php index 018655fe7c20..2550c068a0c4 100644 --- a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php +++ b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php @@ -5,13 +5,13 @@ use App\Events\CheckoutableCheckedOut; use App\Helpers\Helper; use App\Http\Controllers\Controller; +use App\Http\Requests\UploadFileRequest; use App\Models\CheckoutAcceptance; use App\Models\Consumable; use App\Models\User; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; class ConsumableCheckoutController extends Controller { @@ -68,7 +68,7 @@ public function create($id): View|RedirectResponse * * @throws AuthorizationException */ - public function store(Request $request, $consumableId) + public function store(UploadFileRequest $request, $consumableId) { if (is_null($consumable = Consumable::with('users')->find($consumableId))) { return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.not_found')); @@ -118,6 +118,11 @@ public function store(Request $request, $consumableId) $consumable->checkout_qty = $quantity; + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/consumables/', 'checkout-'.$consumable->id, $request->file('file')); + } + event(new CheckoutableCheckedOut( $consumable, $user, @@ -126,6 +131,7 @@ public function store(Request $request, $consumableId) [], $consumable->checkout_qty, $request->boolean('sign_in_place'), + $file_name, )); $request->request->add(['checkout_to_type' => 'user']); diff --git a/app/Http/Controllers/Licenses/LicenseCheckinController.php b/app/Http/Controllers/Licenses/LicenseCheckinController.php index 6960caadbf61..87bb6e7ea20e 100644 --- a/app/Http/Controllers/Licenses/LicenseCheckinController.php +++ b/app/Http/Controllers/Licenses/LicenseCheckinController.php @@ -5,6 +5,7 @@ use App\Events\CheckoutableCheckedIn; use App\Helpers\Helper; use App\Http\Controllers\Controller; +use App\Http\Requests\UploadFileRequest; use App\Models\Asset; use App\Models\License; use App\Models\LicenseSeat; @@ -55,7 +56,7 @@ public function create(LicenseSeat $licenseSeat, $backTo = null) * * @throws AuthorizationException */ - public function store(Request $request, $seatId = null, $backTo = null) + public function store(UploadFileRequest $request, $seatId = null, $backTo = null) { // Check if the asset exists if (is_null($licenseSeat = LicenseSeat::find($seatId))) { @@ -108,9 +109,14 @@ public function store(Request $request, $seatId = null, $backTo = null) session()->put(['checkout_to_type' => 'user']); } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/licenses/', 'checkin-'.$licenseSeat->license_id, $request->file('file')); + } + // Was the asset updated? if ($licenseSeat->save()) { - event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes)); + event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes, null, [], $file_name)); return Helper::getRedirectOption($request, $license->id, 'Licenses') ->with('success', trans('admin/licenses/message.checkin.success')); diff --git a/app/Http/Controllers/Licenses/LicenseCheckoutController.php b/app/Http/Controllers/Licenses/LicenseCheckoutController.php index 54bcef42f8d1..53c6a5bdb3c0 100644 --- a/app/Http/Controllers/Licenses/LicenseCheckoutController.php +++ b/app/Http/Controllers/Licenses/LicenseCheckoutController.php @@ -118,18 +118,23 @@ public function store(LicenseCheckoutRequest $request, $licenseId, $seatId = nul } } + $file_name = null; + if ($request->hasFile('file')) { + $file_name = $request->handleFile('private_uploads/licenses/', 'checkout-'.$license->id, $request->file('file')); + } + $licenseSeat = null; $checkoutTarget = null; - DB::transaction(function () use ($request, $license, $seatId, &$licenseSeat, &$checkoutTarget): void { + DB::transaction(function () use ($request, $license, $seatId, $file_name, &$licenseSeat, &$checkoutTarget): void { $licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId, lock: true); $licenseSeat->created_by = auth()->id(); $licenseSeat->notes = $request->input('notes'); if ($request->filled('asset_id')) { - $checkoutTarget = $this->checkoutToAsset($licenseSeat); + $checkoutTarget = $this->checkoutToAsset($licenseSeat, $file_name); } elseif ($request->filled('assigned_to')) { - $checkoutTarget = $this->checkoutToUser($licenseSeat); + $checkoutTarget = $this->checkoutToUser($licenseSeat, $file_name); } }); @@ -209,7 +214,7 @@ protected function findLicenseSeatToCheckout($license, $seatId, bool $lock = fal return $licenseSeat; } - protected function checkoutToAsset($licenseSeat) + protected function checkoutToAsset($licenseSeat, ?string $file_name = null) { if (is_null($target = Asset::find(request('asset_id')))) { return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.asset_does_not_exist')); @@ -221,7 +226,7 @@ protected function checkoutToAsset($licenseSeat) $licenseSeat->assigned_to = $target->assigned_to; } if ($licenseSeat->save()) { - event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'), [], 1, request()->boolean('sign_in_place'))); + event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'), [], 1, request()->boolean('sign_in_place'), $file_name)); return $target; } @@ -229,7 +234,7 @@ protected function checkoutToAsset($licenseSeat) return false; } - protected function checkoutToUser($licenseSeat) + protected function checkoutToUser($licenseSeat, ?string $file_name = null) { // Fetch the target and set the license user if (is_null($target = User::find(request('assigned_to')))) { @@ -238,7 +243,7 @@ protected function checkoutToUser($licenseSeat) $licenseSeat->assigned_to = request('assigned_to'); if ($licenseSeat->save()) { - event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'), [], 1, request()->boolean('sign_in_place'))); + event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), request('notes'), [], 1, request()->boolean('sign_in_place'), $file_name)); return $target; } diff --git a/app/Http/Requests/AssetCheckinRequest.php b/app/Http/Requests/AssetCheckinRequest.php index dc5179f49c5b..0cbe96eacfc6 100644 --- a/app/Http/Requests/AssetCheckinRequest.php +++ b/app/Http/Requests/AssetCheckinRequest.php @@ -4,7 +4,7 @@ use App\Models\Setting; -class AssetCheckinRequest extends Request +class AssetCheckinRequest extends UploadFileRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Http/Requests/AssetCheckoutRequest.php b/app/Http/Requests/AssetCheckoutRequest.php index 35e972d1c6f4..7f9e8e2d3ce6 100644 --- a/app/Http/Requests/AssetCheckoutRequest.php +++ b/app/Http/Requests/AssetCheckoutRequest.php @@ -4,7 +4,7 @@ use App\Models\Setting; -class AssetCheckoutRequest extends Request +class AssetCheckoutRequest extends UploadFileRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Http/Requests/ImageUploadRequest.php b/app/Http/Requests/ImageUploadRequest.php index 8c30c486f134..9e5289d84dc7 100644 --- a/app/Http/Requests/ImageUploadRequest.php +++ b/app/Http/Requests/ImageUploadRequest.php @@ -13,7 +13,7 @@ use Intervention\Image\Exception\NotReadableException; use Intervention\Image\Facades\Image; -class ImageUploadRequest extends Request +class ImageUploadRequest extends UploadFileRequest { use ConvertsBase64ToFiles; diff --git a/app/Http/Requests/LicenseCheckoutRequest.php b/app/Http/Requests/LicenseCheckoutRequest.php index ad14e00e1c93..daafefbbe2b7 100644 --- a/app/Http/Requests/LicenseCheckoutRequest.php +++ b/app/Http/Requests/LicenseCheckoutRequest.php @@ -2,9 +2,7 @@ namespace App\Http\Requests; -use Illuminate\Foundation\Http\FormRequest; - -class LicenseCheckoutRequest extends FormRequest +class LicenseCheckoutRequest extends UploadFileRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Listeners/LogListener.php b/app/Listeners/LogListener.php index c2de69d88827..b90b45c2ec93 100644 --- a/app/Listeners/LogListener.php +++ b/app/Listeners/LogListener.php @@ -10,6 +10,7 @@ use App\Models\Actionlog; use App\Models\LicenseSeat; use App\Models\User; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Log; class LogListener @@ -23,6 +24,13 @@ class LogListener public function onCheckoutableCheckedIn(CheckoutableCheckedIn $event) { $event->checkoutable->logCheckin($event->checkedOutTo, $event->note, $event->action_date, $event->originalValues); + + if ($event->filename) { + $item = $event->checkoutable instanceof LicenseSeat ? $event->checkoutable->license : $event->checkoutable; + if (Gate::allows('files', $item)) { + $event->checkoutable->logUpload($event->filename, null); + } + } } /** @@ -40,6 +48,13 @@ public function onCheckoutableCheckedOut(CheckoutableCheckedOut $event) $event->originalValues, $event->quantity ); + + if ($event->filename) { + $item = $event->checkoutable instanceof LicenseSeat ? $event->checkoutable->license : $event->checkoutable; + if (Gate::allows('files', $item)) { + $event->checkoutable->logUpload($event->filename, null); + } + } } /** diff --git a/app/Models/Asset.php b/app/Models/Asset.php index d3b1991a5a3a..1cb9eafb44dc 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -520,7 +520,7 @@ public function availableForCheckIn() * * @return bool */ - public function checkOut($target, $admin = null, $checkout_at = null, $expected_checkin = null, $note = null, $name = null, $location = null, bool $signInPlace = false) + public function checkOut($target, $admin = null, $checkout_at = null, $expected_checkin = null, $note = null, $name = null, $location = null, bool $signInPlace = false, ?string $filename = null) { if (! $target) { return false; @@ -564,7 +564,7 @@ public function checkOut($target, $admin = null, $checkout_at = null, $expected_ } else { $checkedOutBy = auth()->user(); } - event(new CheckoutableCheckedOut($this, $target, $checkedOutBy, $note, $originalValues, 1, $signInPlace)); + event(new CheckoutableCheckedOut($this, $target, $checkedOutBy, $note, $originalValues, 1, $signInPlace, $filename)); $this->increment('checkout_counter', 1); diff --git a/app/Models/Traits/HasUploads.php b/app/Models/Traits/HasUploads.php index 3af2a94c9c3c..e81e765fd13c 100644 --- a/app/Models/Traits/HasUploads.php +++ b/app/Models/Traits/HasUploads.php @@ -15,7 +15,7 @@ public function uploads() return $this->hasMany(Actionlog::class, 'item_id') ->withoutGlobalScope(CompanyableScope::class) ->where('item_type', self::class) - ->where('action_type', '=', 'uploaded') + ->whereIn('action_type', ['uploaded', 'checkout', 'checkin', 'audit']) ->whereNotNull('filename') ->whereNotIn('filename', function ($query) { $query->select('filename') diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 6adaa776ff78..2cac4cae8044 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -475,6 +475,31 @@ public function manageSupplierFiles() return $this->appendPermission(['suppliers.files' => '1']); } + public function assetsFiles() + { + return $this->appendPermission(['assets.files' => '1']); + } + + public function accessoriesFiles() + { + return $this->appendPermission(['accessories.files' => '1']); + } + + public function licensesFiles() + { + return $this->appendPermission(['licenses.files' => '1']); + } + + public function componentsFiles() + { + return $this->appendPermission(['components.files' => '1']); + } + + public function consumablesFiles() + { + return $this->appendPermission(['consumables.files' => '1']); + } + private function appendPermission(array $permission) { return $this->state(function ($currentState) use ($permission) { diff --git a/resources/views/accessories/checkin.blade.php b/resources/views/accessories/checkin.blade.php index e467aa5f7281..2665d6032e17 100755 --- a/resources/views/accessories/checkin.blade.php +++ b/resources/views/accessories/checkin.blade.php @@ -30,6 +30,8 @@ type="textarea" /> + +
diff --git a/resources/views/accessories/checkout.blade.php b/resources/views/accessories/checkout.blade.php index 3aa81e9e6b35..1195ccf30e9a 100755 --- a/resources/views/accessories/checkout.blade.php +++ b/resources/views/accessories/checkout.blade.php @@ -89,6 +89,8 @@ type="textarea" /> + + 'file', + 'label' => null, + 'accept' => null, + 'help_text' => null, + 'multiple' => false, + 'required' => false, +]) + +
$errors->has($name)])> + +
+ + + + + + +

+ {{ $help_text ?? trans('general.upload_filetypes_help', ['allowed_filetypes' => config('filesystems.allowed_upload_extensions'), 'size' => Helper::file_upload_max_size_readable()]) }} +

+ + {!! $errors->first($name, '') !!} +
+
diff --git a/resources/views/components/checkin.blade.php b/resources/views/components/checkin.blade.php index 4fdcf3a1fa76..c8d50299e561 100644 --- a/resources/views/components/checkin.blade.php +++ b/resources/views/components/checkin.blade.php @@ -37,6 +37,8 @@ type="textarea" /> + + + + + + + autocomplete="off" enctype="multipart/form-data"> @else
+ autocomplete="off" enctype="multipart/form-data"> @endif {{csrf_field()}} @@ -194,6 +194,8 @@ class="form-group"
+ + @include("models/custom_fields_form", [ 'model' => $asset->model, diff --git a/resources/views/hardware/checkout.blade.php b/resources/views/hardware/checkout.blade.php index 5d96a7b31245..7f3059157c0a 100755 --- a/resources/views/hardware/checkout.blade.php +++ b/resources/views/hardware/checkout.blade.php @@ -20,7 +20,7 @@
- +

{{ trans('admin/hardware/form.tag') }} {{ $asset->asset_tag }}

@@ -176,6 +176,8 @@
+ + @include("models/custom_fields_form", [ 'model' => $asset->model, diff --git a/resources/views/licenses/checkin.blade.php b/resources/views/licenses/checkin.blade.php index 6e3f9db4a86e..defd9172d7d3 100755 --- a/resources/views/licenses/checkin.blade.php +++ b/resources/views/licenses/checkin.blade.php @@ -17,7 +17,7 @@
- + {{csrf_field()}}
@@ -79,6 +79,9 @@ {!! $errors->first('notes', '') !!}
+ + +
- + {{csrf_field()}}
@@ -85,6 +85,9 @@ {!! $errors->first('note', '') !!}
+ + +
diff --git a/resources/views/modals/upload-file.blade.php b/resources/views/modals/upload-file.blade.php index 2433344157ff..d6ebd1ff4604 100644 --- a/resources/views/modals/upload-file.blade.php +++ b/resources/views/modals/upload-file.blade.php @@ -16,29 +16,18 @@ class="form-horizontal"