diff --git a/app/Http/Controllers/Transactions/PostchecksController.php b/app/Http/Controllers/Transactions/PostchecksController.php index f022fee..d2e8b8e 100644 --- a/app/Http/Controllers/Transactions/PostchecksController.php +++ b/app/Http/Controllers/Transactions/PostchecksController.php @@ -6,6 +6,8 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Postcheck; use App\Models\Transaction; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Log; class PostchecksController extends Controller { @@ -32,41 +34,155 @@ class PostchecksController 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(',', Postcheck::getAcConditionOptions()), - 'ac_image' => 'nullable|string', + 'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()), - 'blower_image' => 'nullable|string', + 'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()), - 'evaporator_image' => 'nullable|string', + 'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()), 'postcheck_notes' => 'nullable|string', - 'front_image' => 'required|string', + 'front_image' => 'required|image|mimes:jpeg,png,jpg|max:2048', ]); - // Pastikan transaction_id sama dengan $transaction->id - $postcheck = Postcheck::create([ + $data = [ 'transaction_id' => $transaction->id, 'postcheck_by' => auth()->id(), 'postcheck_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, 'postcheck_notes' => $request->postcheck_notes, - ]); + ]; - return redirect()->route('transaction')->with('success', 'Postcheck 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 with transaction ID + $filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension(); + + // Create directory path: transactions/{transaction_id}/postcheck/ + $directory = 'transactions/' . $transaction->id . '/postcheck'; + + // Ensure base storage directory exists + $this->ensureStorageDirectoryExists(); + + // Ensure transactions directory exists + if (!Storage::disk('public')->exists('transactions')) { + Storage::disk('public')->makeDirectory('transactions', 0755, true); + Log::info('Created transactions directory'); + } + + // Ensure transaction ID directory exists + $transactionDir = 'transactions/' . $transaction->id; + if (!Storage::disk('public')->exists($transactionDir)) { + Storage::disk('public')->makeDirectory($transactionDir, 0755, true); + Log::info('Created transaction directory: ' . $transactionDir); + } + + // Ensure postcheck directory exists + if (!Storage::disk('public')->exists($directory)) { + Storage::disk('public')->makeDirectory($directory, 0755, true); + Log::info('Created postcheck 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(), + 'transaction_id' => $transaction->id, + 'filename' => $filename, + ]; + + 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(), + 'transaction_id' => $transaction->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]); + } + } + } + + try { + Postcheck::create($data); + return redirect()->route('transaction')->with('success', 'Postcheck berhasil disimpan'); + } catch (\Exception $e) { + Log::error('Postcheck creation failed: ' . $e->getMessage()); + return back()->withErrors(['error' => 'Gagal menyimpan data postcheck. 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/transactions/{transaction_id}/postcheck' + ); + } + + // 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/Http/Controllers/Transactions/PrechecksController.php b/app/Http/Controllers/Transactions/PrechecksController.php index 69a3dc6..e9d4c69 100644 --- a/app/Http/Controllers/Transactions/PrechecksController.php +++ b/app/Http/Controllers/Transactions/PrechecksController.php @@ -74,32 +74,32 @@ class PrechecksController extends Controller try { $file = $request->file($field); - // Generate unique filename - $filename = time() . '_' . uniqid() . '_' . $field . '.' . $file->getClientOriginalExtension(); + // Generate unique filename with transaction ID + $filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension(); - // Create directory path - $directory = 'prechecks/' . date('Y/m'); + // Create directory path: transactions/{transaction_id}/precheck/ + $directory = 'transactions/' . $transaction->id . '/precheck'; // 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 transactions directory exists + if (!Storage::disk('public')->exists('transactions')) { + Storage::disk('public')->makeDirectory('transactions', 0755, true); + Log::info('Created transactions 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 transaction ID directory exists + $transactionDir = 'transactions/' . $transaction->id; + if (!Storage::disk('public')->exists($transactionDir)) { + Storage::disk('public')->makeDirectory($transactionDir, 0755, true); + Log::info('Created transaction directory: ' . $transactionDir); } - // Ensure month directory exists + // Ensure precheck directory exists if (!Storage::disk('public')->exists($directory)) { Storage::disk('public')->makeDirectory($directory, 0755, true); - Log::info('Created month directory: ' . $directory); + Log::info('Created precheck directory: ' . $directory); } // Store file in organized directory structure @@ -114,6 +114,8 @@ class PrechecksController extends Controller 'size' => $file->getSize(), 'mime_type' => $file->getMimeType(), 'uploaded_at' => now()->toISOString(), + 'transaction_id' => $transaction->id, + 'filename' => $filename, ]; Log::info('File uploaded successfully: ' . $path); @@ -123,6 +125,7 @@ class PrechecksController extends Controller Log::error('File upload failed: ' . $e->getMessage(), [ 'field' => $field, 'file' => $file->getClientOriginalName(), + 'transaction_id' => $transaction->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); @@ -164,7 +167,7 @@ class PrechecksController extends Controller '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') + '3) mkdir -p storage/app/public/transactions/{transaction_id}/precheck' ); } diff --git a/app/Models/Postcheck.php b/app/Models/Postcheck.php index 5134376..83b6b01 100644 --- a/app/Models/Postcheck.php +++ b/app/Models/Postcheck.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 Postcheck extends Model { @@ -16,17 +17,22 @@ class Postcheck 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', 'postcheck_notes' ]; @@ -37,6 +43,11 @@ class Postcheck 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 Postcheck extends Model return $this->belongsTo(User::class, 'postcheck_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 ($postcheck) { + $imageFields = [ + 'front_image', 'cabin_temperature_image', 'ac_image', + 'blower_image', 'evaporator_image' + ]; + + foreach ($imageFields as $field) { + if ($postcheck->$field && Storage::disk('public')->exists($postcheck->$field)) { + Storage::disk('public')->delete($postcheck->$field); + } + } + }); + } + /** * Get the AC condition options * diff --git a/database/migrations/2025_07_10_140406_create_postchecks_table.php b/database/migrations/2025_07_10_140406_create_postchecks_table.php index 082d054..7340925 100644 --- a/database/migrations/2025_07_10_140406_create_postchecks_table.php +++ b/database/migrations/2025_07_10_140406_create_postchecks_table.php @@ -20,18 +20,23 @@ class CreatePostchecksTable extends Migration $table->timestamp('postcheck_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', ['sudah dikerjakan', 'sudah diganti'])->nullable(); - $table->string('ac_image')->nullable(); + $table->string('ac_image', 255)->nullable(); + $table->json('ac_image_metadata')->nullable(); $table->enum('blower_condition', ['sudah dibersihkan atau dicuci', 'sudah diganti'])->nullable(); - $table->string('blower_image')->nullable(); + $table->string('blower_image', 255)->nullable(); + $table->json('blower_image_metadata')->nullable(); $table->enum('evaporator_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable(); - $table->string('evaporator_image')->nullable(); + $table->string('evaporator_image', 255)->nullable(); + $table->json('evaporator_image_metadata')->nullable(); $table->enum('compressor_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable(); $table->text('postcheck_notes')->nullable(); $table->timestamps(); diff --git a/resources/views/transaction/postchecks.blade.php b/resources/views/transaction/postchecks.blade.php index 38c02bc..6135ac0 100644 --- a/resources/views/transaction/postchecks.blade.php +++ b/resources/views/transaction/postchecks.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 @@ -332,13 +342,12 @@ -
Foto Depan Kendaraan *
- +
@@ -372,7 +381,7 @@
- +
@@ -413,23 +422,23 @@
- +
-
- - -
-
- Atau upload foto dari galeri: - -
-
+
+ + +
+
+ Atau upload foto dari galeri: + +
+
@@ -454,23 +463,23 @@
- +
-
- - -
-
- Atau upload foto dari galeri: - -
-
+
+ + +
+
+ Atau upload foto dari galeri: + +
+
@@ -495,23 +504,23 @@
- +
-
- - -
-
- Atau upload foto dari galeri: - -
-
+
+ + +
+
+ Atau upload foto dari galeri: + +
+
@@ -580,8 +589,6 @@ function logout(event){ }) } - - // Fallback untuk browser lama if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; @@ -671,11 +678,11 @@ async function startCamera(videoId) { } } -// 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) { @@ -695,23 +702,37 @@ function capturePhoto(videoId, canvasId, inputId, previewId) { 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); } } - - -// Handle file upload +// Handle file upload from gallery function handleFileUpload(input, inputId, previewId) { const file = input.files[0]; if (!file) return; @@ -721,20 +742,29 @@ function handleFileUpload(input, inputId, previewId) { 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 @@ -751,11 +781,23 @@ document.getElementById('postcheckForm').addEventListener('submit', function(e) 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'); + } } });