fix handle upload file on page precheck and postcheck

This commit is contained in:
2025-07-11 14:55:11 +07:00
parent 748ac8a77e
commit e3956ae0e4
5 changed files with 352 additions and 114 deletions

View File

@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Postcheck; use App\Models\Postcheck;
use App\Models\Transaction; use App\Models\Transaction;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class PostchecksController extends Controller class PostchecksController extends Controller
{ {
@@ -32,41 +34,155 @@ class PostchecksController extends Controller
'pressure_high' => 'required|numeric|min:0', 'pressure_high' => 'required|numeric|min:0',
'pressure_low' => 'nullable|numeric|min:0', 'pressure_low' => 'nullable|numeric|min:0',
'cabin_temperature' => 'nullable|numeric', '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_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_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_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()), 'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()),
'postcheck_notes' => 'nullable|string', '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 $data = [
$postcheck = Postcheck::create([
'transaction_id' => $transaction->id, 'transaction_id' => $transaction->id,
'postcheck_by' => auth()->id(), 'postcheck_by' => auth()->id(),
'postcheck_at' => now(), 'postcheck_at' => now(),
'police_number' => $transaction->police_number, 'police_number' => $transaction->police_number,
'spk_number' => $transaction->spk, 'spk_number' => $transaction->spk,
'front_image' => $request->front_image,
'kilometer' => $request->kilometer, 'kilometer' => $request->kilometer,
'pressure_high' => $request->pressure_high, 'pressure_high' => $request->pressure_high,
'pressure_low' => $request->pressure_low, 'pressure_low' => $request->pressure_low,
'cabin_temperature' => $request->cabin_temperature, 'cabin_temperature' => $request->cabin_temperature,
'cabin_temperature_image' => $request->cabin_temperature_image,
'ac_condition' => $request->ac_condition, 'ac_condition' => $request->ac_condition,
'ac_image' => $request->ac_image,
'blower_condition' => $request->blower_condition, 'blower_condition' => $request->blower_condition,
'blower_image' => $request->blower_image,
'evaporator_condition' => $request->evaporator_condition, 'evaporator_condition' => $request->evaporator_condition,
'evaporator_image' => $request->evaporator_image,
'compressor_condition' => $request->compressor_condition, 'compressor_condition' => $request->compressor_condition,
'postcheck_notes' => $request->postcheck_notes, '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);
} }
} }

View File

@@ -74,32 +74,32 @@ class PrechecksController extends Controller
try { try {
$file = $request->file($field); $file = $request->file($field);
// Generate unique filename // Generate unique filename with transaction ID
$filename = time() . '_' . uniqid() . '_' . $field . '.' . $file->getClientOriginalExtension(); $filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
// Create directory path // Create directory path: transactions/{transaction_id}/precheck/
$directory = 'prechecks/' . date('Y/m'); $directory = 'transactions/' . $transaction->id . '/precheck';
// Ensure base storage directory exists // Ensure base storage directory exists
$this->ensureStorageDirectoryExists(); $this->ensureStorageDirectoryExists();
// Ensure prechecks directory exists // Ensure transactions directory exists
if (!Storage::disk('public')->exists('prechecks')) { if (!Storage::disk('public')->exists('transactions')) {
Storage::disk('public')->makeDirectory('prechecks', 0755, true); Storage::disk('public')->makeDirectory('transactions', 0755, true);
Log::info('Created prechecks directory'); Log::info('Created transactions directory');
} }
// Ensure year directory exists // Ensure transaction ID directory exists
$yearDir = 'prechecks/' . date('Y'); $transactionDir = 'transactions/' . $transaction->id;
if (!Storage::disk('public')->exists($yearDir)) { if (!Storage::disk('public')->exists($transactionDir)) {
Storage::disk('public')->makeDirectory($yearDir, 0755, true); Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
Log::info('Created year directory: ' . $yearDir); Log::info('Created transaction directory: ' . $transactionDir);
} }
// Ensure month directory exists // Ensure precheck directory exists
if (!Storage::disk('public')->exists($directory)) { if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory, 0755, true); 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 // Store file in organized directory structure
@@ -114,6 +114,8 @@ class PrechecksController extends Controller
'size' => $file->getSize(), 'size' => $file->getSize(),
'mime_type' => $file->getMimeType(), 'mime_type' => $file->getMimeType(),
'uploaded_at' => now()->toISOString(), 'uploaded_at' => now()->toISOString(),
'transaction_id' => $transaction->id,
'filename' => $filename,
]; ];
Log::info('File uploaded successfully: ' . $path); Log::info('File uploaded successfully: ' . $path);
@@ -123,6 +125,7 @@ class PrechecksController extends Controller
Log::error('File upload failed: ' . $e->getMessage(), [ Log::error('File upload failed: ' . $e->getMessage(), [
'field' => $field, 'field' => $field,
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
'transaction_id' => $transaction->id,
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString() 'trace' => $e->getTraceAsString()
]); ]);
@@ -164,7 +167,7 @@ class PrechecksController extends Controller
'Please run one of these commands from your project root: ' . 'Please run one of these commands from your project root: ' .
'1) php fix_permissions.php ' . '1) php fix_permissions.php ' .
'2) chmod -R 775 storage/ ' . '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'
); );
} }

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Postcheck extends Model class Postcheck extends Model
{ {
@@ -16,17 +17,22 @@ class Postcheck extends Model
'police_number', 'police_number',
'spk_number', 'spk_number',
'front_image', 'front_image',
'front_image_metadata',
'kilometer', 'kilometer',
'pressure_high', 'pressure_high',
'pressure_low', 'pressure_low',
'cabin_temperature', 'cabin_temperature',
'cabin_temperature_image', 'cabin_temperature_image',
'cabin_temperature_image_metadata',
'ac_condition', 'ac_condition',
'ac_image', 'ac_image',
'ac_image_metadata',
'blower_condition', 'blower_condition',
'blower_image', 'blower_image',
'blower_image_metadata',
'evaporator_condition', 'evaporator_condition',
'evaporator_image', 'evaporator_image',
'evaporator_image_metadata',
'compressor_condition', 'compressor_condition',
'postcheck_notes' 'postcheck_notes'
]; ];
@@ -37,6 +43,11 @@ class Postcheck extends Model
'pressure_high' => 'decimal:2', 'pressure_high' => 'decimal:2',
'pressure_low' => 'decimal:2', 'pressure_low' => 'decimal:2',
'cabin_temperature' => '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'); 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 * Get the AC condition options
* *

View File

@@ -20,18 +20,23 @@ class CreatePostchecksTable extends Migration
$table->timestamp('postcheck_at')->nullable(); $table->timestamp('postcheck_at')->nullable();
$table->string('police_number'); $table->string('police_number');
$table->string('spk_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('kilometer', 10, 2);
$table->decimal('pressure_high', 10, 2); $table->decimal('pressure_high', 10, 2);
$table->decimal('pressure_low', 10, 2)->nullable(); $table->decimal('pressure_low', 10, 2)->nullable();
$table->decimal('cabin_temperature', 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->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->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->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->enum('compressor_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
$table->text('postcheck_notes')->nullable(); $table->text('postcheck_notes')->nullable();
$table->timestamps(); $table->timestamps();

View File

@@ -151,6 +151,16 @@
min-height: 80px; min-height: 80px;
} }
.file-input-hidden {
display: none;
}
.file-info {
margin-top: 5px;
font-size: 12px;
color: #6c757d;
}
/* Mobile Responsive */ /* Mobile Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
@@ -275,7 +285,7 @@
<a href="/" class="btn btn-warning btn-sm"> <a href="/" class="btn btn-warning btn-sm">
Kembali Kembali
</a> </a>
<form action="{{ route('postchecks.store', $transaction->id) }}" method="POST" id="postcheckForm"> <form action="{{ route('postchecks.store', $transaction->id) }}" method="POST" id="postcheckForm" enctype="multipart/form-data">
@csrf @csrf
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}"> <input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
@@ -332,13 +342,12 @@
</div> </div>
</div> </div>
<!-- Foto Depan --> <!-- Foto Depan -->
<div class="section-header"> <div class="section-header">
<h5><i class="fas fa-camera"></i> Foto Depan Kendaraan <span class="text-danger">*</span></h5> <h5><i class="fas fa-camera"></i> Foto Depan Kendaraan <span class="text-danger">*</span></h5>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="hidden" id="front_image" name="front_image" required> <input type="file" id="front_image" name="front_image" accept="image/*" class="file-input-hidden" required>
<div class="camera-container"> <div class="camera-container">
<video id="front_camera" autoplay playsinline class="camera-video"></video> <video id="front_camera" autoplay playsinline class="camera-video"></video>
<canvas id="front_canvas" style="display: none;"></canvas> <canvas id="front_canvas" style="display: none;"></canvas>
@@ -372,7 +381,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label for="cabin_temperature_image">Foto Suhu Kabin</label> <label for="cabin_temperature_image">Foto Suhu Kabin</label>
<input type="hidden" id="cabin_temperature_image" name="cabin_temperature_image"> <input type="file" id="cabin_temperature_image" name="cabin_temperature_image" accept="image/*" class="file-input-hidden">
<div class="camera-container"> <div class="camera-container">
<video id="cabin_camera" autoplay playsinline class="camera-video"></video> <video id="cabin_camera" autoplay playsinline class="camera-video"></video>
<canvas id="cabin_canvas" style="display: none;"></canvas> <canvas id="cabin_canvas" style="display: none;"></canvas>
@@ -413,23 +422,23 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label for="ac_image">Foto AC</label> <label for="ac_image">Foto AC</label>
<input type="hidden" id="ac_image" name="ac_image"> <input type="file" id="ac_image" name="ac_image" accept="image/*" class="file-input-hidden">
<div class="camera-container"> <div class="camera-container">
<video id="ac_camera" autoplay playsinline class="camera-video"></video> <video id="ac_camera" autoplay playsinline class="camera-video"></video>
<canvas id="ac_canvas" style="display: none;"></canvas> <canvas id="ac_canvas" style="display: none;"></canvas>
<div class="camera-controls"> <div class="camera-controls">
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('ac_camera')"> <button type="button" class="btn btn-primary btn-sm" onclick="startCamera('ac_camera')">
<i class="fas fa-camera"></i> Buka Kamera <i class="fas fa-camera"></i> Buka Kamera
</button> </button>
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('ac_camera', 'ac_canvas', 'ac_image', 'ac_preview')"> <button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('ac_camera', 'ac_canvas', 'ac_image', 'ac_preview')">
<i class="fas fa-camera-retro"></i> Ambil Foto <i class="fas fa-camera-retro"></i> Ambil Foto
</button> </button>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<small class="text-muted">Atau upload foto dari galeri:</small> <small class="text-muted">Atau upload foto dari galeri:</small>
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'ac_image', 'ac_preview')"> <input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'ac_image', 'ac_preview')">
</div> </div>
<div id="ac_preview" class="photo-preview"></div> <div id="ac_preview" class="photo-preview"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -454,23 +463,23 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label for="blower_image">Foto Blower</label> <label for="blower_image">Foto Blower</label>
<input type="hidden" id="blower_image" name="blower_image"> <input type="file" id="blower_image" name="blower_image" accept="image/*" class="file-input-hidden">
<div class="camera-container"> <div class="camera-container">
<video id="blower_camera" autoplay playsinline class="camera-video"></video> <video id="blower_camera" autoplay playsinline class="camera-video"></video>
<canvas id="blower_canvas" style="display: none;"></canvas> <canvas id="blower_canvas" style="display: none;"></canvas>
<div class="camera-controls"> <div class="camera-controls">
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('blower_camera')"> <button type="button" class="btn btn-primary btn-sm" onclick="startCamera('blower_camera')">
<i class="fas fa-camera"></i> Buka Kamera <i class="fas fa-camera"></i> Buka Kamera
</button> </button>
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('blower_camera', 'blower_canvas', 'blower_image', 'blower_preview')"> <button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('blower_camera', 'blower_canvas', 'blower_image', 'blower_preview')">
<i class="fas fa-camera-retro"></i> Ambil Foto <i class="fas fa-camera-retro"></i> Ambil Foto
</button> </button>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<small class="text-muted">Atau upload foto dari galeri:</small> <small class="text-muted">Atau upload foto dari galeri:</small>
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'blower_image', 'blower_preview')"> <input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'blower_image', 'blower_preview')">
</div> </div>
<div id="blower_preview" class="photo-preview"></div> <div id="blower_preview" class="photo-preview"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -495,23 +504,23 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label for="evaporator_image">Foto Evaporator</label> <label for="evaporator_image">Foto Evaporator</label>
<input type="hidden" id="evaporator_image" name="evaporator_image"> <input type="file" id="evaporator_image" name="evaporator_image" accept="image/*" class="file-input-hidden">
<div class="camera-container"> <div class="camera-container">
<video id="evaporator_camera" autoplay playsinline class="camera-video"></video> <video id="evaporator_camera" autoplay playsinline class="camera-video"></video>
<canvas id="evaporator_canvas" style="display: none;"></canvas> <canvas id="evaporator_canvas" style="display: none;"></canvas>
<div class="camera-controls"> <div class="camera-controls">
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('evaporator_camera')"> <button type="button" class="btn btn-primary btn-sm" onclick="startCamera('evaporator_camera')">
<i class="fas fa-camera"></i> Buka Kamera <i class="fas fa-camera"></i> Buka Kamera
</button> </button>
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('evaporator_camera', 'evaporator_canvas', 'evaporator_image', 'evaporator_preview')"> <button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('evaporator_camera', 'evaporator_canvas', 'evaporator_image', 'evaporator_preview')">
<i class="fas fa-camera-retro"></i> Ambil Foto <i class="fas fa-camera-retro"></i> Ambil Foto
</button> </button>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<small class="text-muted">Atau upload foto dari galeri:</small> <small class="text-muted">Atau upload foto dari galeri:</small>
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'evaporator_image', 'evaporator_preview')"> <input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'evaporator_image', 'evaporator_preview')">
</div> </div>
<div id="evaporator_preview" class="photo-preview"></div> <div id="evaporator_preview" class="photo-preview"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -580,8 +589,6 @@ function logout(event){
}) })
} }
// Fallback untuk browser lama // Fallback untuk browser lama
if (navigator.mediaDevices === undefined) { if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {}; navigator.mediaDevices = {};
@@ -671,11 +678,11 @@ async function startCamera(videoId) {
} }
} }
// Capture photo // Capture photo and convert to file
function capturePhoto(videoId, canvasId, inputId, previewId) { function capturePhoto(videoId, canvasId, inputId, previewId) {
const video = document.getElementById(videoId); const video = document.getElementById(videoId);
const canvas = document.getElementById(canvasId); const canvas = document.getElementById(canvasId);
const input = document.getElementById(inputId); const fileInput = document.getElementById(inputId);
const preview = document.getElementById(previewId); const preview = document.getElementById(previewId);
if (!video.srcObject) { if (!video.srcObject) {
@@ -695,23 +702,37 @@ function capturePhoto(videoId, canvasId, inputId, previewId) {
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height); context.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = canvas.toDataURL('image/jpeg', 0.8); // Convert canvas ke File object
input.value = imageData; 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 = `
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div class="mt-2">
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
<br>
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
</div>
`;
}, 'image/jpeg', 0.8);
preview.innerHTML = `
<img src="${imageData}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div class="mt-2">
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
</div>
`;
} catch (err) { } catch (err) {
alert('Gagal mengambil foto: ' + err.message); alert('Gagal mengambil foto: ' + err.message);
} }
} }
// Handle file upload from gallery
// Handle file upload
function handleFileUpload(input, inputId, previewId) { function handleFileUpload(input, inputId, previewId) {
const file = input.files[0]; const file = input.files[0];
if (!file) return; if (!file) return;
@@ -721,20 +742,29 @@ function handleFileUpload(input, inputId, previewId) {
return; return;
} }
const reader = new FileReader(); // Validasi ukuran file (max 2MB)
reader.onload = function(e) { if (file.size > 2 * 1024 * 1024) {
const imageData = e.target.result; alert('Ukuran file maksimal 2MB');
document.getElementById(inputId).value = imageData; return;
}
const preview = document.getElementById(previewId); // Assign ke file input yang sesuai
preview.innerHTML = ` const targetInput = document.getElementById(inputId);
<img src="${imageData}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"> const dataTransfer = new DataTransfer();
<div class="mt-2"> dataTransfer.items.add(file);
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small> targetInput.files = dataTransfer.files;
</div>
`; // Preview
}; const url = URL.createObjectURL(file);
reader.readAsDataURL(file); const preview = document.getElementById(previewId);
preview.innerHTML = `
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div class="mt-2">
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
<br>
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
</div>
`;
} }
// Stop all cameras when page is unloaded // Stop all cameras when page is unloaded
@@ -751,11 +781,23 @@ document.getElementById('postcheckForm').addEventListener('submit', function(e)
requiredFields.forEach(fieldId => { requiredFields.forEach(fieldId => {
const field = document.getElementById(fieldId); const field = document.getElementById(fieldId);
if (!field.value.trim()) {
field.classList.add('is-invalid'); if (field.type === 'file') {
isValid = false; // Validasi file input
if (!field.files || field.files.length === 0) {
field.classList.add('is-invalid');
isValid = false;
} else {
field.classList.remove('is-invalid');
}
} else { } 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');
}
} }
}); });