Add asset photo uploads with gallery and QR section
This commit is contained in:
52
app/Http/Controllers/AssetPhotoController.php
Normal file
52
app/Http/Controllers/AssetPhotoController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
@@ -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
28
app/Models/AssetPhoto.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user