From 748ac8a77e707940e68dd936b0784cf5175592cf Mon Sep 17 00:00:00 2001 From: arifal Date: Fri, 11 Jul 2025 14:21:37 +0700 Subject: [PATCH] fix upload using file upload storage php not base64 --- .../Transactions/PrechecksController.php | 141 ++++++++++++++-- app/Models/Precheck.php | 72 +++++++++ ...25_07_10_140352_create_prechecks_table.php | 15 +- .../views/transaction/prechecks.blade.php | 151 +++++++++++------- 4 files changed, 306 insertions(+), 73 deletions(-) diff --git a/app/Http/Controllers/Transactions/PrechecksController.php b/app/Http/Controllers/Transactions/PrechecksController.php index 21d121e..69a3dc6 100644 --- a/app/Http/Controllers/Transactions/PrechecksController.php +++ b/app/Http/Controllers/Transactions/PrechecksController.php @@ -6,6 +6,8 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Precheck; use App\Models\Transaction; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Log; class PrechecksController extends Controller { @@ -32,41 +34,152 @@ class PrechecksController extends Controller 'pressure_high' => 'required|numeric|min:0', 'pressure_low' => 'nullable|numeric|min:0', 'cabin_temperature' => 'nullable|numeric', - 'cabin_temperature_image' => 'nullable|string', + 'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()), - 'ac_image' => 'nullable|string', + 'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()), - 'blower_image' => 'nullable|string', + 'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()), - 'evaporator_image' => 'nullable|string', + 'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()), 'precheck_notes' => 'nullable|string', - 'front_image' => 'required|string', + 'front_image' => 'required|image|mimes:jpeg,png,jpg|max:2048', ]); - // Pastikan transaction_id sama dengan $transaction->id - $precheck = Precheck::create([ + $data = [ 'transaction_id' => $transaction->id, 'precheck_by' => auth()->id(), 'precheck_at' => now(), 'police_number' => $transaction->police_number, 'spk_number' => $transaction->spk, - 'front_image' => $request->front_image, 'kilometer' => $request->kilometer, 'pressure_high' => $request->pressure_high, 'pressure_low' => $request->pressure_low, 'cabin_temperature' => $request->cabin_temperature, - 'cabin_temperature_image' => $request->cabin_temperature_image, 'ac_condition' => $request->ac_condition, - 'ac_image' => $request->ac_image, 'blower_condition' => $request->blower_condition, - 'blower_image' => $request->blower_image, 'evaporator_condition' => $request->evaporator_condition, - 'evaporator_image' => $request->evaporator_image, 'compressor_condition' => $request->compressor_condition, 'precheck_notes' => $request->precheck_notes, - ]); + ]; - return redirect()->route('transaction')->with('success', 'Precheck berhasil disimpan'); + // Handle file uploads + $imageFields = [ + 'front_image', 'cabin_temperature_image', 'ac_image', + 'blower_image', 'evaporator_image' + ]; + + foreach ($imageFields as $field) { + if ($request->hasFile($field) && $request->file($field)->isValid()) { + try { + $file = $request->file($field); + + // Generate unique filename + $filename = time() . '_' . uniqid() . '_' . $field . '.' . $file->getClientOriginalExtension(); + + // Create directory path + $directory = 'prechecks/' . date('Y/m'); + + // Ensure base storage directory exists + $this->ensureStorageDirectoryExists(); + + // Ensure prechecks directory exists + if (!Storage::disk('public')->exists('prechecks')) { + Storage::disk('public')->makeDirectory('prechecks', 0755, true); + Log::info('Created prechecks directory'); + } + + // Ensure year directory exists + $yearDir = 'prechecks/' . date('Y'); + if (!Storage::disk('public')->exists($yearDir)) { + Storage::disk('public')->makeDirectory($yearDir, 0755, true); + Log::info('Created year directory: ' . $yearDir); + } + + // Ensure month directory exists + if (!Storage::disk('public')->exists($directory)) { + Storage::disk('public')->makeDirectory($directory, 0755, true); + Log::info('Created month directory: ' . $directory); + } + + // Store file in organized directory structure + $path = $file->storeAs($directory, $filename, 'public'); + + // Store file path + $data[$field] = $path; + + // Store metadata + $data[$field . '_metadata'] = [ + 'original_name' => $file->getClientOriginalName(), + 'size' => $file->getSize(), + 'mime_type' => $file->getMimeType(), + 'uploaded_at' => now()->toISOString(), + ]; + + Log::info('File uploaded successfully: ' . $path); + + } catch (\Exception $e) { + // Log error for debugging + Log::error('File upload failed: ' . $e->getMessage(), [ + 'field' => $field, + 'file' => $file->getClientOriginalName(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]); + } + } + } + + try { + Precheck::create($data); + return redirect()->route('transaction')->with('success', 'Precheck berhasil disimpan'); + } catch (\Exception $e) { + Log::error('Precheck creation failed: ' . $e->getMessage()); + return back()->withErrors(['error' => 'Gagal menyimpan data precheck. Silakan coba lagi.']); + } + } + + /** + * Ensure the base storage directory exists + */ + private function ensureStorageDirectoryExists() + { + $storagePath = storage_path('app/public'); + + if (!is_dir($storagePath)) { + if (!mkdir($storagePath, 0755, true)) { + Log::error('Failed to create storage directory: ' . $storagePath); + throw new \Exception('Cannot create storage directory: ' . $storagePath . '. Please run: php fix_permissions.php or manually create the directory.'); + } + Log::info('Created storage directory: ' . $storagePath); + } + + // Check if directory is writable + if (!is_writable($storagePath)) { + Log::error('Storage directory is not writable: ' . $storagePath); + throw new \Exception( + 'Storage directory is not writable: ' . $storagePath . '. ' . + 'Please run one of these commands from your project root: ' . + '1) php fix_permissions.php ' . + '2) chmod -R 775 storage/ ' . + '3) mkdir -p storage/app/public/prechecks/' . date('Y/m') + ); + } + + // Check if we can create subdirectories + $testDir = $storagePath . '/test_' . time(); + if (!mkdir($testDir, 0755, true)) { + Log::error('Cannot create subdirectories in storage: ' . $storagePath); + throw new \Exception( + 'Cannot create subdirectories in storage. ' . + 'Please check permissions and run: php fix_permissions.php' + ); + } + + // Clean up test directory + rmdir($testDir); + Log::info('Storage directory is properly configured: ' . $storagePath); } } diff --git a/app/Models/Precheck.php b/app/Models/Precheck.php index 104b63c..598ea04 100644 --- a/app/Models/Precheck.php +++ b/app/Models/Precheck.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Storage; class Precheck extends Model { @@ -16,17 +17,22 @@ class Precheck extends Model 'police_number', 'spk_number', 'front_image', + 'front_image_metadata', 'kilometer', 'pressure_high', 'pressure_low', 'cabin_temperature', 'cabin_temperature_image', + 'cabin_temperature_image_metadata', 'ac_condition', 'ac_image', + 'ac_image_metadata', 'blower_condition', 'blower_image', + 'blower_image_metadata', 'evaporator_condition', 'evaporator_image', + 'evaporator_image_metadata', 'compressor_condition', 'precheck_notes' ]; @@ -37,6 +43,11 @@ class Precheck extends Model 'pressure_high' => 'decimal:2', 'pressure_low' => 'decimal:2', 'cabin_temperature' => 'decimal:2', + 'front_image_metadata' => 'array', + 'cabin_temperature_image_metadata' => 'array', + 'ac_image_metadata' => 'array', + 'blower_image_metadata' => 'array', + 'evaporator_image_metadata' => 'array', ]; /** @@ -59,6 +70,67 @@ class Precheck extends Model return $this->belongsTo(User::class, 'precheck_by'); } + /** + * Get front image URL + */ + public function getFrontImageUrlAttribute() + { + return $this->front_image ? Storage::disk('public')->url($this->front_image) : null; + } + + /** + * Get cabin temperature image URL + */ + public function getCabinTemperatureImageUrlAttribute() + { + return $this->cabin_temperature_image ? Storage::disk('public')->url($this->cabin_temperature_image) : null; + } + + /** + * Get AC image URL + */ + public function getAcImageUrlAttribute() + { + return $this->ac_image ? Storage::disk('public')->url($this->ac_image) : null; + } + + /** + * Get blower image URL + */ + public function getBlowerImageUrlAttribute() + { + return $this->blower_image ? Storage::disk('public')->url($this->blower_image) : null; + } + + /** + * Get evaporator image URL + */ + public function getEvaporatorImageUrlAttribute() + { + return $this->evaporator_image ? Storage::disk('public')->url($this->evaporator_image) : null; + } + + /** + * Delete associated files when model is deleted + */ + protected static function boot() + { + parent::boot(); + + static::deleting(function ($precheck) { + $imageFields = [ + 'front_image', 'cabin_temperature_image', 'ac_image', + 'blower_image', 'evaporator_image' + ]; + + foreach ($imageFields as $field) { + if ($precheck->$field && Storage::disk('public')->exists($precheck->$field)) { + Storage::disk('public')->delete($precheck->$field); + } + } + }); + } + /** * Get the AC condition options * diff --git a/database/migrations/2025_07_10_140352_create_prechecks_table.php b/database/migrations/2025_07_10_140352_create_prechecks_table.php index 87a54dd..635df64 100644 --- a/database/migrations/2025_07_10_140352_create_prechecks_table.php +++ b/database/migrations/2025_07_10_140352_create_prechecks_table.php @@ -20,18 +20,23 @@ class CreatePrechecksTable extends Migration $table->timestamp('precheck_at')->nullable(); $table->string('police_number'); $table->string('spk_number'); - $table->string('front_image'); + $table->string('front_image', 255)->nullable(); + $table->json('front_image_metadata')->nullable(); $table->decimal('kilometer', 10, 2); $table->decimal('pressure_high', 10, 2); $table->decimal('pressure_low', 10, 2)->nullable(); $table->decimal('cabin_temperature', 10, 2)->nullable(); - $table->string('cabin_temperature_image')->nullable(); + $table->string('cabin_temperature_image', 255)->nullable(); + $table->json('cabin_temperature_image_metadata')->nullable(); $table->enum('ac_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable(); - $table->string('ac_image')->nullable(); + $table->string('ac_image', 255)->nullable(); + $table->json('ac_image_metadata')->nullable(); $table->enum('blower_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable(); - $table->string('blower_image')->nullable(); + $table->string('blower_image', 255)->nullable(); + $table->json('blower_image_metadata')->nullable(); $table->enum('evaporator_condition', ['kotor', 'berlendir', 'bocor', 'bersih'])->nullable(); - $table->string('evaporator_image')->nullable(); + $table->string('evaporator_image', 255)->nullable(); + $table->json('evaporator_image_metadata')->nullable(); $table->enum('compressor_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable(); $table->text('precheck_notes')->nullable(); $table->timestamps(); diff --git a/resources/views/transaction/prechecks.blade.php b/resources/views/transaction/prechecks.blade.php index bbf0de7..6e1e910 100644 --- a/resources/views/transaction/prechecks.blade.php +++ b/resources/views/transaction/prechecks.blade.php @@ -151,6 +151,16 @@ min-height: 80px; } + .file-input-hidden { + display: none; + } + + .file-info { + margin-top: 5px; + font-size: 12px; + color: #6c757d; + } + /* Mobile Responsive */ @media (max-width: 768px) { .container { @@ -275,7 +285,7 @@ Kembali -
+ @csrf @@ -337,22 +347,22 @@
Foto Depan Kendaraan *
- +
-
- - -
-
- Atau upload foto dari galeri: - -
+
+ + +
+
+ Atau upload foto dari galeri: + +
@@ -371,7 +381,7 @@
- +
@@ -412,7 +422,7 @@
- +
@@ -453,7 +463,7 @@
- +
@@ -494,7 +504,7 @@
- +
@@ -579,8 +589,6 @@ }) } - - // Fallback untuk browser lama if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; @@ -670,11 +678,11 @@ } } - // Capture photo + // Capture photo and convert to file function capturePhoto(videoId, canvasId, inputId, previewId) { const video = document.getElementById(videoId); const canvas = document.getElementById(canvasId); - const input = document.getElementById(inputId); + const fileInput = document.getElementById(inputId); const preview = document.getElementById(previewId); if (!video.srcObject) { @@ -694,23 +702,37 @@ canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); - const imageData = canvas.toDataURL('image/jpeg', 0.8); - input.value = imageData; + // Convert canvas ke File object + canvas.toBlob(function(blob) { + // Buat File object + const file = new File([blob], `photo_${Date.now()}.jpg`, { + type: 'image/jpeg', + lastModified: Date.now() + }); + + // Assign ke file input + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + fileInput.files = dataTransfer.files; + + // Preview + const url = URL.createObjectURL(blob); + preview.innerHTML = ` + +
+ Foto berhasil diambil +
+ Ukuran: ${(file.size / 1024).toFixed(1)} KB +
+ `; + }, 'image/jpeg', 0.8); - preview.innerHTML = ` - -
- Foto berhasil diambil -
- `; - } catch (err) { - alert('Gagal mengambil foto: ' + err.message); - } + } catch (err) { + alert('Gagal mengambil foto: ' + err.message); + } } - - - // Handle file upload + // Handle file upload from gallery function handleFileUpload(input, inputId, previewId) { const file = input.files[0]; if (!file) return; @@ -720,20 +742,29 @@ return; } - const reader = new FileReader(); - reader.onload = function(e) { - const imageData = e.target.result; - document.getElementById(inputId).value = imageData; - - const preview = document.getElementById(previewId); - preview.innerHTML = ` - -
- Foto berhasil diupload -
- `; - }; - reader.readAsDataURL(file); + // Validasi ukuran file (max 2MB) + if (file.size > 2 * 1024 * 1024) { + alert('Ukuran file maksimal 2MB'); + return; + } + + // Assign ke file input yang sesuai + const targetInput = document.getElementById(inputId); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + targetInput.files = dataTransfer.files; + + // Preview + const url = URL.createObjectURL(file); + const preview = document.getElementById(previewId); + preview.innerHTML = ` + +
+ Foto berhasil diupload +
+ Ukuran: ${(file.size / 1024).toFixed(1)} KB +
+ `; } // Stop all cameras when page is unloaded @@ -750,11 +781,23 @@ requiredFields.forEach(fieldId => { const field = document.getElementById(fieldId); - if (!field.value.trim()) { - field.classList.add('is-invalid'); - isValid = false; + + if (field.type === 'file') { + // Validasi file input + if (!field.files || field.files.length === 0) { + field.classList.add('is-invalid'); + isValid = false; + } else { + field.classList.remove('is-invalid'); + } } else { - field.classList.remove('is-invalid'); + // Validasi input biasa + if (!field.value.trim()) { + field.classList.add('is-invalid'); + isValid = false; + } else { + field.classList.remove('is-invalid'); + } } });