const DetailPenjemputan = (function() { 'use strict'; const CONFIG = { 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' } ], JENIS_SAMPAH: ['Organik', 'Anorganik', 'Residu'], DEFAULT_JENIS: 'Residu' }; let state = { activeTpsIndex: 0, tpsData: [], availableTpsList: [], selectedTpsList: [], hasRequestedLocation: [], nomorSpj: 'SPJ/07-2025/PKM/000476' }; const elements = { grandTotalDisplay: null, tpsSelectionContainer: null, tpsTabsContainer: null, tpsCheckboxesContainer: null, btnConfirmTps: null, tpsTabsEl: null, tpsContentContainer: null, totalOrganikDisplay: null, totalAnorganikDisplay: null, totalResiduDisplay: null }; function init(tpsList) { initElements(); initializeLocation(tpsList); } function initElements() { elements.grandTotalDisplay = document.getElementById('grand-total-timbangan'); elements.tpsSelectionContainer = document.getElementById('tps-selection-container'); elements.tpsTabsContainer = document.getElementById('tps-tabs-container'); elements.tpsCheckboxesContainer = document.getElementById('tps-checkboxes'); elements.btnConfirmTps = document.getElementById('btn-confirm-tps'); elements.tpsTabsEl = document.getElementById('tps-tabs'); elements.tpsContentContainer = document.getElementById('tps-content'); elements.totalOrganikDisplay = document.getElementById('grand-total-organik'); elements.totalAnorganikDisplay = document.getElementById('grand-total-anorganik'); elements.totalResiduDisplay = document.getElementById('grand-total-residu'); if (elements.btnConfirmTps) { elements.btnConfirmTps.addEventListener('click', handleConfirmTps); } } function initializeLocation(tpsList) { state.availableTpsList = tpsList || []; if (state.availableTpsList.length === 0) { state.selectedTpsList = ['1 Lokasi TPS']; initializeTpsData(state.selectedTpsList); elements.tpsTabsContainer.style.display = 'block'; renderSingleForm(); return; } if (state.availableTpsList.length === 1) { state.selectedTpsList = [state.availableTpsList[0]]; initializeTpsData(state.selectedTpsList); elements.tpsTabsContainer.style.display = 'block'; renderTabs(); renderTpsForm(); return; } renderTpsSelection(); elements.tpsSelectionContainer.style.display = 'block'; } function initializeTpsData(tpsNames) { state.tpsData = tpsNames.map((name, index) => ({ name: name, index: index, latitude: '', longitude: '', alamatJalan: '', waktuKedatangan: '', fotoKedatangan: [], fotoKedatanganUploaded: false, timbangan: [], totalOrganik: 0, totalAnorganik: 0, totalResidu: 0, totalTimbangan: 0, fotoPetugas: [], fotoPetugasUploaded: false, namaPetugas: '', submitted: false })); state.hasRequestedLocation = new Array(tpsNames.length).fill(false); } function renderTpsSelection() { elements.tpsCheckboxesContainer.innerHTML = ''; state.availableTpsList.forEach((tpsName) => { 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); elements.tpsCheckboxesContainer.appendChild(wrapper); }); } function handleConfirmTps() { const checkboxes = elements.tpsCheckboxesContainer.querySelectorAll('input[type="checkbox"]:checked'); state.selectedTpsList = Array.from(checkboxes).map(cb => cb.value); if (state.selectedTpsList.length === 0) { alert('Pilih minimal 1 TPS untuk diangkut!'); return; } initializeTpsData(state.selectedTpsList); elements.tpsSelectionContainer.style.display = 'none'; elements.tpsTabsContainer.style.display = 'block'; if (state.selectedTpsList.length === 1) { renderSingleForm(); } else { renderTabs(); renderTpsForm(); } } function renderSingleForm() { elements.tpsTabsEl.style.display = 'none'; state.activeTpsIndex = 0; renderTpsForm(); } function renderTabs() { elements.tpsTabsEl.style.display = 'flex'; elements.tpsTabsEl.innerHTML = ''; state.tpsData.forEach((tps, index) => { const tab = document.createElement('button'); tab.type = 'button'; const isActive = index === state.activeTpsIndex; const isSubmitted = !!tps.submitted; let tabClass = 'bg-gray-100 text-gray-600 hover:bg-gray-200'; if (isSubmitted) { tabClass = isActive ? 'bg-green-600 text-white' : 'bg-green-100 text-green-700 hover:bg-green-200'; } else if (isActive) { tabClass = 'bg-upst text-white'; } tab.className = `px-4 py-2 rounded-xl font-bold text-sm whitespace-nowrap transition ${tabClass}`; tab.textContent = tps.name; if (tps.submitted) { tab.innerHTML += ' '; } tab.addEventListener('click', () => switchToTps(index)); elements.tpsTabsEl.appendChild(tab); }); } function switchToTps(index) { state.activeTpsIndex = index; renderTabs(); renderTpsForm(); if (!state.hasRequestedLocation[index]) { state.hasRequestedLocation[index] = true; getLocationUpdate(); } updateAllTotals(); } function renderTpsForm() { const tps = state.tpsData[state.activeTpsIndex]; const showTpsName = state.selectedTpsList.length > 1 || state.availableTpsList.length > 0; elements.tpsContentContainer.innerHTML = `
${renderSection1Kedatangan(tps, showTpsName)} ${renderSection2Timbangan(tps, showTpsName)} ${renderSection3Petugas(tps)}
Batal
`; attachTpsFormListeners(); restoreTpsTimbanganItems(); restorePhotoPreview(); } function renderSection1Kedatangan(tps, showTpsName) { return `
1

Foto Kedatangan${showTpsName ? ' - ' + tps.name : ''}

Upload foto kedatangan

${tps.fotoKedatangan.length > 0 && !tps.fotoKedatanganUploaded ? ` ` : tps.fotoKedatanganUploaded ? `
✓ Foto kedatangan sudah diupload
` : ''}
`; } function renderSection2Timbangan(tps, showTpsName) { return `
2

Foto Timbang Sampah

Upload foto timbangan, berat auto terisi

`; } function renderSection3Petugas(tps) { return `
3

Foto Petugas

Upload dokumentasi petugas

${tps.fotoPetugas.length > 0 && !tps.fotoPetugasUploaded ? ` ` : tps.fotoPetugasUploaded ? `
✓ Foto petugas sudah diupload
` : ''}
`; } function attachTpsFormListeners() { const form = elements.tpsContentContainer.querySelector('form'); const tps = state.tpsData[state.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'); 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', uploadFotoKedatangan); } const btnUploadPetugas = form.querySelector('.tps-btn-upload-petugas'); if (btnUploadPetugas) { btnUploadPetugas.addEventListener('click', uploadFotoPetugas); } form.addEventListener('submit', function(e) { e.preventDefault(); submitTpsData(); }); } function restoreTpsTimbanganItems() { const tps = state.tpsData[state.activeTpsIndex]; const form = elements.tpsContentContainer.querySelector('form'); const repeater = form.querySelector('.tps-timbangan-repeater'); if (tps.timbangan.length === 0) { createTimbanganItem(repeater); } else { tps.timbangan.forEach(timb => { createTimbanganItem(repeater, timb); }); } } function restorePhotoPreview() { const tps = state.tpsData[state.activeTpsIndex]; const form = elements.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, '"'); item.innerHTML = `
Preview ${index + 1}

${index + 1}. ${safeName}

${formatFileSize(file.size)}

`; const img = item.querySelector('.preview-multi-image'); if (img) { img.onload = function() { URL.revokeObjectURL(imageUrl); }; } container.appendChild(item); }); } function updateWaktuKedatangan() { const tps = state.tpsData[state.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 = elements.tpsContentContainer.querySelector('form'); const displayWaktu = form.querySelector('.tps-waktu-kedatangan'); if (displayWaktu) displayWaktu.value = formatted; getLocationUpdate(); } 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 reverseGeocode(lat, lng) { const tps = state.tpsData[state.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}`; updateTpsLocation(lat, lng, address); }) .catch(() => { updateTpsLocation(lat, lng, `${lat}, ${lng}`); }); } function updateTpsLocation(lat, lng, address) { const tps = state.tpsData[state.activeTpsIndex]; tps.latitude = lat; tps.longitude = lng; tps.alamatJalan = address; const form = elements.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 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, '"'); item.innerHTML = `
Preview ${index + 1}

${index + 1}. ${safeName}

${formatFileSize(file.size)}

`; const img = item.querySelector('.preview-multi-image'); if (img) { img.onload = function() { URL.revokeObjectURL(imageUrl); }; } previewContainer.appendChild(item); }); } function createTimbanganItem(repeater, existingData = null) { const photoNumber = repeater.children.length + 1; const item = document.createElement('div'); item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50'; item.dataset.photoNumber = photoNumber; const weight = existingData ? existingData.weight : 0; const jenisSampah = existingData ? existingData.jenisSampah : CONFIG.DEFAULT_JENIS; const hasFile = existingData && existingData.file; const isUploaded = existingData && existingData.uploaded; item.innerHTML = `

Item Timbangan #${photoNumber}

Preview foto timbangan

${hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'}

${hasFile && !isUploaded ? ` ` : isUploaded ? `
✓ Foto timbangan sudah diupload
` : ''}
`; const fileInput = item.querySelector('.input-foto-timbangan'); const previewWrap = item.querySelector('.input-preview-wrap'); const previewImage = item.querySelector('.input-preview-image'); const ocrInfoEl = item.querySelector('.input-ocr-info'); const weightInputDisplay = item.querySelector('.input-berat-timbangan-display'); const weightInputValue = item.querySelector('.input-berat-timbangan-value'); const jenisSampahSelect = item.querySelector('.input-jenis-sampah'); 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 originalFile = fileInput.files[0]; const watermarkedFile = await applyWatermark(originalFile, photoNumber); const dataTransfer = new DataTransfer(); dataTransfer.items.add(watermarkedFile); fileInput.files = dataTransfer.files; const localUrl = URL.createObjectURL(watermarkedFile); previewImage.src = localUrl; previewWrap.classList.remove('hidden'); previewImage.onload = function() { URL.revokeObjectURL(localUrl); }; await autoFillWeight(watermarkedFile, weightInputDisplay, ocrInfoEl); const parsed = parseWeightInput(weightInputDisplay.value); weightInputValue.value = parsed.toFixed(2); updateTpsTotalTimbangan(); syncTimbanganToTpsData(); const tps = state.tpsData[state.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(); }); jenisSampahSelect.addEventListener('change', function() { updateTpsTotalTimbangan(); syncTimbanganToTpsData(); }); removeBtn.addEventListener('click', function() { item.remove(); const form = elements.tpsContentContainer.querySelector('form'); const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null; if (repeater) { renumberTimbanganItems(repeater); if (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 renumberTimbanganItems(repeater) { const items = repeater.querySelectorAll('.timbangan-item'); items.forEach((item, index) => { const newNumber = index + 1; item.dataset.photoNumber = newNumber; const label = item.querySelector('.text-xs.font-bold.text-gray-600'); if (label) { label.textContent = `Item Timbangan #${newNumber}`; } }); } async function applyWatermark(file, photoNumber) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { const img = new Image(); img.onload = function() { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const now = new Date(); const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; const dayName = days[now.getDay()]; const date = now.getDate().toString().padStart(2, '0'); const month = months[now.getMonth()]; const year = now.getFullYear(); const hours = now.getHours().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); const timestamp = `${dayName}, ${date} ${month} ${year} • ${hours}:${minutes}:${seconds}`; const baseFontSize = Math.max(16, Math.min(img.width, img.height) * 0.025); const fontFamily = "'Montserrat', 'Segoe UI', 'Roboto', sans-serif"; const lines = [ { text: `FOTO TIMBANG #${photoNumber}`, size: baseFontSize * 1.1, weight: '900', color: '#FFD700' }, { text: `${state.nomorSpj}`, size: baseFontSize * 0.9, weight: '700', color: '#FFFFFF' }, { text: timestamp, size: baseFontSize * 0.75, weight: '500', color: '#E2E8F0' } ]; const paddingX = baseFontSize * 1.2; const paddingY = baseFontSize * 1.0; const lineGap = baseFontSize * 0.4; let maxWidth = 0; let totalHeight = 0; lines.forEach(line => { ctx.font = `${line.weight} ${line.size}px ${fontFamily}`; const metrics = ctx.measureText(line.text); maxWidth = Math.max(maxWidth, metrics.width); totalHeight += line.size + lineGap; }); totalHeight -= lineGap; const margin = baseFontSize * 1.5; const boxWidth = maxWidth + (paddingX * 2); const boxHeight = totalHeight + (paddingY * 2); const boxX = img.width - boxWidth - margin; const boxY = img.height - boxHeight - margin; ctx.save(); ctx.beginPath(); if (ctx.roundRect) { ctx.roundRect(boxX, boxY, boxWidth, boxHeight, baseFontSize * 0.8); } else { ctx.rect(boxX, boxY, boxWidth, boxHeight); } ctx.fillStyle = 'rgba(15, 23, 42, 0.85)'; ctx.fill(); ctx.beginPath(); const accentWidth = baseFontSize * 0.3; if (ctx.roundRect) { ctx.roundRect(boxX + boxWidth - accentWidth, boxY, accentWidth, boxHeight, [0, baseFontSize * 0.8, baseFontSize * 0.8, 0]); } else { ctx.rect(boxX + boxWidth - accentWidth, boxY, accentWidth, boxHeight); } ctx.fillStyle = '#FFD700'; ctx.fill(); ctx.restore(); ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; ctx.textAlign = 'right'; ctx.textBaseline = 'top'; let currentY = boxY + paddingY; const textRightLimit = boxX + boxWidth - paddingX - accentWidth; lines.forEach(line => { ctx.font = `${line.weight} ${line.size}px ${fontFamily}`; ctx.fillStyle = line.color; ctx.fillText(line.text, textRightLimit, currentY); currentY += line.size + lineGap; }); ctx.shadowColor = 'transparent'; canvas.toBlob(function(blob) { const watermarkedFile = new File([blob], file.name, { type: 'image/jpeg', lastModified: Date.now() }); resolve(watermarkedFile); }, 'image/jpeg', 0.95); }; img.onerror = reject; img.src = e.target.result; }; reader.onerror = reject; reader.readAsDataURL(file); }); } async function autoFillWeight(file, weightInput, ocrInfoEl) { let guessedWeight = 0; weightInput.placeholder = 'Membaca angka dari foto...'; if (ocrInfoEl) ocrInfoEl.textContent = 'AI: memproses gambar...'; try { const img = await readFileAsImage(file); let bestRawText = ''; let isSuccess = false; for (const area of CONFIG.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 terbaca: ${cleaned}` : (cleaned ? `AI tidak valid: ${cleaned}` : 'AI tidak menemukan angka valid.'); } } catch (_) { guessedWeight = 0; if (ocrInfoEl) ocrInfoEl.textContent = 'AI gagal diproses.'; } if (guessedWeight > 0) { weightInput.value = formatWeightDisplay(guessedWeight); weightInput.placeholder = 'Berat terdeteksi otomatis'; } else { weightInput.placeholder = 'Tidak terbaca otomatis, isi manual'; } updateTpsTotalTimbangan(); } 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; } function updateTpsTotalTimbangan() { const tps = state.tpsData[state.activeTpsIndex]; const form = elements.tpsContentContainer.querySelector('form'); if (!form) return; let totalOrganik = 0.0; let totalAnorganik = 0.0; let totalResidu = 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'); const jenisSampahSelect = item.querySelector('.input-jenis-sampah'); if (weightInput && jenisSampahSelect) { const value = parseWeightInput(weightInput.value || '0'); const jenis = jenisSampahSelect.value; if (jenis === 'Organik') { totalOrganik += value; } else if (jenis === 'Anorganik') { totalAnorganik += value; } else if (jenis === 'Residu') { totalResidu += value; } } }); tps.totalOrganik = totalOrganik; tps.totalAnorganik = totalAnorganik; tps.totalResidu = totalResidu; tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu; const displayTotalOrganik = form.querySelector('.tps-display-total-organik'); const displayTotalAnorganik = form.querySelector('.tps-display-total-anorganik'); const displayTotalResidu = form.querySelector('.tps-display-total-residu'); const displayTotal = form.querySelector('.tps-display-total'); if (displayTotalOrganik) displayTotalOrganik.textContent = formatWeightDisplay(totalOrganik); if (displayTotalAnorganik) displayTotalAnorganik.textContent = formatWeightDisplay(totalAnorganik); if (displayTotalResidu) displayTotalResidu.textContent = formatWeightDisplay(totalResidu); if (displayTotal) displayTotal.textContent = formatWeightDisplay(tps.totalTimbangan); updateAllTotals(); } function updateAllTotals() { let grandTotal = 0; let grandOrganik = 0; let grandAnorganik = 0; let grandResidu = 0; state.tpsData.forEach(tps => { grandTotal += tps.totalTimbangan; grandOrganik += tps.totalOrganik; grandAnorganik += tps.totalAnorganik; grandResidu += tps.totalResidu; }); if (elements.grandTotalDisplay) { elements.grandTotalDisplay.textContent = formatWeightDisplay(grandTotal); } if (elements.totalOrganikDisplay) { elements.totalOrganikDisplay.textContent = formatWeightDisplay(grandOrganik); } if (elements.totalAnorganikDisplay) { elements.totalAnorganikDisplay.textContent = formatWeightDisplay(grandAnorganik); } if (elements.totalResiduDisplay) { elements.totalResiduDisplay.textContent = formatWeightDisplay(grandResidu); } } function syncTimbanganToTpsData() { const tps = state.tpsData[state.activeTpsIndex]; const form = elements.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 jenisSampahSelect = item.querySelector('.input-jenis-sampah'); 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), jenisSampah: jenisSampahSelect.value, uploaded: existingData ? existingData.uploaded : false }); }); } function uploadSingleFotoTimbangan(itemIndex) { const tps = state.tpsData[state.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}\nJenis: ${timbanganItem.jenisSampah}\nBerat: ${timbanganItem.weight} kg\n(Implementasi upload ke server)`); timbanganItem.uploaded = true; const form = elements.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 = state.tpsData[state.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 = state.tpsData[state.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 buildSubmitFormData(tps) { 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('TotalOrganik', tps.totalOrganik); formData.append('TotalAnorganik', tps.totalAnorganik); formData.append('TotalResidu', tps.totalResidu); formData.append('NamaPetugas', tps.namaPetugas); tps.fotoKedatangan.forEach((file) => { formData.append('FotoKedatangan', file); }); tps.timbangan.forEach((timb) => { if (timb.file) formData.append('FotoTimbangan', timb.file); formData.append('BeratTimbangan', timb.weight); formData.append('JenisSampahList', timb.jenisSampah); }); tps.fotoPetugas.forEach((file) => { formData.append('FotoPetugas', file); }); const antiForgeryTokenEl = document.querySelector('#upst-antiforgery input[name="__RequestVerificationToken"]'); if (antiForgeryTokenEl && antiForgeryTokenEl.value) { formData.append('__RequestVerificationToken', antiForgeryTokenEl.value); } return formData; } function markTpsSubmitted(tps) { tps.submitted = true; if (state.selectedTpsList.length > 1) { renderTabs(); } } function submitTpsData() { const tps = state.tpsData[state.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; } // MODE STATIK (aktif sekarang) // Cuma validasi + tandai TPS selesai, ga kirim ke backend. markTpsSubmitted(tps); alert(`Validasi ${tps.name} OK. Data belum dikirim ke server (mode statik).`); // MODE PRODUCTION (aktifkan kalau backend udah ready mas ebik) /* const formData = buildSubmitFormData(tps); fetch('/upst/detail-penjemputan', { method: 'POST', body: formData }) .then(async response => { if (response.ok) { markTpsSubmitted(tps); alert(`Data ${tps.name} berhasil disimpan!`); window.location.reload(); } else { const errorText = await response.text(); if (response.status === 400) { alert('Sesi submit tidak valid. Silakan refresh halaman lalu coba lagi.'); } else { alert(errorText || 'Gagal menyimpan data. Silakan coba lagi.'); } } }) .catch(error => { console.error('Error:', error); alert('Terjadi kesalahan saat menyimpan data.'); }); */ } 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 formatFileSize(bytes) { const mb = bytes / (1024 * 1024); return `${mb.toFixed(2)} MB`; } return { init: init, setNomorSpj: function(nomorSpj) { state.nomorSpj = nomorSpj; } }; })(); document.addEventListener('DOMContentLoaded', function() { DetailPenjemputan.init(['TPS A', 'TPS B', 'TPS C']); const platNomorEl = document.getElementById('plat-nomor'); if (platNomorEl) { const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); if (nomorSpjEl) { DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim()); } } });