From 5e17b0efc049b026bdca810d0a6bdaf1341e2fba Mon Sep 17 00:00:00 2001 From: Arya Dwi Putra Date: Tue, 2 Dec 2025 07:46:05 +0700 Subject: [PATCH] Add asset photo uploads with gallery and QR section --- app/Http/Controllers/AssetPhotoController.php | 52 +++++++++++++++ app/Models/Asset.php | 10 +++ app/Models/AssetPhoto.php | 28 +++++++++ ...12_08_000000_create_asset_photos_table.php | 25 ++++++++ resources/views/assets/show.blade.php | 63 ++++++++++++++++--- routes/web.php | 5 ++ 6 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 app/Http/Controllers/AssetPhotoController.php create mode 100644 app/Models/AssetPhoto.php create mode 100644 database/migrations/2025_12_08_000000_create_asset_photos_table.php diff --git a/app/Http/Controllers/AssetPhotoController.php b/app/Http/Controllers/AssetPhotoController.php new file mode 100644 index 0000000..9092c8b --- /dev/null +++ b/app/Http/Controllers/AssetPhotoController.php @@ -0,0 +1,52 @@ +validate([ + 'photos' => ['required', 'array'], + 'photos.*' => ['image', 'mimes:jpeg,png,jpg,webp', 'max:4096'], + ]); + + $stored = 0; + + foreach ($data['photos'] as $file) { + $path = $file->store("assets/{$asset->id}", 'public'); + + AssetPhoto::create([ + 'asset_id' => $asset->id, + 'path' => $path, + 'is_primary' => $asset->photos()->count() === 0, + ]); + $stored++; + } + + return back()->with('success', "{$stored} foto aset berhasil diunggah."); + } + + public function destroy(AssetPhoto $asset_photo): RedirectResponse + { + Storage::disk('public')->delete($asset_photo->path); + $asset = $asset_photo->asset; + $wasPrimary = $asset_photo->is_primary; + $asset_photo->delete(); + + if ($wasPrimary) { + $next = $asset->photos()->first(); + if ($next) { + $next->update(['is_primary' => true]); + } + } + + return back()->with('success', 'Foto aset berhasil dihapus.'); + } +} diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 097fc5e..74f6b61 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -107,4 +107,14 @@ class Asset extends Model { return $this->hasMany(AssetMaintenance::class); } + + public function photos() + { + return $this->hasMany(AssetPhoto::class); + } + + public function primaryPhoto() + { + return $this->hasOne(AssetPhoto::class)->where('is_primary', true); + } } diff --git a/app/Models/AssetPhoto.php b/app/Models/AssetPhoto.php new file mode 100644 index 0000000..e6460a5 --- /dev/null +++ b/app/Models/AssetPhoto.php @@ -0,0 +1,28 @@ + 'boolean', + ]; + + public function asset() + { + return $this->belongsTo(Asset::class); + } +} diff --git a/database/migrations/2025_12_08_000000_create_asset_photos_table.php b/database/migrations/2025_12_08_000000_create_asset_photos_table.php new file mode 100644 index 0000000..ba6f5d3 --- /dev/null +++ b/database/migrations/2025_12_08_000000_create_asset_photos_table.php @@ -0,0 +1,25 @@ +uuid('id')->primary(); + $table->foreignUuid('asset_id')->constrained('assets')->cascadeOnDelete(); + $table->string('path'); + $table->boolean('is_primary')->default(false); + $table->string('caption')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('asset_photos'); + } +}; diff --git a/resources/views/assets/show.blade.php b/resources/views/assets/show.blade.php index 7c385b3..e5e5acd 100644 --- a/resources/views/assets/show.blade.php +++ b/resources/views/assets/show.blade.php @@ -45,16 +45,35 @@
-
-
QR Aset
+
+
QR & Foto Aset
+
-
- @if($asset->qr_path) - QR {{ $asset->code }} - @else -

QR belum tersedia.

- @endif -

{{ route('assets.show', $asset) }}

+
+
+ @if($asset->qr_path) + QR {{ $asset->code }} + @else +

QR belum tersedia.

+ @endif +

{{ route('assets.show', $asset) }}

+
+
+ @forelse($asset->photos as $photo) +
+
+ foto aset +
+ @csrf + @method('DELETE') + +
+
+
+ @empty +
Belum ada foto aset.
+ @endforelse +
@@ -272,4 +291,30 @@
+ + {{-- Modal Upload Foto --}} + @endsection diff --git a/routes/web.php b/routes/web.php index 0dbed21..abfc047 100644 --- a/routes/web.php +++ b/routes/web.php @@ -23,6 +23,7 @@ use App\Http\Controllers\AssetTransactionPageController; use App\Http\Controllers\ReportController; use App\Http\Controllers\AssetMaintenanceController; use App\Http\Controllers\AuthController; +use App\Http\Controllers\AssetPhotoController; use App\Http\Controllers\Controller; Route::view('/', 'pages.landing.index')->name('landing'); @@ -63,6 +64,10 @@ Route::middleware('auth')->group(function () { Route::get('assets/{asset}/history', [AssetController::class, 'history'])->name('assets.history')->middleware('permission:assets.view'); Route::resource('assets', AssetController::class)->only(['index', 'store', 'update', 'show']) ->middleware(['permission:assets.view|assets.manage']); + Route::post('assets/{asset}/photos', [AssetPhotoController::class, 'store'])->name('assets.photos.store') + ->middleware('permission:assets.manage'); + Route::delete('asset-photos/{asset_photo}', [AssetPhotoController::class, 'destroy'])->name('assets.photos.destroy') + ->middleware('permission:assets.manage'); Route::post('assets/{asset}/movements', [AssetTransactionController::class, 'storeMovement'])->name('assets.movements.store') ->middleware('permission:movements.manage');