eSPJ/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Index.cshtml

1019 lines
43 KiB
Plaintext

@{
Layout = "~/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml";
ViewData["Title"] = "Detail Penjemputan";
}
<div class="w-full lg:max-w-sm mx-auto min-h-screen bg-gray-50 pb-24">
<div class="bg-upst text-white px-6 pt-8 pb-16 rounded-b-[40px] shadow-lg relative">
<div class="flex items-center justify-between relative z-10">
<a href="@Url.Action("Index", "Home")" class="w-10 h-10 flex items-center justify-center bg-white/10 rounded-xl backdrop-blur-md">
<i class="w-5 h-5" data-lucide="chevron-left"></i>
</a>
<h1 class="text-lg font-black uppercase tracking-tight">Detail Lokasi</h1>
<div class="w-10"></div>
</div>
</div>
<div class="flex justify-center -mt-8 mb-6 px-6 relative z-20">
<div class="bg-white border border-gray-100 px-6 py-3 rounded-2xl flex items-center gap-4 w-full justify-between">
<div class="flex flex-col">
<span class="text-[10px] font-black text-gray-400 uppercase tracking-widest">Plat Nomor</span>
<span id="plat-nomor" class="text-green-700 font-black text-xl leading-none">B 9632 TOR</span>
</div>
<div class="h-8 w-[1px] bg-gray-100"></div>
<div class="text-right">
<span class="text-[10px] font-black text-gray-400 uppercase tracking-widest">No. Pintu</span>
<p class="text-orange-500 font-bold text-sm">JRC 005</p>
</div>
</div>
</div>
<div class="px-6 space-y-6">
<div class="bg-white rounded-[32px] p-6 border border-gray-100 relative overflow-hidden">
<div class="absolute top-0 right-0 p-4">
<span class="px-3 py-1 bg-green-500 text-white rounded-full text-[10px] font-black tracking-tighter uppercase">
PENGANGKUTAN
</span>
</div>
<div class="space-y-4">
<div>
<p class="text-[10px] text-gray-400 uppercase tracking-widest mb-1">Perusahaan</p>
<h2 class="text-xl font-black text-gray-800 leading-tight">CV Tri Berkah Sejahtera</h2>
</div>
<div class="flex items-center justify-center gap-2 bg-gray-50 p-2 rounded-xl">
<p class="text-gray-600 font-mono text-xl font-bold uppercase tracking-tighter">SPJ/07-2025/PKM/000476</p>
</div>
<div class="pt-2 border-t border-gray-50">
<div class="flex gap-2 items-start">
<p class="text-gray-500 text-xs font-medium leading-relaxed">
Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470
</p>
</div>
</div>
<div class="pt-4 border-t border-gray-100 mt-4">
<div class="flex items-center justify-between mb-2">
<p class="text-[10px] text-gray-400 uppercase tracking-widest">Total Berat Semua TPS</p>
<span class="text-2xl font-black text-upst"><span id="grand-total-timbangan">0,00</span> kg</span>
</div>
</div>
</div>
</div>
<div id="tps-selection-container" class="bg-white rounded-[32px] p-6 border border-gray-100" style="display: none;">
<div class="flex items-center gap-2 mb-4">
<div class="w-2 h-2 rounded-full bg-upst"></div>
<h3 class="font-black text-gray-800">Pilih TPS yang Akan Diangkut</h3>
</div>
<div id="tps-checkboxes" class="space-y-2 mb-4">
</div>
<button type="button" id="btn-confirm-tps" class="w-full bg-upst text-white py-3 rounded-xl font-bold text-sm hover:brightness-110">
Lanjutkan ke Form
</button>
</div>
<div id="tps-tabs-container" class="bg-white rounded-[32px] p-6 border border-gray-100" style="display: none;">
<div class="flex items-center gap-2 mb-4">
<div class="w-2 h-2 rounded-full bg-upst"></div>
<h3 class="font-black text-gray-800">Form Pengangkutan</h3>
</div>
<div id="tps-tabs" class="flex gap-2 overflow-x-auto pb-2 mb-4">
</div>
<div id="tps-content">
</div>
</div>
@if (TempData["Success"] != null)
{
<div class="bg-green-50 border border-green-200 text-green-700 rounded-2xl p-3 text-sm font-medium">
@TempData["Success"]
</div>
}
@if (TempData["Error"] != null)
{
<div class="bg-red-50 border border-red-200 text-red-700 rounded-2xl p-3 text-sm font-medium">
@TempData["Error"]
</div>
}
</div>
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
</div>
<register-block dynamic-section="scripts" key="jsDetailPenjemputan">
<script>
document.addEventListener('DOMContentLoaded', function() {
const grandTotalDisplay = document.getElementById('grand-total-timbangan');
const tpsSelectionContainer = document.getElementById('tps-selection-container');
const tpsTabsContainer = document.getElementById('tps-tabs-container');
const tpsCheckboxesContainer = document.getElementById('tps-checkboxes');
const btnConfirmTps = document.getElementById('btn-confirm-tps');
const tpsTabsEl = document.getElementById('tps-tabs');
const tpsContentContainer = document.getElementById('tps-content');
let activeTpsIndex = 0;
let tpsData = [];
let availableTpsList = [];
let selectedTpsList = [];
let hasRequestedLocation = [];
const OCR_AREAS = [
{ id: 'A', x: 0.34, y: 0.35, w: 0.40, h: 0.11, color: 'border-lime-400 bg-lime-500/15' },
{ id: 'B', x: 0.31, y: 0.33, w: 0.45, h: 0.14, color: 'border-amber-300 bg-amber-400/10' },
{ id: 'C', x: 0.29, y: 0.31, w: 0.49, h: 0.17, color: 'border-cyan-300 bg-cyan-400/10' }
];
function initializeLocation(tpsList) {
availableTpsList = tpsList || [];
if (availableTpsList.length === 0) {
selectedTpsList = ['Lokasi Utama'];
initializeTpsData(selectedTpsList);
tpsTabsContainer.style.display = 'block';
renderSingleForm();
return;
}
if (availableTpsList.length === 1) {
selectedTpsList = [availableTpsList[0]];
initializeTpsData(selectedTpsList);
tpsTabsContainer.style.display = 'block';
renderTabs();
renderTpsForm();
return;
}
renderTpsSelection();
tpsSelectionContainer.style.display = 'block';
}
function renderTpsSelection() {
tpsCheckboxesContainer.innerHTML = '';
availableTpsList.forEach((tpsName, index) => {
const wrapper = document.createElement('label');
wrapper.className = 'flex items-center gap-3 p-3 rounded-xl border border-gray-200 hover:bg-gray-50 cursor-pointer transition';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = tpsName;
checkbox.className = 'w-5 h-5 rounded border-gray-300 text-upst focus:ring-upst';
checkbox.checked = true;
const label = document.createElement('span');
label.className = 'text-sm font-bold text-gray-700';
label.textContent = tpsName;
wrapper.appendChild(checkbox);
wrapper.appendChild(label);
tpsCheckboxesContainer.appendChild(wrapper);
});
}
btnConfirmTps.addEventListener('click', function() {
const checkboxes = tpsCheckboxesContainer.querySelectorAll('input[type="checkbox"]:checked');
selectedTpsList = Array.from(checkboxes).map(cb => cb.value);
if (selectedTpsList.length === 0) {
alert('Pilih minimal 1 TPS untuk diangkut!');
return;
}
initializeTpsData(selectedTpsList);
tpsSelectionContainer.style.display = 'none';
tpsTabsContainer.style.display = 'block';
if (selectedTpsList.length === 1) {
renderSingleForm();
} else {
renderTabs();
renderTpsForm();
}
});
function initializeTpsData(tpsNames) {
tpsData = tpsNames.map((name, index) => ({
name: name,
index: index,
latitude: '',
longitude: '',
alamatJalan: '',
waktuKedatangan: '',
fotoKedatangan: [],
fotoKedatanganUploaded: false,
timbangan: [],
totalTimbangan: 0,
fotoPetugas: [],
fotoPetugasUploaded: false,
namaPetugas: '',
submitted: false
}));
hasRequestedLocation = new Array(tpsNames.length).fill(false);
}
function renderSingleForm() {
tpsTabsEl.style.display = 'none';
activeTpsIndex = 0;
renderTpsForm();
}
function renderTabs() {
tpsTabsEl.style.display = 'flex';
tpsTabsEl.innerHTML = '';
tpsData.forEach((tps, index) => {
const tab = document.createElement('button');
tab.type = 'button';
tab.className = `px-4 py-2 rounded-xl font-bold text-sm whitespace-nowrap transition ${
index === activeTpsIndex
? 'bg-upst text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`;
tab.textContent = tps.name;
if (tps.submitted) {
tab.innerHTML += ' <span class="text-xs">✓</span>';
}
tab.addEventListener('click', () => switchToTps(index));
tpsTabsEl.appendChild(tab);
});
}
function switchToTps(index) {
activeTpsIndex = index;
renderTabs();
renderTpsForm();
if (!hasRequestedLocation[index]) {
hasRequestedLocation[index] = true;
getLocationUpdate();
}
updateGrandTotal();
}
function renderTpsForm() {
const tps = tpsData[activeTpsIndex];
const showTpsName = selectedTpsList.length > 1 || availableTpsList.length > 0;
tpsContentContainer.innerHTML = `
<form class="space-y-5 pb-8" data-tps-index="${tps.index}">
<input type="hidden" class="tps-latitude" value="${tps.latitude}" />
<input type="hidden" class="tps-longitude" value="${tps.longitude}" />
<input type="hidden" class="tps-alamat-jalan" value="${tps.alamatJalan}" />
<input type="hidden" class="tps-total-timbangan" value="${tps.totalTimbangan}" />
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">1</div>
<div>
<h3 class="font-black text-gray-800">Foto Kedatangan${showTpsName ? ' - ' + tps.name : ''}</h3>
<p class="text-xs text-gray-500">Upload foto kedatangan</p>
</div>
</div>
<label class="block text-xs font-semibold text-gray-600">Upload Foto Kedatangan</label>
<input type="file" class="tps-foto-kedatangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" accept="image/*" multiple />
<div class="tps-preview-kedatangan space-y-2"></div>
${tps.fotoKedatangan.length > 0 && !tps.fotoKedatanganUploaded ? `
<button type="button" class="tps-btn-upload-kedatangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">
Upload ${tps.fotoKedatangan.length} Foto Kedatangan
</button>
` : tps.fotoKedatanganUploaded ? `
<div class="text-center text-xs text-green-600 font-bold py-2">
✓ Foto kedatangan sudah diupload
</div>
` : ''}
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Latitude</label>
<input type="text" class="tps-display-latitude w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.latitude}" />
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Longitude</label>
<input type="text" class="tps-display-longitude w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.longitude}" />
</div>
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Waktu Kedatangan</label>
<input type="text" class="tps-waktu-kedatangan w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.waktuKedatangan}" />
</div>
</section>
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">2</div>
<div>
<h3 class="font-black text-gray-800">Foto Timbang Sampah</h3>
<p class="text-xs text-gray-500">Upload foto timbangan, berat auto terisi</p>
</div>
</div>
<div class="tps-timbangan-repeater space-y-3"></div>
<button type="button" class="tps-btn-add-timbangan w-full border border-dashed border-upst text-upst rounded-xl py-2 text-xs font-bold transition">
+ Tambah Foto Timbangan
</button>
<div class="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2 flex items-center justify-between">
<span class="text-xs font-semibold text-gray-600">Total Timbangan${showTpsName ? ' ' + tps.name : ''}</span>
<span class="text-base font-black text-upst"><span class="tps-display-total">${formatWeightDisplay(tps.totalTimbangan)}</span> kg</span>
</div>
</section>
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">3</div>
<div>
<h3 class="font-black text-gray-800">Foto Petugas</h3>
<p class="text-xs text-gray-500">Upload dokumentasi petugas</p>
</div>
</div>
<label class="block text-xs font-semibold text-gray-600">Upload Foto Petugas</label>
<input type="file" class="tps-foto-petugas block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" accept="image/*" multiple />
<div class="tps-preview-petugas space-y-2"></div>
${tps.fotoPetugas.length > 0 && !tps.fotoPetugasUploaded ? `
<button type="button" class="tps-btn-upload-petugas w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">
Upload ${tps.fotoPetugas.length} Foto Petugas
</button>
` : tps.fotoPetugasUploaded ? `
<div class="text-center text-xs text-green-600 font-bold py-2">
✓ Foto petugas sudah diupload
</div>
` : ''}
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Nama Petugas</label>
<input type="text" class="tps-nama-petugas w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Masukkan nama petugas" value="${tps.namaPetugas}" />
</div>
</section>
<div class="flex gap-3">
<a href="@Url.Action("Batal", "DetailPenjemputan")" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
<button type="submit" class="w-2/3 bg-upst text-white py-3 rounded-xl font-bold text-sm hover:brightness-110">Submit${showTpsName ? ' ' + tps.name : ''}</button>
</div>
</form>
`;
attachTpsFormListeners();
restoreTpsTimbanganItems();
restorePhotoPreview();
}
function restorePhotoPreview() {
const tps = tpsData[activeTpsIndex];
const form = tpsContentContainer.querySelector('form');
if (!form) return;
const previewKedatangan = form.querySelector('.tps-preview-kedatangan');
if (previewKedatangan && tps.fotoKedatangan.length > 0) {
renderStoredPhotos(tps.fotoKedatangan, previewKedatangan);
}
const previewPetugas = form.querySelector('.tps-preview-petugas');
if (previewPetugas && tps.fotoPetugas.length > 0) {
renderStoredPhotos(tps.fotoPetugas, previewPetugas);
}
}
function renderStoredPhotos(files, container) {
container.innerHTML = '';
container.className = 'space-y-2';
files.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
const imageUrl = URL.createObjectURL(file);
const safeName = file.name.replace(/"/g, '&quot;');
item.innerHTML = `
<div class="h-44 bg-black/80">
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
</div>
<div class="px-2 py-1 bg-white">
<p class="text-[11px] font-semibold text-gray-700 truncate">${index + 1}. ${safeName}</p>
<p class="text-[10px] text-gray-500">${formatFileSize(file.size)}</p>
</div>
`;
const img = item.querySelector('.preview-multi-image');
if (img) {
img.onload = function() {
URL.revokeObjectURL(imageUrl);
};
}
container.appendChild(item);
});
}
function attachTpsFormListeners() {
const form = tpsContentContainer.querySelector('form');
const tps = tpsData[activeTpsIndex];
const fotoKedatanganInput = form.querySelector('.tps-foto-kedatangan');
const fotoPetugasInput = form.querySelector('.tps-foto-petugas');
const namaPetugasInput = form.querySelector('.tps-nama-petugas');
const btnAddTimbangan = form.querySelector('.tps-btn-add-timbangan');
const toggleDebug = form.querySelector('.tps-toggle-debug-crop');
fotoKedatanganInput.addEventListener('change', function() {
tps.fotoKedatangan = Array.from(this.files);
tps.fotoKedatanganUploaded = false;
updateWaktuKedatangan();
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
renderTpsForm();
});
fotoPetugasInput.addEventListener('change', function() {
tps.fotoPetugas = Array.from(this.files);
tps.fotoPetugasUploaded = false;
updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
renderTpsForm();
});
namaPetugasInput.addEventListener('input', function() {
tps.namaPetugas = this.value;
});
btnAddTimbangan.addEventListener('click', function() {
createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
});
const btnUploadKedatangan = form.querySelector('.tps-btn-upload-kedatangan');
if (btnUploadKedatangan) {
btnUploadKedatangan.addEventListener('click', function() {
uploadFotoKedatangan();
});
}
const btnUploadPetugas = form.querySelector('.tps-btn-upload-petugas');
if (btnUploadPetugas) {
btnUploadPetugas.addEventListener('click', function() {
uploadFotoPetugas();
});
}
form.addEventListener('submit', function(e) {
e.preventDefault();
submitTpsData();
});
}
function restoreTpsTimbanganItems() {
const tps = tpsData[activeTpsIndex];
const form = tpsContentContainer.querySelector('form');
const repeater = form.querySelector('.tps-timbangan-repeater');
if (tps.timbangan.length === 0) {
createTimbanganItem(repeater);
} else {
tps.timbangan.forEach(timb => {
const item = createTimbanganItem(repeater, timb);
});
}
}
function updateWaktuKedatangan() {
const tps = tpsData[activeTpsIndex];
const now = new Date();
const formatted = now.toLocaleString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
tps.waktuKedatangan = formatted;
const form = tpsContentContainer.querySelector('form');
const displayWaktu = form.querySelector('.tps-waktu-kedatangan');
if (displayWaktu) displayWaktu.value = formatted;
getLocationUpdate();
}
function reverseGeocode(lat, lng) {
const tps = tpsData[activeTpsIndex];
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
.then(res => res.json())
.then(data => {
const address = data.display_name || `${lat}, ${lng}`;
tps.latitude = lat;
tps.longitude = lng;
tps.alamatJalan = address;
const form = tpsContentContainer.querySelector('form');
if (form) {
const latInput = form.querySelector('.tps-display-latitude');
const lngInput = form.querySelector('.tps-display-longitude');
if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng;
}
})
.catch(() => {
tps.latitude = lat;
tps.longitude = lng;
tps.alamatJalan = `${lat}, ${lng}`;
const form = tpsContentContainer.querySelector('form');
if (form) {
const latInput = form.querySelector('.tps-display-latitude');
const lngInput = form.querySelector('.tps-display-longitude');
if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng;
}
});
}
function getLocationUpdate() {
if (!('geolocation' in navigator)) return;
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude.toFixed(6);
const lng = position.coords.longitude.toFixed(6);
reverseGeocode(lat, lng);
},
function() {
console.log('Lokasi tidak diizinkan');
}
);
}
function formatFileSize(bytes) {
const mb = bytes / (1024 * 1024);
return `${mb.toFixed(2)} MB`;
}
function updateMultiPreview(input, previewContainer) {
if (!input || !previewContainer) return;
previewContainer.innerHTML = '';
previewContainer.className = 'space-y-2';
if (!input.files || input.files.length === 0) return;
Array.from(input.files).forEach((file, index) => {
const item = document.createElement('div');
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
const imageUrl = URL.createObjectURL(file);
const safeName = file.name.replace(/"/g, '&quot;');
item.innerHTML = `
<div class="h-44 bg-black/80">
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
</div>
<div class="px-2 py-1 bg-white">
<p class="text-[11px] font-semibold text-gray-700 truncate">${index + 1}. ${safeName}</p>
<p class="text-[10px] text-gray-500">${formatFileSize(file.size)}</p>
</div>
`;
const img = item.querySelector('.preview-multi-image');
if (img) {
img.onload = function() {
URL.revokeObjectURL(imageUrl);
};
}
previewContainer.appendChild(item);
});
}
function formatWeightDisplay(value) {
if (isNaN(value)) return '0,00';
return value.toFixed(2).replace('.', ',');
}
function parseWeightInput(value) {
if (!value) return 0;
const cleaned = value.toString().trim().replace(/\s/g, '').replace(',', '.');
const parsed = parseFloat(cleaned);
return isNaN(parsed) ? 0 : parsed;
}
function readFileAsImage(file) {
return new Promise(function(resolve, reject) {
const objectUrl = URL.createObjectURL(file);
const img = new Image();
img.onload = function() {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = function() {
URL.revokeObjectURL(objectUrl);
reject(new Error('Gagal membaca gambar.'));
};
img.src = objectUrl;
});
}
function createCropCanvas(img, area) {
const sx = Math.max(0, Math.floor(img.width * area.x));
const sy = Math.max(0, Math.floor(img.height * area.y));
const sw = Math.max(1, Math.floor(img.width * area.w));
const sh = Math.max(1, Math.floor(img.height * area.h));
const canvas = document.createElement('canvas');
const scale = 2.2;
canvas.width = Math.max(1, Math.floor(sw * scale));
canvas.height = Math.max(1, Math.floor(sh * scale));
const ctx = canvas.getContext('2d');
if (!ctx) return canvas;
ctx.imageSmoothingEnabled = true;
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
return canvas;
}
function canvasToJpegFile(canvas, fileName) {
return new Promise(function(resolve, reject) {
canvas.toBlob(function(blob) {
if (!blob) {
reject(new Error('Gagal membuat crop image.'));
return;
}
resolve(new File([blob], fileName, { type: 'image/jpeg' }));
}, 'image/jpeg', 0.92);
});
}
async function requestOpenRouterWeight(imageFile) {
const formData = new FormData();
formData.append('Foto', imageFile);
const response = await fetch('/upst/detail-penjemputan/ocr-timbangan', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Request OCR gagal.');
}
return result;
}
async function autoFillWeight(file, weightInput, ocrInfoEl) {
let guessedWeight = 0;
weightInput.placeholder = 'Membaca angka dari foto...';
if (ocrInfoEl) ocrInfoEl.textContent = 'AI OCR: memproses gambar...';
try {
const img = await readFileAsImage(file);
let bestRawText = '';
let isSuccess = false;
for (const area of OCR_AREAS) {
const cropCanvas = createCropCanvas(img, area);
const cropFile = await canvasToJpegFile(cropCanvas, `crop-${area.id}.jpg`);
const aiResult = await requestOpenRouterWeight(cropFile);
if (aiResult && aiResult.success && aiResult.weight) {
guessedWeight = parseWeightInput(aiResult.weight);
bestRawText = aiResult.raw || aiResult.weight;
isSuccess = guessedWeight > 0;
if (isSuccess) break;
}
if (aiResult && aiResult.raw) {
bestRawText = aiResult.raw;
}
}
if (ocrInfoEl) {
const cleaned = (bestRawText || '').replace(/\s+/g, ' ').trim();
ocrInfoEl.textContent = isSuccess
? `AI OCR terbaca: ${cleaned}`
: (cleaned ? `AI OCR tidak valid: ${cleaned}` : 'AI OCR tidak menemukan angka valid.');
}
} catch (_) {
guessedWeight = 0;
if (ocrInfoEl) ocrInfoEl.textContent = 'AI OCR gagal diproses.';
}
if (guessedWeight > 0) {
weightInput.value = formatWeightDisplay(guessedWeight);
weightInput.placeholder = 'Berat terdeteksi otomatis';
} else {
weightInput.placeholder = 'Tidak terbaca otomatis, isi manual';
}
updateTpsTotalTimbangan();
}
function updateTpsTotalTimbangan() {
const tps = tpsData[activeTpsIndex];
const form = tpsContentContainer.querySelector('form');
if (!form) return;
let total = 0.0;
const repeater = form.querySelector('.tps-timbangan-repeater');
const items = repeater.querySelectorAll('.timbangan-item');
items.forEach(function(item) {
const weightInput = item.querySelector('.input-berat-timbangan-value');
if (weightInput) {
const value = parseWeightInput(weightInput.value || '0');
total += value;
}
});
tps.totalTimbangan = total;
const displayTotal = form.querySelector('.tps-display-total');
if (displayTotal) displayTotal.textContent = formatWeightDisplay(total);
updateGrandTotal();
}
function updateGrandTotal() {
let grandTotal = 0;
tpsData.forEach(tps => {
grandTotal += tps.totalTimbangan;
});
if (grandTotalDisplay) {
grandTotalDisplay.textContent = formatWeightDisplay(grandTotal);
}
}
function createTimbanganItem(repeater, existingData = null) {
const item = document.createElement('div');
item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50';
const weight = existingData ? existingData.weight : 0;
const hasFile = existingData && existingData.file;
const isUploaded = existingData && existingData.uploaded;
item.innerHTML = `
<div class="flex items-center justify-between">
<p class="text-xs font-bold text-gray-600">Item Timbangan</p>
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
</div>
<input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
<div class="${hasFile ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
<div class="input-crop-overlay absolute inset-0 pointer-events-none"></div>
</div>
<p class="text-[11px] text-gray-500 input-ocr-info">${hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'}</p>
${hasFile && !isUploaded ? `
<button type="button" class="btn-upload-timbangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">
Upload Foto Timbangan Ini
</button>
` : isUploaded ? `
<div class="text-center text-xs text-green-600 font-bold py-2">
✓ Foto timbangan sudah diupload
</div>
` : ''}
<div class="grid grid-cols-1 gap-2">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ''}" />
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
</div>
</div>
`;
const fileInput = item.querySelector('.input-foto-timbangan');
const previewWrap = item.querySelector('.input-preview-wrap');
const previewImage = item.querySelector('.input-preview-image');
const cropOverlay = item.querySelector('.input-crop-overlay');
const ocrInfoEl = item.querySelector('.input-ocr-info');
const weightInputDisplay = item.querySelector('.input-berat-timbangan-display');
const weightInputValue = item.querySelector('.input-berat-timbangan-value');
const removeBtn = item.querySelector('.btn-remove-timbangan');
if (existingData && existingData.file) {
const localUrl = URL.createObjectURL(existingData.file);
previewImage.src = localUrl;
previewImage.onload = function() {
URL.revokeObjectURL(localUrl);
};
}
fileInput.addEventListener('change', async function() {
if (fileInput.files && fileInput.files[0]) {
const localUrl = URL.createObjectURL(fileInput.files[0]);
previewImage.src = localUrl;
previewWrap.classList.remove('hidden');
previewImage.onload = function() {
URL.revokeObjectURL(localUrl);
};
await autoFillWeight(fileInput.files[0], weightInputDisplay, ocrInfoEl);
const parsed = parseWeightInput(weightInputDisplay.value);
weightInputValue.value = parsed.toFixed(2);
updateTpsTotalTimbangan();
syncTimbanganToTpsData();
const tps = tpsData[activeTpsIndex];
const itemIndex = Array.from(repeater.children).indexOf(item);
if (itemIndex >= 0 && tps.timbangan[itemIndex]) {
tps.timbangan[itemIndex].uploaded = false;
const existingUploadBtn = item.querySelector('.btn-upload-timbangan');
if (!existingUploadBtn) {
const ocrInfo = item.querySelector('.input-ocr-info');
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn-upload-timbangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110';
uploadBtn.textContent = 'Upload Foto Timbangan Ini';
uploadBtn.addEventListener('click', function() {
uploadSingleFotoTimbangan(itemIndex);
});
ocrInfo.parentNode.insertBefore(uploadBtn, ocrInfo.nextSibling);
}
}
}
});
weightInputDisplay.addEventListener('input', function() {
const cleaned = this.value.replace(/[^0-9.,]/g, '');
this.value = cleaned;
const parsed = parseWeightInput(cleaned);
weightInputValue.value = parsed.toFixed(2);
updateTpsTotalTimbangan();
syncTimbanganToTpsData();
});
weightInputDisplay.addEventListener('blur', function() {
const parsed = parseWeightInput(this.value);
if (parsed > 0) {
this.value = formatWeightDisplay(parsed);
weightInputValue.value = parsed.toFixed(2);
} else {
this.value = '';
weightInputValue.value = '0.00';
}
updateTpsTotalTimbangan();
syncTimbanganToTpsData();
});
removeBtn.addEventListener('click', function() {
item.remove();
const form = tpsContentContainer.querySelector('form');
const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null;
if (repeater && repeater.children.length === 0) {
createTimbanganItem(repeater);
}
updateTpsTotalTimbangan();
syncTimbanganToTpsData();
});
const btnUploadTimbangan = item.querySelector('.btn-upload-timbangan');
if (btnUploadTimbangan) {
btnUploadTimbangan.addEventListener('click', function() {
const itemIndex = Array.from(repeater.children).indexOf(item);
uploadSingleFotoTimbangan(itemIndex);
});
}
repeater.appendChild(item);
return item;
}
function syncTimbanganToTpsData() {
const tps = tpsData[activeTpsIndex];
const form = tpsContentContainer.querySelector('form');
if (!form) return;
const repeater = form.querySelector('.tps-timbangan-repeater');
const items = repeater.querySelectorAll('.timbangan-item');
tps.timbangan = [];
items.forEach(item => {
const fileInput = item.querySelector('.input-foto-timbangan');
const weightValue = item.querySelector('.input-berat-timbangan-value');
const existingIndex = tps.timbangan.length;
const existingData = tps.timbangan[existingIndex];
tps.timbangan.push({
file: fileInput.files[0] || (existingData ? existingData.file : null),
weight: parseWeightInput(weightValue.value),
uploaded: existingData ? existingData.uploaded : false
});
});
}
function uploadSingleFotoTimbangan(itemIndex) {
const tps = tpsData[activeTpsIndex];
if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) {
alert('Belum ada foto timbangan yang dipilih!');
return;
}
const timbanganItem = tps.timbangan[itemIndex];
alert(`Upload foto timbangan #${itemIndex + 1} untuk ${tps.name}\nBerat: ${timbanganItem.weight} kg\n(Implementasi upload ke server)`);
timbanganItem.uploaded = true;
const form = tpsContentContainer.querySelector('form');
const repeater = form.querySelector('.tps-timbangan-repeater');
const items = repeater.querySelectorAll('.timbangan-item');
const targetItem = items[itemIndex];
if (targetItem) {
const uploadBtn = targetItem.querySelector('.btn-upload-timbangan');
if (uploadBtn) {
uploadBtn.remove();
}
const ocrInfo = targetItem.querySelector('.input-ocr-info');
if (ocrInfo && !targetItem.querySelector('.upload-success-message')) {
const successMsg = document.createElement('div');
successMsg.className = 'text-center text-xs text-green-600 font-bold py-2 upload-success-message';
successMsg.textContent = '✓ Foto timbangan sudah diupload';
ocrInfo.parentNode.insertBefore(successMsg, ocrInfo.nextSibling);
}
}
}
function uploadFotoKedatangan() {
const tps = tpsData[activeTpsIndex];
if (tps.fotoKedatangan.length === 0) {
alert('Belum ada foto kedatangan yang dipilih!');
return;
}
alert(`Upload ${tps.fotoKedatangan.length} foto kedatangan untuk ${tps.name}\n(Implementasi upload ke server)`);
tps.fotoKedatanganUploaded = true;
renderTpsForm();
}
function uploadFotoPetugas() {
const tps = tpsData[activeTpsIndex];
if (tps.fotoPetugas.length === 0) {
alert('Belum ada foto petugas yang dipilih!');
return;
}
alert(`Upload ${tps.fotoPetugas.length} foto petugas untuk ${tps.name}\n(Implementasi upload ke server)`);
tps.fotoPetugasUploaded = true;
renderTpsForm();
}
function submitTpsData() {
const tps = tpsData[activeTpsIndex];
if (!tps.fotoKedatangan.length) {
alert('Foto kedatangan belum diupload!');
return;
}
if (!tps.timbangan.length) {
alert('Belum ada data timbangan!');
return;
}
if (!tps.fotoPetugas.length) {
alert('Foto petugas belum diupload!');
return;
}
if (!tps.namaPetugas.trim()) {
alert('Nama petugas belum diisi!');
return;
}
const formData = new FormData();
formData.append('TpsName', tps.name);
formData.append('Latitude', tps.latitude);
formData.append('Longitude', tps.longitude);
formData.append('AlamatJalan', tps.alamatJalan);
formData.append('WaktuKedatangan', tps.waktuKedatangan);
formData.append('TotalTimbangan', tps.totalTimbangan);
formData.append('NamaPetugas', tps.namaPetugas);
tps.fotoKedatangan.forEach((file, i) => {
formData.append(`FotoKedatangan`, file);
});
tps.timbangan.forEach((timb, i) => {
if (timb.file) formData.append(`FotoTimbangan`, timb.file);
formData.append(`BeratTimbangan`, timb.weight);
});
tps.fotoPetugas.forEach((file, i) => {
formData.append(`FotoPetugas`, file);
});
alert(`Submit data ${tps.name}:\n- Lokasi: ${tps.alamatJalan}\n- Total: ${tps.totalTimbangan} kg\n- Petugas: ${tps.namaPetugas}\n\n(Implementasi POST ke server)`);
tps.submitted = true;
if (selectedTpsList.length > 1) {
renderTabs();
}
}
initializeLocation(['TPS A', 'TPS B', 'TPS C']);
});
</script>
</register-block>