fix icon target and handle using back or front camera precheck and postcheck
This commit is contained in:
@@ -232,7 +232,7 @@
|
|||||||
@can('view', $menus['kpi.targets.index'])
|
@can('view', $menus['kpi.targets.index'])
|
||||||
<li class="kt-menu__item" aria-haspopup="true">
|
<li class="kt-menu__item" aria-haspopup="true">
|
||||||
<a href="{{ route('kpi.targets.index') }}" class="kt-menu__link">
|
<a href="{{ route('kpi.targets.index') }}" class="kt-menu__link">
|
||||||
<i class="fa fa-user-cog" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
<i class="fa fa-bullseye" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||||
<span class="kt-menu__link-text">Target</span>
|
<span class="kt-menu__link-text">Target</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@extends('layouts.frontapp')
|
@extends('layouts.frontapp')
|
||||||
|
|
||||||
@section('styles')
|
@section('styles')
|
||||||
|
<!-- SweetAlert2 CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ asset('node_modules/sweetalert2/dist/sweetalert2.min.css') }}">
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@@ -123,6 +125,16 @@
|
|||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: linear-gradient(135deg, #0dcaf0 0%, #0aa2c0 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-lg {
|
.btn-lg {
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -140,6 +152,48 @@
|
|||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.photo-preview-container {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls .btn {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls .btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-invalid:focus {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 80%;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-info {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: rgba(5, 150, 105, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #059669;
|
||||||
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -358,6 +412,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('front_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('front_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -392,6 +452,12 @@
|
|||||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
||||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('cabin_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('cabin_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -433,6 +499,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('ac_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('ac_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -474,6 +546,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('blower_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('blower_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -515,6 +593,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('evaporator_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('evaporator_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -569,12 +653,20 @@
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('javascripts')
|
@section('javascripts')
|
||||||
|
<!-- SweetAlert2 JS -->
|
||||||
|
<script src="{{ asset('node_modules/sweetalert2/dist/sweetalert2.all.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
let streams = {};
|
let streams = {};
|
||||||
|
|
||||||
// Logout function
|
// Logout function
|
||||||
function logout(event){
|
function logout(event){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Stop all cameras before logout
|
||||||
|
Object.keys(streams).forEach(videoId => {
|
||||||
|
stopCamera(videoId);
|
||||||
|
});
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Logout?',
|
title: 'Logout?',
|
||||||
text: "Anda akan keluar dari sistem!",
|
text: "Anda akan keluar dari sistem!",
|
||||||
@@ -617,6 +709,45 @@ if (navigator.getUserMedia === undefined) {
|
|||||||
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get available cameras and select back camera
|
||||||
|
async function getBackCamera() {
|
||||||
|
try {
|
||||||
|
// Minta izin kamera terlebih dahulu
|
||||||
|
await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
|
||||||
|
console.log('Kamera yang tersedia:', videoDevices.map(d => ({ label: d.label, deviceId: d.deviceId })));
|
||||||
|
|
||||||
|
// Cari kamera belakang berdasarkan label atau deviceId
|
||||||
|
const backCamera = videoDevices.find(device =>
|
||||||
|
device.label.toLowerCase().includes('back') ||
|
||||||
|
device.label.toLowerCase().includes('belakang') ||
|
||||||
|
device.label.toLowerCase().includes('rear') ||
|
||||||
|
device.label.toLowerCase().includes('environment') ||
|
||||||
|
device.deviceId.includes('back') ||
|
||||||
|
device.deviceId.includes('rear')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (backCamera) {
|
||||||
|
console.log('Kamera belakang ditemukan:', backCamera.label);
|
||||||
|
return backCamera.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada kamera belakang yang terdeteksi, gunakan kamera pertama
|
||||||
|
if (videoDevices.length > 0) {
|
||||||
|
console.log('Menggunakan kamera pertama:', videoDevices[0].label);
|
||||||
|
return videoDevices[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Tidak dapat mendapatkan daftar kamera:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start camera
|
// Start camera
|
||||||
async function startCamera(videoId) {
|
async function startCamera(videoId) {
|
||||||
try {
|
try {
|
||||||
@@ -627,13 +758,82 @@ async function startCamera(videoId) {
|
|||||||
throw new Error('Browser tidak mendukung akses kamera');
|
throw new Error('Browser tidak mendukung akses kamera');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stream yang sedang berjalan
|
// Cek apakah kamera sudah berjalan
|
||||||
if (streams[videoId]) {
|
if (streams[videoId] && streams[videoId].active) {
|
||||||
streams[videoId].getTracks().forEach(track => track.stop());
|
console.log('Kamera sudah berjalan untuk:', videoId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konfigurasi kamera
|
// Stop stream yang sedang berjalan untuk kamera lain
|
||||||
|
Object.keys(streams).forEach(key => {
|
||||||
|
if (streams[key] && streams[key].active) {
|
||||||
|
streams[key].getTracks().forEach(track => track.stop());
|
||||||
|
delete streams[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dapatkan deviceId kamera belakang
|
||||||
|
const backCameraId = await getBackCamera();
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
if (backCameraId) {
|
||||||
|
// Gunakan deviceId kamera belakang yang spesifik
|
||||||
const constraints = {
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
deviceId: { exact: backCameraId },
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
console.log('Menggunakan kamera belakang dengan deviceId:', backCameraId);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Gagal menggunakan kamera belakang, mencoba fallback...');
|
||||||
|
// Fallback ke constraint biasa
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: 'environment' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Jika tidak bisa mendapatkan deviceId, gunakan facingMode
|
||||||
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: 'environment' } // Kamera belakang
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Kamera belakang tidak tersedia, mencoba kamera depan...');
|
||||||
|
|
||||||
|
// Fallback ke kamera depan
|
||||||
|
const frontCameraConstraints = {
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: 'user' } // Kamera depan
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(frontCameraConstraints);
|
||||||
|
} catch (frontErr) {
|
||||||
|
// Jika kamera depan juga gagal, coba tanpa constraint facingMode
|
||||||
|
const fallbackConstraints = {
|
||||||
video: {
|
video: {
|
||||||
width: { min: 320, ideal: 640, max: 1280 },
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
height: { min: 240, ideal: 480, max: 720 },
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
@@ -641,7 +841,10 @@ async function startCamera(videoId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
stream = await navigator.mediaDevices.getUserMedia(fallbackConstraints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
streams[videoId] = stream;
|
streams[videoId] = stream;
|
||||||
@@ -649,13 +852,48 @@ async function startCamera(videoId) {
|
|||||||
// Tunggu video siap
|
// Tunggu video siap
|
||||||
video.onloadedmetadata = function() {
|
video.onloadedmetadata = function() {
|
||||||
video.play();
|
video.play();
|
||||||
|
|
||||||
|
// Tampilkan informasi kamera yang digunakan
|
||||||
|
const cameraInfo = document.createElement('div');
|
||||||
|
cameraInfo.className = 'camera-info';
|
||||||
|
cameraInfo.innerHTML = `
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-camera"></i> Kamera aktif - ${backCameraId ? 'Kamera belakang' : 'Kamera default'}
|
||||||
|
</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Hapus info kamera sebelumnya jika ada
|
||||||
|
const existingInfo = video.parentElement.querySelector('.camera-info');
|
||||||
|
if (existingInfo) {
|
||||||
|
existingInfo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.parentElement.appendChild(cameraInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
video.onerror = function(e) {
|
video.onerror = function(e) {
|
||||||
console.error('Error pada video:', e);
|
console.error('Error pada video:', e);
|
||||||
alert('Error pada video stream');
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error Video',
|
||||||
|
text: 'Error pada video stream',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('Kamera berhasil dibuka untuk:', videoId);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi sukses
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Kamera Berhasil Dibuka',
|
||||||
|
text: backCameraId ? 'Menggunakan kamera belakang' : 'Menggunakan kamera default',
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Pesan error yang lebih spesifik
|
// Pesan error yang lebih spesifik
|
||||||
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
||||||
@@ -674,7 +912,12 @@ async function startCamera(videoId) {
|
|||||||
errorMessage += 'Error: ' + err.message;
|
errorMessage += 'Error: ' + err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(errorMessage);
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error Kamera',
|
||||||
|
text: errorMessage,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,13 +929,23 @@ function capturePhoto(videoId, canvasId, inputId, previewId) {
|
|||||||
const preview = document.getElementById(previewId);
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
if (!video.srcObject) {
|
if (!video.srcObject) {
|
||||||
alert('Silakan buka kamera terlebih dahulu');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Kamera Belum Dibuka',
|
||||||
|
text: 'Silakan buka kamera terlebih dahulu',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pastikan video sudah siap
|
// Pastikan video sudah siap
|
||||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||||
alert('Video belum siap. Tunggu sebentar dan coba lagi.');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Video Belum Siap',
|
||||||
|
text: 'Video belum siap. Tunggu sebentar dan coba lagi.',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,36 +968,250 @@ function capturePhoto(videoId, canvasId, inputId, previewId) {
|
|||||||
dataTransfer.items.add(file);
|
dataTransfer.items.add(file);
|
||||||
fileInput.files = dataTransfer.files;
|
fileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
// Preview
|
// Preview dengan tombol batal dan retake
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
preview.innerHTML = `
|
preview.innerHTML = `
|
||||||
|
<div class="photo-preview-container">
|
||||||
<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);">
|
<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">
|
<div class="mt-2">
|
||||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-warning btn-sm mr-1" onclick="retakePhoto('${videoId}', '${inputId}', '${previewId}')">
|
||||||
|
<i class="fas fa-redo"></i> Ambil Ulang
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="cancelPhoto('${inputId}', '${previewId}')">
|
||||||
|
<i class="fas fa-times"></i> Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Stop kamera setelah mengambil foto
|
||||||
|
stopCamera(videoId);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto berhasil diambil
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Foto Berhasil Diambil',
|
||||||
|
text: 'Foto berhasil disimpan',
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
|
||||||
}, 'image/jpeg', 0.8);
|
}, 'image/jpeg', 0.8);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Gagal mengambil foto: ' + err.message);
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal Mengambil Foto',
|
||||||
|
text: 'Gagal mengambil foto: ' + err.message,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to retake photo
|
||||||
|
function retakePhoto(videoId, inputId, previewId) {
|
||||||
|
const fileInput = document.getElementById(inputId);
|
||||||
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
|
// Clear file input
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
fileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Clear preview
|
||||||
|
preview.innerHTML = '';
|
||||||
|
|
||||||
|
// Stop any existing camera first
|
||||||
|
stopCamera(videoId);
|
||||||
|
|
||||||
|
// Start camera again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
startCamera(videoId);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto diambil ulang
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Mengambil Foto Ulang',
|
||||||
|
text: 'Kamera dibuka untuk mengambil foto ulang',
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to cancel photo
|
||||||
|
function cancelPhoto(inputId, previewId) {
|
||||||
|
const fileInput = document.getElementById(inputId);
|
||||||
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
|
// Clear file input
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
fileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Clear preview
|
||||||
|
preview.innerHTML = '';
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto dibatalkan
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Foto Dibatalkan',
|
||||||
|
text: 'Foto berhasil dibatalkan',
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to switch between front and back camera
|
||||||
|
async function switchCamera(videoId) {
|
||||||
|
const video = document.getElementById(videoId);
|
||||||
|
|
||||||
|
if (!streams[videoId]) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Kamera Belum Dibuka',
|
||||||
|
text: 'Silakan buka kamera terlebih dahulu',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop current stream
|
||||||
|
stopCamera(videoId);
|
||||||
|
|
||||||
|
// Wait a bit before starting new camera
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Get current facing mode
|
||||||
|
const currentTracks = streams[videoId] ? streams[videoId].getVideoTracks() : [];
|
||||||
|
let currentFacingMode = 'environment'; // default to back camera
|
||||||
|
|
||||||
|
if (currentTracks.length > 0) {
|
||||||
|
const settings = currentTracks[0].getSettings();
|
||||||
|
if (settings.facingMode) {
|
||||||
|
currentFacingMode = settings.facingMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to opposite camera
|
||||||
|
const newFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment';
|
||||||
|
|
||||||
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: newFacingMode }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
video.srcObject = stream;
|
||||||
|
streams[videoId] = stream;
|
||||||
|
|
||||||
|
// Update camera info
|
||||||
|
video.onloadedmetadata = function() {
|
||||||
|
video.play();
|
||||||
|
|
||||||
|
const cameraInfo = document.createElement('div');
|
||||||
|
cameraInfo.className = 'camera-info';
|
||||||
|
cameraInfo.innerHTML = `
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-camera"></i> Kamera aktif - ${newFacingMode === 'environment' ? 'Kamera belakang' : 'Kamera depan'}
|
||||||
|
</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existingInfo = video.parentElement.querySelector('.camera-info');
|
||||||
|
if (existingInfo) {
|
||||||
|
existingInfo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.parentElement.appendChild(cameraInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Berhasil beralih ke kamera:', newFacingMode === 'environment' ? 'belakang' : 'depan');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Gagal beralih kamera:', err);
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal Beralih Kamera',
|
||||||
|
text: 'Tidak dapat beralih ke kamera lain: ' + err.message,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to stop camera
|
||||||
|
function stopCamera(videoId) {
|
||||||
|
const video = document.getElementById(videoId);
|
||||||
|
|
||||||
|
if (streams[videoId]) {
|
||||||
|
streams[videoId].getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('Track stopped:', track.kind);
|
||||||
|
});
|
||||||
|
delete streams[videoId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.srcObject) {
|
||||||
|
video.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus info kamera
|
||||||
|
const cameraInfo = video.parentElement.querySelector('.camera-info');
|
||||||
|
if (cameraInfo) {
|
||||||
|
cameraInfo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Kamera ditutup untuk:', videoId);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi kamera ditutup
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Kamera Ditutup',
|
||||||
|
text: 'Kamera berhasil ditutup',
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle file upload from gallery
|
// Handle file upload from gallery
|
||||||
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;
|
||||||
|
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
alert('Pilih file gambar');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'File Tidak Valid',
|
||||||
|
text: 'Pilih file gambar',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validasi ukuran file (max 2MB)
|
// Validasi ukuran file (max 2MB)
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
alert('Ukuran file maksimal 2MB');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Ukuran File Terlalu Besar',
|
||||||
|
text: 'Ukuran file maksimal 2MB',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,30 +1221,71 @@ function handleFileUpload(input, inputId, previewId) {
|
|||||||
dataTransfer.items.add(file);
|
dataTransfer.items.add(file);
|
||||||
targetInput.files = dataTransfer.files;
|
targetInput.files = dataTransfer.files;
|
||||||
|
|
||||||
// Preview
|
// Preview dengan tombol batal
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const preview = document.getElementById(previewId);
|
const preview = document.getElementById(previewId);
|
||||||
preview.innerHTML = `
|
preview.innerHTML = `
|
||||||
|
<div class="photo-preview-container">
|
||||||
<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);">
|
<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">
|
<div class="mt-2">
|
||||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="cancelPhoto('${inputId}', '${previewId}')">
|
||||||
|
<i class="fas fa-times"></i> Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Clear the original input
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto berhasil diupload
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Foto Berhasil Diupload',
|
||||||
|
text: 'Foto berhasil disimpan dari galeri',
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop all cameras when page is unloaded
|
// Stop all cameras when page is unloaded
|
||||||
window.addEventListener('beforeunload', function() {
|
window.addEventListener('beforeunload', function() {
|
||||||
Object.values(streams).forEach(stream => {
|
Object.values(streams).forEach(stream => {
|
||||||
stream.getTracks().forEach(track => track.stop());
|
if (stream && stream.active) {
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('Track stopped on page unload:', track.kind);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop all cameras when page is hidden (mobile browsers)
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.hidden) {
|
||||||
|
Object.values(streams).forEach(stream => {
|
||||||
|
if (stream && stream.active) {
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('Track stopped on page hidden:', track.kind);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
document.getElementById('postcheckForm').addEventListener('submit', function(e) {
|
document.getElementById('postcheckForm').addEventListener('submit', function(e) {
|
||||||
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
let errorMessages = [];
|
||||||
|
|
||||||
requiredFields.forEach(fieldId => {
|
requiredFields.forEach(fieldId => {
|
||||||
const field = document.getElementById(fieldId);
|
const field = document.getElementById(fieldId);
|
||||||
@@ -787,6 +1295,7 @@ document.getElementById('postcheckForm').addEventListener('submit', function(e)
|
|||||||
if (!field.files || field.files.length === 0) {
|
if (!field.files || field.files.length === 0) {
|
||||||
field.classList.add('is-invalid');
|
field.classList.add('is-invalid');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
errorMessages.push('Foto depan kendaraan wajib diisi');
|
||||||
} else {
|
} else {
|
||||||
field.classList.remove('is-invalid');
|
field.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
@@ -795,6 +1304,11 @@ document.getElementById('postcheckForm').addEventListener('submit', function(e)
|
|||||||
if (!field.value.trim()) {
|
if (!field.value.trim()) {
|
||||||
field.classList.add('is-invalid');
|
field.classList.add('is-invalid');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
if (fieldId === 'kilometer') {
|
||||||
|
errorMessages.push('Kilometer wajib diisi');
|
||||||
|
} else if (fieldId === 'pressure_high') {
|
||||||
|
errorMessages.push('Pressure High wajib diisi');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
field.classList.remove('is-invalid');
|
field.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
@@ -803,7 +1317,31 @@ document.getElementById('postcheckForm').addEventListener('submit', function(e)
|
|||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Mohon lengkapi semua field yang wajib diisi');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
html: errorMessages.join('<br>'),
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Stop all cameras before submitting
|
||||||
|
Object.keys(streams).forEach(videoId => {
|
||||||
|
stopCamera(videoId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove error styling when user starts typing
|
||||||
|
document.addEventListener('input', function(e) {
|
||||||
|
if (e.target.classList.contains('is-invalid')) {
|
||||||
|
e.target.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove error styling when user selects a file
|
||||||
|
document.addEventListener('change', function(e) {
|
||||||
|
if (e.target.type === 'file' && e.target.classList.contains('is-invalid')) {
|
||||||
|
e.target.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@extends('layouts.frontapp')
|
@extends('layouts.frontapp')
|
||||||
|
|
||||||
@section('styles')
|
@section('styles')
|
||||||
|
<!-- SweetAlert2 CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ asset('node_modules/sweetalert2/dist/sweetalert2.min.css') }}">
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@@ -123,6 +125,16 @@
|
|||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: linear-gradient(135deg, #0dcaf0 0%, #0aa2c0 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-lg {
|
.btn-lg {
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -140,6 +152,48 @@
|
|||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.photo-preview-container {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls .btn {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls .btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-invalid:focus {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 80%;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-info {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: rgba(5, 150, 105, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #059669;
|
||||||
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -358,6 +412,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('front_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('front_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -392,6 +452,12 @@
|
|||||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
||||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('cabin_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('cabin_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -433,6 +499,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('ac_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('ac_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -474,6 +546,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('blower_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('blower_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -515,6 +593,12 @@
|
|||||||
<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>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm" onclick="stopCamera('evaporator_camera')">
|
||||||
|
<i class="fas fa-times"></i> Tutup Kamera
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm" onclick="switchCamera('evaporator_camera')">
|
||||||
|
<i class="fas fa-sync"></i> Ganti Kamera
|
||||||
|
</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>
|
||||||
@@ -569,12 +653,20 @@
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('javascripts')
|
@section('javascripts')
|
||||||
|
<!-- SweetAlert2 JS -->
|
||||||
|
<script src="{{ asset('node_modules/sweetalert2/dist/sweetalert2.all.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
let streams = {};
|
let streams = {};
|
||||||
|
|
||||||
// Logout function
|
// Logout function
|
||||||
function logout(event){
|
function logout(event){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Stop all cameras before logout
|
||||||
|
Object.keys(streams).forEach(videoId => {
|
||||||
|
stopCamera(videoId);
|
||||||
|
});
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Logout?',
|
title: 'Logout?',
|
||||||
text: "Anda akan keluar dari sistem!",
|
text: "Anda akan keluar dari sistem!",
|
||||||
@@ -617,6 +709,45 @@
|
|||||||
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get available cameras and select back camera
|
||||||
|
async function getBackCamera() {
|
||||||
|
try {
|
||||||
|
// Minta izin kamera terlebih dahulu
|
||||||
|
await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
|
||||||
|
console.log('Kamera yang tersedia:', videoDevices.map(d => ({ label: d.label, deviceId: d.deviceId })));
|
||||||
|
|
||||||
|
// Cari kamera belakang berdasarkan label atau deviceId
|
||||||
|
const backCamera = videoDevices.find(device =>
|
||||||
|
device.label.toLowerCase().includes('back') ||
|
||||||
|
device.label.toLowerCase().includes('belakang') ||
|
||||||
|
device.label.toLowerCase().includes('rear') ||
|
||||||
|
device.label.toLowerCase().includes('environment') ||
|
||||||
|
device.deviceId.includes('back') ||
|
||||||
|
device.deviceId.includes('rear')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (backCamera) {
|
||||||
|
console.log('Kamera belakang ditemukan:', backCamera.label);
|
||||||
|
return backCamera.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada kamera belakang yang terdeteksi, gunakan kamera pertama
|
||||||
|
if (videoDevices.length > 0) {
|
||||||
|
console.log('Menggunakan kamera pertama:', videoDevices[0].label);
|
||||||
|
return videoDevices[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Tidak dapat mendapatkan daftar kamera:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start camera
|
// Start camera
|
||||||
async function startCamera(videoId) {
|
async function startCamera(videoId) {
|
||||||
try {
|
try {
|
||||||
@@ -627,13 +758,82 @@
|
|||||||
throw new Error('Browser tidak mendukung akses kamera');
|
throw new Error('Browser tidak mendukung akses kamera');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stream yang sedang berjalan
|
// Cek apakah kamera sudah berjalan
|
||||||
if (streams[videoId]) {
|
if (streams[videoId] && streams[videoId].active) {
|
||||||
streams[videoId].getTracks().forEach(track => track.stop());
|
console.log('Kamera sudah berjalan untuk:', videoId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konfigurasi kamera
|
// Stop stream yang sedang berjalan untuk kamera lain
|
||||||
|
Object.keys(streams).forEach(key => {
|
||||||
|
if (streams[key] && streams[key].active) {
|
||||||
|
streams[key].getTracks().forEach(track => track.stop());
|
||||||
|
delete streams[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dapatkan deviceId kamera belakang
|
||||||
|
const backCameraId = await getBackCamera();
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
if (backCameraId) {
|
||||||
|
// Gunakan deviceId kamera belakang yang spesifik
|
||||||
const constraints = {
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
deviceId: { exact: backCameraId },
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
console.log('Menggunakan kamera belakang dengan deviceId:', backCameraId);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Gagal menggunakan kamera belakang, mencoba fallback...');
|
||||||
|
// Fallback ke constraint biasa
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: 'environment' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Jika tidak bisa mendapatkan deviceId, gunakan facingMode
|
||||||
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: 'environment' } // Kamera belakang
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Kamera belakang tidak tersedia, mencoba kamera depan...');
|
||||||
|
|
||||||
|
// Fallback ke kamera depan
|
||||||
|
const frontCameraConstraints = {
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: 'user' } // Kamera depan
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(frontCameraConstraints);
|
||||||
|
} catch (frontErr) {
|
||||||
|
// Jika kamera depan juga gagal, coba tanpa constraint facingMode
|
||||||
|
const fallbackConstraints = {
|
||||||
video: {
|
video: {
|
||||||
width: { min: 320, ideal: 640, max: 1280 },
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
height: { min: 240, ideal: 480, max: 720 },
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
@@ -641,7 +841,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
stream = await navigator.mediaDevices.getUserMedia(fallbackConstraints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
streams[videoId] = stream;
|
streams[videoId] = stream;
|
||||||
@@ -649,13 +852,48 @@
|
|||||||
// Tunggu video siap
|
// Tunggu video siap
|
||||||
video.onloadedmetadata = function() {
|
video.onloadedmetadata = function() {
|
||||||
video.play();
|
video.play();
|
||||||
|
|
||||||
|
// Tampilkan informasi kamera yang digunakan
|
||||||
|
const cameraInfo = document.createElement('div');
|
||||||
|
cameraInfo.className = 'camera-info';
|
||||||
|
cameraInfo.innerHTML = `
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-camera"></i> Kamera aktif - ${backCameraId ? 'Kamera belakang' : 'Kamera default'}
|
||||||
|
</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Hapus info kamera sebelumnya jika ada
|
||||||
|
const existingInfo = video.parentElement.querySelector('.camera-info');
|
||||||
|
if (existingInfo) {
|
||||||
|
existingInfo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.parentElement.appendChild(cameraInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
video.onerror = function(e) {
|
video.onerror = function(e) {
|
||||||
console.error('Error pada video:', e);
|
console.error('Error pada video:', e);
|
||||||
alert('Error pada video stream');
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error Video',
|
||||||
|
text: 'Error pada video stream',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('Kamera berhasil dibuka untuk:', videoId);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi sukses
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Kamera Berhasil Dibuka',
|
||||||
|
text: backCameraId ? 'Menggunakan kamera belakang' : 'Menggunakan kamera default',
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Pesan error yang lebih spesifik
|
// Pesan error yang lebih spesifik
|
||||||
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
||||||
@@ -674,7 +912,12 @@
|
|||||||
errorMessage += 'Error: ' + err.message;
|
errorMessage += 'Error: ' + err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(errorMessage);
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error Kamera',
|
||||||
|
text: errorMessage,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,13 +929,23 @@
|
|||||||
const preview = document.getElementById(previewId);
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
if (!video.srcObject) {
|
if (!video.srcObject) {
|
||||||
alert('Silakan buka kamera terlebih dahulu');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Kamera Belum Dibuka',
|
||||||
|
text: 'Silakan buka kamera terlebih dahulu',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pastikan video sudah siap
|
// Pastikan video sudah siap
|
||||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||||
alert('Video belum siap. Tunggu sebentar dan coba lagi.');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Video Belum Siap',
|
||||||
|
text: 'Video belum siap. Tunggu sebentar dan coba lagi.',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,36 +968,250 @@
|
|||||||
dataTransfer.items.add(file);
|
dataTransfer.items.add(file);
|
||||||
fileInput.files = dataTransfer.files;
|
fileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
// Preview
|
// Preview dengan tombol batal dan retake
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
preview.innerHTML = `
|
preview.innerHTML = `
|
||||||
|
<div class="photo-preview-container">
|
||||||
<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);">
|
<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">
|
<div class="mt-2">
|
||||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-warning btn-sm mr-1" onclick="retakePhoto('${videoId}', '${inputId}', '${previewId}')">
|
||||||
|
<i class="fas fa-redo"></i> Ambil Ulang
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="cancelPhoto('${inputId}', '${previewId}')">
|
||||||
|
<i class="fas fa-times"></i> Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Stop kamera setelah mengambil foto
|
||||||
|
stopCamera(videoId);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto berhasil diambil
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Foto Berhasil Diambil',
|
||||||
|
text: 'Foto berhasil disimpan',
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
|
||||||
}, 'image/jpeg', 0.8);
|
}, 'image/jpeg', 0.8);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Gagal mengambil foto: ' + err.message);
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal Mengambil Foto',
|
||||||
|
text: 'Gagal mengambil foto: ' + err.message,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to retake photo
|
||||||
|
function retakePhoto(videoId, inputId, previewId) {
|
||||||
|
const fileInput = document.getElementById(inputId);
|
||||||
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
|
// Clear file input
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
fileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Clear preview
|
||||||
|
preview.innerHTML = '';
|
||||||
|
|
||||||
|
// Stop any existing camera first
|
||||||
|
stopCamera(videoId);
|
||||||
|
|
||||||
|
// Start camera again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
startCamera(videoId);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto diambil ulang
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Mengambil Foto Ulang',
|
||||||
|
text: 'Kamera dibuka untuk mengambil foto ulang',
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to cancel photo
|
||||||
|
function cancelPhoto(inputId, previewId) {
|
||||||
|
const fileInput = document.getElementById(inputId);
|
||||||
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
|
// Clear file input
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
fileInput.files = dataTransfer.files;
|
||||||
|
|
||||||
|
// Clear preview
|
||||||
|
preview.innerHTML = '';
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto dibatalkan
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Foto Dibatalkan',
|
||||||
|
text: 'Foto berhasil dibatalkan',
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to switch between front and back camera
|
||||||
|
async function switchCamera(videoId) {
|
||||||
|
const video = document.getElementById(videoId);
|
||||||
|
|
||||||
|
if (!streams[videoId]) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Kamera Belum Dibuka',
|
||||||
|
text: 'Silakan buka kamera terlebih dahulu',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop current stream
|
||||||
|
stopCamera(videoId);
|
||||||
|
|
||||||
|
// Wait a bit before starting new camera
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Get current facing mode
|
||||||
|
const currentTracks = streams[videoId] ? streams[videoId].getVideoTracks() : [];
|
||||||
|
let currentFacingMode = 'environment'; // default to back camera
|
||||||
|
|
||||||
|
if (currentTracks.length > 0) {
|
||||||
|
const settings = currentTracks[0].getSettings();
|
||||||
|
if (settings.facingMode) {
|
||||||
|
currentFacingMode = settings.facingMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to opposite camera
|
||||||
|
const newFacingMode = currentFacingMode === 'environment' ? 'user' : 'environment';
|
||||||
|
|
||||||
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
width: { min: 320, ideal: 640, max: 1280 },
|
||||||
|
height: { min: 240, ideal: 480, max: 720 },
|
||||||
|
aspectRatio: { ideal: 4/3 },
|
||||||
|
facingMode: { ideal: newFacingMode }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
video.srcObject = stream;
|
||||||
|
streams[videoId] = stream;
|
||||||
|
|
||||||
|
// Update camera info
|
||||||
|
video.onloadedmetadata = function() {
|
||||||
|
video.play();
|
||||||
|
|
||||||
|
const cameraInfo = document.createElement('div');
|
||||||
|
cameraInfo.className = 'camera-info';
|
||||||
|
cameraInfo.innerHTML = `
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-camera"></i> Kamera aktif - ${newFacingMode === 'environment' ? 'Kamera belakang' : 'Kamera depan'}
|
||||||
|
</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existingInfo = video.parentElement.querySelector('.camera-info');
|
||||||
|
if (existingInfo) {
|
||||||
|
existingInfo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.parentElement.appendChild(cameraInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Berhasil beralih ke kamera:', newFacingMode === 'environment' ? 'belakang' : 'depan');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Gagal beralih kamera:', err);
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal Beralih Kamera',
|
||||||
|
text: 'Tidak dapat beralih ke kamera lain: ' + err.message,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to stop camera
|
||||||
|
function stopCamera(videoId) {
|
||||||
|
const video = document.getElementById(videoId);
|
||||||
|
|
||||||
|
if (streams[videoId]) {
|
||||||
|
streams[videoId].getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('Track stopped:', track.kind);
|
||||||
|
});
|
||||||
|
delete streams[videoId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.srcObject) {
|
||||||
|
video.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus info kamera
|
||||||
|
const cameraInfo = video.parentElement.querySelector('.camera-info');
|
||||||
|
if (cameraInfo) {
|
||||||
|
cameraInfo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Kamera ditutup untuk:', videoId);
|
||||||
|
|
||||||
|
// Tampilkan notifikasi kamera ditutup
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: 'Kamera Ditutup',
|
||||||
|
text: 'Kamera berhasil ditutup',
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle file upload from gallery
|
// Handle file upload from gallery
|
||||||
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;
|
||||||
|
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
alert('Pilih file gambar');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'File Tidak Valid',
|
||||||
|
text: 'Pilih file gambar',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validasi ukuran file (max 2MB)
|
// Validasi ukuran file (max 2MB)
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
alert('Ukuran file maksimal 2MB');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Ukuran File Terlalu Besar',
|
||||||
|
text: 'Ukuran file maksimal 2MB',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,30 +1221,71 @@
|
|||||||
dataTransfer.items.add(file);
|
dataTransfer.items.add(file);
|
||||||
targetInput.files = dataTransfer.files;
|
targetInput.files = dataTransfer.files;
|
||||||
|
|
||||||
// Preview
|
// Preview dengan tombol batal
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const preview = document.getElementById(previewId);
|
const preview = document.getElementById(previewId);
|
||||||
preview.innerHTML = `
|
preview.innerHTML = `
|
||||||
|
<div class="photo-preview-container">
|
||||||
<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);">
|
<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">
|
<div class="mt-2">
|
||||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" onclick="cancelPhoto('${inputId}', '${previewId}')">
|
||||||
|
<i class="fas fa-times"></i> Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Clear the original input
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
// Tampilkan notifikasi foto berhasil diupload
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Foto Berhasil Diupload',
|
||||||
|
text: 'Foto berhasil disimpan dari galeri',
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false,
|
||||||
|
toast: true,
|
||||||
|
position: 'top-end'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop all cameras when page is unloaded
|
// Stop all cameras when page is unloaded
|
||||||
window.addEventListener('beforeunload', function() {
|
window.addEventListener('beforeunload', function() {
|
||||||
Object.values(streams).forEach(stream => {
|
Object.values(streams).forEach(stream => {
|
||||||
stream.getTracks().forEach(track => track.stop());
|
if (stream && stream.active) {
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('Track stopped on page unload:', track.kind);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop all cameras when page is hidden (mobile browsers)
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.hidden) {
|
||||||
|
Object.values(streams).forEach(stream => {
|
||||||
|
if (stream && stream.active) {
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('Track stopped on page hidden:', track.kind);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
document.getElementById('precheckForm').addEventListener('submit', function(e) {
|
document.getElementById('precheckForm').addEventListener('submit', function(e) {
|
||||||
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
let errorMessages = [];
|
||||||
|
|
||||||
requiredFields.forEach(fieldId => {
|
requiredFields.forEach(fieldId => {
|
||||||
const field = document.getElementById(fieldId);
|
const field = document.getElementById(fieldId);
|
||||||
@@ -787,6 +1295,7 @@
|
|||||||
if (!field.files || field.files.length === 0) {
|
if (!field.files || field.files.length === 0) {
|
||||||
field.classList.add('is-invalid');
|
field.classList.add('is-invalid');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
errorMessages.push('Foto depan kendaraan wajib diisi');
|
||||||
} else {
|
} else {
|
||||||
field.classList.remove('is-invalid');
|
field.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
@@ -795,6 +1304,11 @@
|
|||||||
if (!field.value.trim()) {
|
if (!field.value.trim()) {
|
||||||
field.classList.add('is-invalid');
|
field.classList.add('is-invalid');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
if (fieldId === 'kilometer') {
|
||||||
|
errorMessages.push('Kilometer wajib diisi');
|
||||||
|
} else if (fieldId === 'pressure_high') {
|
||||||
|
errorMessages.push('Pressure High wajib diisi');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
field.classList.remove('is-invalid');
|
field.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
@@ -803,7 +1317,31 @@
|
|||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('Mohon lengkapi semua field yang wajib diisi');
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
html: errorMessages.join('<br>'),
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Stop all cameras before submitting
|
||||||
|
Object.keys(streams).forEach(videoId => {
|
||||||
|
stopCamera(videoId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove error styling when user starts typing
|
||||||
|
document.addEventListener('input', function(e) {
|
||||||
|
if (e.target.classList.contains('is-invalid')) {
|
||||||
|
e.target.classList.remove('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove error styling when user selects a file
|
||||||
|
document.addEventListener('change', function(e) {
|
||||||
|
if (e.target.type === 'file' && e.target.classList.contains('is-invalid')) {
|
||||||
|
e.target.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user