fix upload using file upload storage php not base64
This commit is contained in:
@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\Precheck;
|
use App\Models\Precheck;
|
||||||
use App\Models\Transaction;
|
use App\Models\Transaction;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class PrechecksController extends Controller
|
class PrechecksController extends Controller
|
||||||
{
|
{
|
||||||
@@ -32,41 +34,152 @@ class PrechecksController 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(',', Precheck::getAcConditionOptions()),
|
'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_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_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()),
|
'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()),
|
||||||
'precheck_notes' => 'nullable|string',
|
'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
|
$data = [
|
||||||
$precheck = Precheck::create([
|
|
||||||
'transaction_id' => $transaction->id,
|
'transaction_id' => $transaction->id,
|
||||||
'precheck_by' => auth()->id(),
|
'precheck_by' => auth()->id(),
|
||||||
'precheck_at' => now(),
|
'precheck_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,
|
||||||
'precheck_notes' => $request->precheck_notes,
|
'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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 Precheck extends Model
|
class Precheck extends Model
|
||||||
{
|
{
|
||||||
@@ -16,17 +17,22 @@ class Precheck 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',
|
||||||
'precheck_notes'
|
'precheck_notes'
|
||||||
];
|
];
|
||||||
@@ -37,6 +43,11 @@ class Precheck 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 Precheck extends Model
|
|||||||
return $this->belongsTo(User::class, 'precheck_by');
|
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
|
* Get the AC condition options
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -20,18 +20,23 @@ class CreatePrechecksTable extends Migration
|
|||||||
$table->timestamp('precheck_at')->nullable();
|
$table->timestamp('precheck_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', ['kotor', 'rusak', 'baik', 'tidak ada'])->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->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->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->enum('compressor_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||||
$table->text('precheck_notes')->nullable();
|
$table->text('precheck_notes')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|||||||
@@ -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('prechecks.store', $transaction->id) }}" method="POST" id="precheckForm">
|
<form action="{{ route('prechecks.store', $transaction->id) }}" method="POST" id="precheckForm" enctype="multipart/form-data">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
|
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
|
||||||
|
|
||||||
@@ -337,22 +347,22 @@
|
|||||||
<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>
|
||||||
<div class="camera-controls">
|
<div class="camera-controls">
|
||||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('front_camera')">
|
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('front_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('front_camera', 'front_canvas', 'front_image', 'front_preview')">
|
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('front_camera', 'front_canvas', 'front_image', 'front_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, 'front_image', 'front_preview')">
|
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'front_image', 'front_preview')">
|
||||||
</div>
|
</div>
|
||||||
<div id="front_preview" class="photo-preview"></div>
|
<div id="front_preview" class="photo-preview"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,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>
|
||||||
@@ -412,7 +422,7 @@
|
|||||||
<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>
|
||||||
@@ -453,7 +463,7 @@
|
|||||||
<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>
|
||||||
@@ -494,7 +504,7 @@
|
|||||||
<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>
|
||||||
@@ -579,8 +589,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Fallback untuk browser lama
|
// Fallback untuk browser lama
|
||||||
if (navigator.mediaDevices === undefined) {
|
if (navigator.mediaDevices === undefined) {
|
||||||
navigator.mediaDevices = {};
|
navigator.mediaDevices = {};
|
||||||
@@ -670,11 +678,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -694,23 +702,37 @@
|
|||||||
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 = `
|
} catch (err) {
|
||||||
<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);">
|
alert('Gagal mengambil foto: ' + err.message);
|
||||||
<div class="mt-2">
|
}
|
||||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} catch (err) {
|
|
||||||
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;
|
||||||
@@ -720,20 +742,29 @@
|
|||||||
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);
|
|
||||||
preview.innerHTML = `
|
// Assign ke file input yang sesuai
|
||||||
<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 targetInput = document.getElementById(inputId);
|
||||||
<div class="mt-2">
|
const dataTransfer = new DataTransfer();
|
||||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
dataTransfer.items.add(file);
|
||||||
</div>
|
targetInput.files = dataTransfer.files;
|
||||||
`;
|
|
||||||
};
|
// Preview
|
||||||
reader.readAsDataURL(file);
|
const url = URL.createObjectURL(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
|
||||||
@@ -750,11 +781,23 @@
|
|||||||
|
|
||||||
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user