Add asset photo uploads with gallery and QR section

This commit is contained in:
Arya Dwi Putra
2025-12-02 07:46:05 +07:00
parent c44364c344
commit 5e17b0efc0
6 changed files with 174 additions and 9 deletions

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers;
use App\Models\Asset;
use App\Models\AssetPhoto;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class AssetPhotoController extends Controller
{
public function store(Request $request, Asset $asset): RedirectResponse
{
$data = $request->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.');
}
}

View File

@@ -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);
}
}

28
app/Models/AssetPhoto.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AssetPhoto extends Model
{
use HasFactory, HasUuids;
protected $fillable = [
'asset_id',
'path',
'is_primary',
'caption',
];
protected $casts = [
'is_primary' => 'boolean',
];
public function asset()
{
return $this->belongsTo(Asset::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('asset_photos', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -45,16 +45,35 @@
</div>
<div class="col-xl-4">
<div class="card custom-card h-100">
<div class="card-header">
<div class="card-title mb-0">QR Aset</div>
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card-title mb-0">QR & Foto Aset</div>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#uploadPhotoModal">Upload Foto</button>
</div>
<div class="card-body text-center">
@if($asset->qr_path)
<img src="{{ asset('storage/'.$asset->qr_path) }}" alt="QR {{ $asset->code }}" class="img-fluid mb-3">
@else
<p class="text-muted mb-3">QR belum tersedia.</p>
@endif
<p class="mb-0"><code>{{ route('assets.show', $asset) }}</code></p>
<div class="card-body">
<div class="text-center mb-3">
@if($asset->qr_path)
<img src="{{ asset('storage/'.$asset->qr_path) }}" alt="QR {{ $asset->code }}" class="img-fluid mb-2" style="max-height:180px;">
@else
<p class="text-muted mb-3">QR belum tersedia.</p>
@endif
<p class="mb-0"><code>{{ route('assets.show', $asset) }}</code></p>
</div>
<div class="row g-2">
@forelse($asset->photos as $photo)
<div class="col-6">
<div class="position-relative">
<img src="{{ asset('storage/'.$photo->path) }}" alt="foto aset" class="img-fluid rounded shadow-sm">
<form action="{{ route('assets.photos.destroy', $photo) }}" method="POST" class="position-absolute top-0 end-0 m-1" onsubmit="return confirm('Hapus foto ini?')">
@csrf
@method('DELETE')
<button class="btn btn-xs btn-outline-danger">x</button>
</form>
</div>
</div>
@empty
<div class="col-12 text-center text-muted">Belum ada foto aset.</div>
@endforelse
</div>
</div>
</div>
</div>
@@ -272,4 +291,30 @@
</div>
</div>
</div>
{{-- Modal Upload Foto --}}
<div class="modal fade" id="uploadPhotoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Upload Foto Aset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ route('assets.photos.store', $asset) }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Pilih Foto (bisa lebih dari satu)</label>
<input type="file" name="photos[]" class="form-control" multiple accept="image/*" required>
<small class="text-muted">Format: jpg, png, webp. Maks 4MB per file.</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -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');