fix icon target and handle using back or front camera precheck and postcheck

This commit is contained in:
2025-07-14 11:46:51 +07:00
parent 68e7eb3087
commit a59f685d41
3 changed files with 1329 additions and 253 deletions

View File

@@ -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>

View File

@@ -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!",
@@ -587,14 +679,14 @@ function logout(event){
$('#logout-form').submit(); $('#logout-form').submit();
} }
}) })
} }
// Fallback untuk browser lama // Fallback untuk browser lama
if (navigator.mediaDevices === undefined) { if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {}; navigator.mediaDevices = {};
} }
if (navigator.mediaDevices.getUserMedia === undefined) { if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function(constraints) { navigator.mediaDevices.getUserMedia = function(constraints) {
// Coba berbagai versi getUserMedia // Coba berbagai versi getUserMedia
const getUserMedia = navigator.webkitGetUserMedia || const getUserMedia = navigator.webkitGetUserMedia ||
@@ -610,15 +702,54 @@ if (navigator.mediaDevices.getUserMedia === undefined) {
getUserMedia.call(navigator, constraints, resolve, reject); getUserMedia.call(navigator, constraints, resolve, reject);
}); });
} }
} }
// Tambahan fallback untuk browser yang sangat lama // Tambahan fallback untuk browser yang sangat lama
if (navigator.getUserMedia === undefined) { if (navigator.getUserMedia === undefined) {
navigator.getUserMedia = navigator.mediaDevices.getUserMedia; navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
} }
// Start camera // Get available cameras and select back camera
async function startCamera(videoId) { 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
async function startCamera(videoId) {
try { try {
const video = document.getElementById(videoId); const video = document.getElementById(videoId);
@@ -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,25 +912,40 @@ 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'
});
}
} }
}
// Capture photo and convert to file // 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 fileInput = document.getElementById(inputId); const fileInput = document.getElementById(inputId);
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'
});
}
} }
}
// Handle file upload from gallery // Function to retake photo
function handleFileUpload(input, inputId, previewId) { 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
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>
`; `;
}
// Stop all cameras when page is unloaded // Clear the original input
window.addEventListener('beforeunload', function() { input.value = '';
Object.values(streams).forEach(stream => {
stream.getTracks().forEach(track => track.stop()); // 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'
}); });
}); }
// Form validation // Stop all cameras when page is unloaded
document.getElementById('postcheckForm').addEventListener('submit', function(e) { window.addEventListener('beforeunload', function() {
Object.values(streams).forEach(stream => {
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
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,8 +1317,32 @@ 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>
@endsection @endsection

View File

@@ -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>