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' }; const ENDPOINTS = { saveDraft: '/upst/detail-penjemputan/save-draft', loadDraft: '/upst/detail-penjemputan/load-draft', deleteDraft: '/upst/detail-penjemputan/delete-draft', uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan', uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan', uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas', submit: '/upst/detail-penjemputan' }; let state = { activeTpsIndex: 0, tpsData: [], availableTpsList: [], selectedTpsList: [], hasRequestedLocation: [], nomorSpj: "SPJ/07-2025/PKM/000476", }; const STORAGE_KEY = "detailPenjemputanTpsState"; function saveState() { try { const stateCopy = JSON.parse( JSON.stringify(state, (key, value) => { if (value instanceof File) { return { name: value.name, size: value.size, type: value.type }; } return value; }), ); localStorage.setItem(STORAGE_KEY, JSON.stringify(stateCopy)); } catch (e) { console.warn("Failed to save state to localStorage:", e); } } function loadState() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); state = { ...state, ...parsed }; } } catch (e) { console.warn("Failed to load state from localStorage:", e); } } function clearState() { localStorage.removeItem(STORAGE_KEY); } function isBrowserFile(value) { return typeof File !== "undefined" && value instanceof File; } function getFirstValue(value) { return Array.isArray(value) ? value[0] : value; } function resolveStoredPhoto(value) { if (!value) { return null; } if (isBrowserFile(value)) { return value; } if (typeof value === "string") { return { url: value, name: value.split("/").pop() || "Foto", size: 0, source: "api", }; } if (typeof value !== "object") { return null; } const url = value.url || value.fileUrl || value.path || value.filePath || value.src || value.fotoUrl || value.photoUrl || value.fotoFileName || value.fileName || ""; return { ...value, url, name: value.name || value.fileName || value.file_name || (url ? url.split("/").pop() : "Foto"), size: Number(value.size || value.fileSize || 0) || 0, source: value.source || "api", }; } function getStoredPhotoUrl(value) { const photo = resolveStoredPhoto(value); if (!photo) { return ""; } if (isBrowserFile(photo)) { return URL.createObjectURL(photo); } return photo.url || ""; } function getStoredPhotoName(value, fallbackName = "Foto") { const photo = resolveStoredPhoto(value); if (!photo) { return fallbackName; } return isBrowserFile(photo) ? photo.name || fallbackName : photo.name || fallbackName; } function getStoredPhotoSize(value) { const photo = resolveStoredPhoto(value); if (!photo) { return 0; } return isBrowserFile(photo) ? photo.size || 0 : Number(photo.size || 0) || 0; } function hasStoredPhoto(value) { const photo = resolveStoredPhoto(value); if (!photo) { return false; } return isBrowserFile(photo) || Boolean(photo.url || photo.name); } function normalizePhotoList(value) { if (!Array.isArray(value)) { return []; } return value.map(resolveStoredPhoto).filter(Boolean); } function normalizeTimbanganItem(item) { if (!item) { return { file: null, weight: 0, jenisSampah: CONFIG.DEFAULT_JENIS, uploaded: false, ocrInfo: "OCR: belum diproses.", }; } const file = resolveStoredPhoto( item.file || item.foto || item.photo || item.fotoUrl || item.photoUrl || item.fotoFileName, ); const weight = Number(item.weight ?? getFirstValue(item.berat) ?? 0) || 0; const jenisSampah = getFirstValue(item.jenisSampah) || item.JenisSampah || CONFIG.DEFAULT_JENIS; const uploaded = Boolean( item.uploaded ?? item.isUploaded ?? item.IsUploaded ?? hasStoredPhoto(file), ); return { ...item, file, weight, jenisSampah, uploaded, ocrInfo: item.ocrInfo || item.OcrInfo || (uploaded ? "Foto dari server." : "OCR: belum diproses."), }; } function findMatchingApiTps(apiList, currentTps, index) { return ( apiList.find( (item) => (currentTps.spjDetailId && (item.spjDetailId === currentTps.spjDetailId || item.SpjDetailID === currentTps.spjDetailId)) || (currentTps.lokasiAngkutId && (item.lokasiAngkutId === currentTps.lokasiAngkutId || item.LokasiAngkutID === currentTps.lokasiAngkutId)) || (item.name || item.Name) === currentTps.name, ) || apiList[index] ); } function applyApiDraftData(draftData) { const apiList = Array.isArray(draftData) ? draftData : draftData?.tpsData || draftData?.draftPenjemputan || []; if (!Array.isArray(apiList) || apiList.length === 0) { return; } if (!state.tpsData.length) { initializeTpsData(apiList); } state.tpsData = state.tpsData.map((currentTps, index) => { const apiTps = findMatchingApiTps(apiList, currentTps, index); if (!apiTps) { return currentTps; } const fotoKedatangan = normalizePhotoList( apiTps.fotoKedatangan || apiTps.FotoKedatangan, ); const fotoPetugas = normalizePhotoList( apiTps.fotoPetugas || apiTps.FotoPetugas, ); const apiTimbangan = apiTps.timbangan || apiTps.Timbangan; const timbangan = Array.isArray(apiTimbangan) ? apiTimbangan.map(normalizeTimbanganItem) : currentTps.timbangan; return { ...currentTps, name: apiTps.name || apiTps.Name || currentTps.name, lokasiAngkutId: apiTps.lokasiAngkutId || apiTps.LokasiAngkutID || currentTps.lokasiAngkutId, spjDetailId: apiTps.spjDetailId || apiTps.SpjDetailID || currentTps.spjDetailId, latitude: apiTps.latitude || apiTps.Latitude || currentTps.latitude, longitude: apiTps.longitude || apiTps.Longitude || currentTps.longitude, alamatJalan: apiTps.alamatJalan || apiTps.AlamatJalan || currentTps.alamatJalan, waktuKedatangan: apiTps.waktuKedatangan || apiTps.WaktuKedatangan || currentTps.waktuKedatangan, fotoKedatangan: fotoKedatangan.length ? fotoKedatangan : currentTps.fotoKedatangan, fotoKedatanganUploaded: Boolean( apiTps.fotoKedatanganUploaded ?? apiTps.FotoKedatanganUploaded, ) || fotoKedatangan.length > 0 || currentTps.fotoKedatanganUploaded, timbangan, totalOrganik: Number( apiTps.totalOrganik ?? apiTps.TotalOrganik ?? currentTps.totalOrganik, ) || 0, totalAnorganik: Number( apiTps.totalAnorganik ?? apiTps.TotalAnorganik ?? currentTps.totalAnorganik, ) || 0, totalResidu: Number( apiTps.totalResidu ?? apiTps.TotalResidu ?? currentTps.totalResidu, ) || 0, totalTimbangan: Number( apiTps.totalTimbangan ?? apiTps.TotalTimbangan ?? currentTps.totalTimbangan, ) || 0, fotoPetugas: fotoPetugas.length ? fotoPetugas : currentTps.fotoPetugas, fotoPetugasUploaded: Boolean(apiTps.fotoPetugasUploaded ?? apiTps.FotoPetugasUploaded) || fotoPetugas.length > 0 || currentTps.fotoPetugasUploaded, namaPetugas: apiTps.namaPetugas || apiTps.NamaPetugas || currentTps.namaPetugas, submitted: Boolean( apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted, ), }; }); state.selectedTpsList = state.tpsData.map((item) => item.name); state.activeTpsIndex = Math.min( state.activeTpsIndex, Math.max(state.tpsData.length - 1, 0), ); elements.tpsTabsContainer.style.display = "block"; if (state.tpsData.length === 1) { renderSingleForm(); } else { renderTabs(); renderTpsForm(); } updateAllTotals(); saveState(); } const elements = { grandTotalDisplay: null, tpsSelectionContainer: null, tpsTabsContainer: null, tpsCheckboxesContainer: null, btnConfirmTps: null, tpsTabsEl: null, tpsContentContainer: null, totalOrganikDisplay: null, totalAnorganikDisplay: null, totalResiduDisplay: null, }; let autoSaveTimer = null; let autoSaveStatusEl = null; async function init(tpsList) { initElements(); await 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 buildDraftKey(tps) { const spjDetailId = (tps?.spjDetailId || '').trim(); const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim(); if (!spjDetailId && !lokasiAngkutId) return ''; return `tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, ''); } function getActiveTps() { return state.tpsData[state.activeTpsIndex] || null; } function getAutoSaveStatusEl() { if (!autoSaveStatusEl || !document.body.contains(autoSaveStatusEl)) { autoSaveStatusEl = document.getElementById('auto-save-status'); } return autoSaveStatusEl; } function showAutoSaveStatus(msg, isOk = false) { const statusEl = getAutoSaveStatusEl(); if (!statusEl) return; statusEl.textContent = msg; statusEl.className = isOk ? 'text-[11px] text-green-600 text-center font-medium transition-opacity' : 'text-[11px] text-amber-500 text-center font-medium transition-opacity'; statusEl.style.opacity = '1'; if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500); } function scheduleAutoSave() { clearTimeout(autoSaveTimer); showAutoSaveStatus('menyimpan...'); autoSaveTimer = setTimeout(autoSaveDraft, 1000); } async function autoSaveDraft() { const tps = getActiveTps(); if (!tps || !tps.draftKey) return; const payload = { draftKey: tps.draftKey, lokasiAngkutId: tps.lokasiAngkutId || '', spjDetailId: tps.spjDetailId || '', latitude: tps.latitude || '', longitude: tps.longitude || '', alamatJalan: tps.alamatJalan || '', waktuKedatangan: tps.waktuKedatangan || '', fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [], fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false, timbangan: (tps.timbangan || []).map(item => ({ berat: item.weight || 0, jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS, fotoFileName: item.fotoFileName || '', uploaded: item.uploaded || false, ocrInfo: item.ocrInfo || '' })), totalOrganik: tps.totalOrganik || 0, totalAnorganik: tps.totalAnorganik || 0, totalResidu: tps.totalResidu || 0, totalTimbangan: tps.totalTimbangan || 0, fotoPetugasFileNames: tps.fotoPetugasFileNames || [], fotoPetugasUploaded: tps.fotoPetugasUploaded || false, namaPetugas: tps.namaPetugas || '' }; try { const res = await fetch(ENDPOINTS.saveDraft, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success); } catch (_) { showAutoSaveStatus('✗ Gagal simpan draft'); } } async function loadDraftForTps(tps) { if (!tps || !tps.draftKey) return; try { const res = await fetch(`${ENDPOINTS.loadDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`); if (!res.ok) return; const data = await res.json(); if (!data.success || !data.hasDraft || !data.draft) return; const draft = data.draft; tps.lokasiAngkutId = draft.lokasiAngkutId || tps.lokasiAngkutId; tps.spjDetailId = draft.spjDetailId || tps.spjDetailId; tps.latitude = draft.latitude || ''; tps.longitude = draft.longitude || ''; tps.alamatJalan = draft.alamatJalan || ''; tps.waktuKedatangan = draft.waktuKedatangan || ''; tps.fotoKedatanganFileNames = draft.fotoKedatanganFileNames || []; tps.fotoKedatanganUploaded = draft.fotoKedatanganUploaded || false; tps.fotoPetugasFileNames = draft.fotoPetugasFileNames || []; tps.fotoPetugasUploaded = draft.fotoPetugasUploaded || false; tps.namaPetugas = draft.namaPetugas || ''; tps.totalOrganik = draft.totalOrganik || 0; tps.totalAnorganik = draft.totalAnorganik || 0; tps.totalResidu = draft.totalResidu || 0; tps.totalTimbangan = draft.totalTimbangan || 0; tps.timbangan = (draft.timbangan || []).map(item => ({ file: null, fotoFileName: item.fotoFileName || '', weight: item.berat || 0, jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS, uploaded: item.uploaded || false, ocrInfo: item.ocrInfo || 'OCR: belum diproses.' })); } catch (error) { console.warn('Gagal memuat draft TPS:', error); } } async function loadDraftsForAllTps() { await Promise.all(state.tpsData.map(loadDraftForTps)); } async function initializeLocation(tpsList) { state.availableTpsList = tpsList || []; if (elements.tpsSelectionContainer) { elements.tpsSelectionContainer.style.display = 'none'; } if (state.availableTpsList.length === 0) { state.selectedTpsList = ['1 Lokasi TPS']; initializeTpsData(state.selectedTpsList); await loadDraftsForAllTps(); elements.tpsTabsContainer.style.display = 'block'; renderSingleForm(); return; } state.selectedTpsList = [...state.availableTpsList]; initializeTpsData(state.selectedTpsList); await loadDraftsForAllTps(); elements.tpsTabsContainer.style.display = 'block'; if (state.selectedTpsList.length === 1) { renderSingleForm(); } else { renderTabs(); renderTpsForm(); } } function initializeTpsData(tpsNames) { state.tpsData = tpsNames.map((tpsItem, index) => ({ name: typeof tpsItem === 'string' ? tpsItem : (tpsItem?.name || tpsItem?.Name || `TPS ${index + 1}`), index: index, lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''), spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''), draftKey: '', latitude: '', longitude: '', alamatJalan: '', waktuKedatangan: '', fotoKedatangan: [], fotoKedatanganFileNames: [], fotoKedatanganUploaded: false, timbangan: [], totalOrganik: 0, totalAnorganik: 0, totalResidu: 0, totalTimbangan: 0, fotoPetugas: [], fotoPetugasFileNames: [], fotoPetugasUploaded: false, namaPetugas: '', submitted: false })).map(tps => ({ ...tps, draftKey: buildDraftKey(tps) })); 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); }); } async 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) { showToast('Pilih minimal 1 TPS untuk diangkut!', 'error'); return; } initializeTpsData(state.selectedTpsList); await loadDraftsForAllTps(); 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 = "block"; elements.tpsTabsEl.className = "mb-4"; const activeTps = state.tpsData[state.activeTpsIndex]; const isActiveSubmitted = !!(activeTps && activeTps.submitted); const selectClass = isActiveSubmitted ? "w-full rounded-xl border border-green-300 bg-green-50 text-green-700 px-3 py-2 text-sm font-bold" : "w-full rounded-xl border border-gray-200 bg-white text-gray-700 px-3 py-2 text-sm font-bold"; const optionsHtml = state.tpsData .map((tps, index) => { const marker = tps.submitted ? "✅ " : ""; return ``; }) .join(""); elements.tpsTabsEl.innerHTML = ` `; const dropdown = elements.tpsTabsEl.querySelector("#tps-dropdown"); if (dropdown) { dropdown.addEventListener("change", (e) => { const nextIndex = parseInt(e.target.value, 10); if (!Number.isNaN(nextIndex)) { switchToTps(nextIndex); } }); } } 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; const submitState = getSubmitState(tps); elements.tpsContentContainer.innerHTML = `
`; attachTpsFormListeners(); restoreTpsTimbanganItems(); restorePhotoPreview(); } function renderSection1Kedatangan(tps, showTpsName) { return `Upload foto kedatangan
Upload foto timbangan, berat auto terisi
Upload dokumentasi petugas
✓ Foto ${index + 1}
`; } container.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) : 0; const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS; const hasFileBlob = Boolean(existingData?.file); const hasFile = Boolean(existingData?.file || existingData?.fotoFileName); const isUploaded = Boolean(existingData?.uploaded); const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); item.innerHTML = `Item Timbangan #${photoNumber}
${ocrInfoText}
Pilih foto timbangan terlebih dahulu
'; } if (isUploaded) { return `Jika ingin revisi, pilih file baru diatas. Status upload akan tereset otomatis.
`; } if (!hasValidWeight) { return `Isi berat manual dulu sebelum upload jika berat tidak terbaca.
`; } return `Foto siap diupload.
`; } function getKedatanganUploadStateMarkup(tps) { if (tps.fotoKedatanganUploaded) { return 'Isi nama petugas terlebih dahulu
`; } return ``; } function refreshPetugasUploadState(form) { const stateContainer = form.querySelector(".petugas-upload-state"); if (!stateContainer) return; const tps = state.tpsData[state.activeTpsIndex]; stateContainer.innerHTML = getPetugasUploadStateMarkup(tps); const btnUploadPetugas = stateContainer.querySelector( ".tps-btn-upload-petugas:not([disabled])", ); if (btnUploadPetugas) { btnUploadPetugas.addEventListener("click", uploadFotoPetugas); } refreshSubmitButtonState(form); } function isTimbanganItemReady(timbanganItem) { return Boolean(timbanganItem?.file || timbanganItem?.fotoFileName) && Boolean(timbanganItem?.uploaded) && (timbanganItem?.weight || 0) > 0; } function getSubmitState(tps) { if (!tps.fotoKedatanganUploaded) { if (!tps.fotoKedatangan.length) { return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan terlebih dahulu.' }; } return { canSubmit: false, message: 'Silakan upload foto kedatangan terlebih dahulu.' }; } if (!tps.timbangan.length) { return { canSubmit: false, message: "Tambahkan minimal 1 data timbangan sebelum submit.", }; } if (tps.timbangan.some((item) => !isTimbanganItemReady(item))) { return { canSubmit: false, message: "Pastikan semua foto timbangan sudah diupload dan beratnya valid.", }; } if (!tps.fotoPetugasUploaded) { if (!tps.fotoPetugas.length) { return { canSubmit: false, message: 'Silakan pilih dan upload foto petugas terlebih dahulu.' }; } return { canSubmit: false, message: 'Silakan upload foto petugas terlebih dahulu' }; } if (!tps.namaPetugas.trim()) { return { canSubmit: false, message: "Isi nama petugas dulu sebelum submit.", }; } return { canSubmit: true, message: "" }; } function refreshSubmitButtonState(form) { const submitButton = form.querySelector('button[type="submit"]'); if (!submitButton) return; let messageEl = form.querySelector(".submit-state-message"); const tps = state.tpsData[state.activeTpsIndex]; const submitState = getSubmitState(tps); submitButton.disabled = !submitState.canSubmit; submitButton.className = `w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`; if (!messageEl) { messageEl = document.createElement("p"); messageEl.className = "submit-state-message text-[11px] text-center text-red-500 font-medium"; submitButton .closest(".flex.gap-3") ?.insertAdjacentElement("afterend", messageEl); } if (submitState.canSubmit) { messageEl.textContent = ""; messageEl.classList.add("hidden"); } else { messageEl.textContent = submitState.message; messageEl.classList.remove("hidden"); } } function refreshTimbanganUploadState(item) { const stateContainer = item.querySelector(".timbangan-upload-state"); if (!stateContainer) return; const repeater = item.parentElement; const itemIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1; const tps = state.tpsData[state.activeTpsIndex]; const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null; const fileInput = item.querySelector('.input-foto-timbangan'); const hasFile = Boolean(currentData?.file || fileInput?.files?.[0] || currentData?.fotoFileName); const isUploaded = Boolean(currentData?.uploaded); const weightInputValue = item.querySelector('.input-berat-timbangan-value'); const currentWeight = currentData?.weight ?? parseWeightInput(weightInputValue?.value || '0'); const hasValidWeight = currentWeight > 0; stateContainer.innerHTML = getTimbanganUploadStateMarkup( hasFile, isUploaded, hasValidWeight, ); const uploadBtn = stateContainer.querySelector(".btn-upload-timbangan"); if (uploadBtn) { uploadBtn.addEventListener("click", function () { const latestIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1; uploadSingleFotoTimbangan(latestIndex, item); }); } const form = elements.tpsContentContainer.querySelector("form"); if (form) { refreshSubmitButtonState(form); } } 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(); saveState(); } 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); } saveState(); } 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'); const previousTimbangan = [...tps.timbangan]; tps.timbangan = []; items.forEach((item, index) => { const fileInput = item.querySelector('.input-foto-timbangan'); const weightValue = item.querySelector('.input-berat-timbangan-value'); const jenisSampahSelect = item.querySelector('.input-jenis-sampah'); const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.'; const existingData = previousTimbangan[index]; tps.timbangan.push({ file: fileInput.files[0] || (existingData ? existingData.file : null), fotoFileName: existingData ? existingData.fotoFileName || '' : '', weight: parseWeightInput(weightValue.value), jenisSampah: jenisSampahSelect.value, uploaded: existingData ? existingData.uploaded : false, ocrInfo }); }); } async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { const tps = state.tpsData[state.activeTpsIndex]; if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { showToast('Belum ada foto timbangan yang dipilih!', 'error'); return; } const timbanganItem = tps.timbangan[itemIndex]; if (timbanganItem.weight <= 0) { showToast('Berat belum valid. Isi manual dulu sebelum upload foto timbangan.', 'error'); return; } if (!targetItem) { const form = elements.tpsContentContainer.querySelector('form'); const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null; const items = repeater ? repeater.querySelectorAll('.timbangan-item') : []; targetItem = items[itemIndex] || null; } const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null; if (uploadBtn) { uploadBtn.disabled = true; uploadBtn.textContent = 'Mengupload...'; } const formData = new FormData(); formData.append('FotoTimbangan', timbanganItem.file); formData.append('DraftKey', tps.draftKey || ''); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('ItemIndex', itemIndex); formData.append('JenisSampah', timbanganItem.jenisSampah || CONFIG.DEFAULT_JENIS); formData.append('Berat', Number(timbanganItem.weight || 0).toFixed(2)); try { const res = await fetch(ENDPOINTS.uploadTimbangan, { method: 'POST', body: formData }); const data = await res.json(); if (res.ok && data.success) { timbanganItem.uploaded = true; timbanganItem.fotoFileName = data.fileUrl || data.fileName || ''; const fotoInputTimb = targetItem?.querySelector('.input-foto-timbangan'); if (fotoInputTimb) fotoInputTimb.value = ''; showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success'); } else { showToast(data.message || 'Gagal upload foto timbangan.', 'error'); if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.textContent = 'Upload Foto Timbangan Ini'; } } } catch (_) { showToast('Koneksi gagal saat upload foto timbangan.', 'error'); if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.textContent = 'Upload Foto Timbangan Ini'; } } if (targetItem) { refreshTimbanganUploadState(targetItem); } syncTimbanganToTpsData(); const form = elements.tpsContentContainer.querySelector('form'); if (form) refreshSubmitButtonState(form); scheduleAutoSave(); } async function uploadFotoKedatangan() { const tps = state.tpsData[state.activeTpsIndex]; if (tps.fotoKedatangan.length === 0) { showToast('Belum ada foto kedatangan yang dipilih!', 'error'); return; } const form = elements.tpsContentContainer.querySelector('form'); const btn = form ? form.querySelector('.tps-btn-upload-kedatangan') : null; if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; } const formData = new FormData(); tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file)); formData.append('DraftKey', tps.draftKey || ''); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); formData.append('Latitude', tps.latitude || ''); formData.append('Longitude', tps.longitude || ''); formData.append('AlamatJalan', tps.alamatJalan || ''); try { const res = await fetch(ENDPOINTS.uploadKedatangan, { method: 'POST', body: formData }); const data = await res.json(); if (res.ok && data.success) { tps.fotoKedatanganUploaded = true; tps.fotoKedatanganFileNames = data.fileUrls || data.fileNames || []; showToast(data.message || 'Foto kedatangan berhasil diupload.', 'success'); if (form) { const fotoInput = form.querySelector('.tps-foto-kedatangan'); if (fotoInput) fotoInput.value = ''; refreshKedatanganUploadState(form); } scheduleAutoSave(); } else { showToast(data.message || 'Gagal upload foto kedatangan.', 'error'); if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; } } } catch (_) { showToast('Koneksi gagal saat upload foto kedatangan.', 'error'); if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; } } } async function uploadFotoPetugas() { const tps = state.tpsData[state.activeTpsIndex]; if (tps.fotoPetugas.length === 0) { showToast('Belum ada foto petugas yang dipilih!', 'error'); return; } if (!tps.namaPetugas.trim()) { showToast('Nama petugas wajib diisi sebelum upload foto petugas!', 'error'); return; } const form = elements.tpsContentContainer.querySelector('form'); const btn = form ? form.querySelector('.tps-btn-upload-petugas:not([disabled])') : null; if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; } const formData = new FormData(); tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file)); formData.append('DraftKey', tps.draftKey || ''); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('NamaPetugas', tps.namaPetugas || ''); try { const res = await fetch(ENDPOINTS.uploadPetugas, { method: 'POST', body: formData }); const data = await res.json(); if (res.ok && data.success) { tps.fotoPetugasUploaded = true; tps.fotoPetugasFileNames = data.fileUrls || data.fileNames || []; showToast(data.message || 'Foto petugas berhasil diupload.', 'success'); if (form) { const fotoInput = form.querySelector('.tps-foto-petugas'); if (fotoInput) fotoInput.value = ''; refreshPetugasUploadState(form); } scheduleAutoSave(); } else { showToast(data.message || 'Gagal upload foto petugas.', 'error'); if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; } } } catch (_) { showToast('Koneksi gagal saat upload foto petugas.', 'error'); if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; } } } function buildSubmitFormData(tps) { const formData = new FormData(); formData.append("LokasiAngkutID", tps.lokasiAngkutId || ""); formData.append("SpjDetailID", tps.spjDetailId || ""); 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) => { if (isBrowserFile(file)) { formData.append("FotoKedatangan", file); } }); tps.timbangan.forEach((timb) => { if (isBrowserFile(timb.file)) formData.append("FotoTimbangan", timb.file); formData.append("BeratTimbangan", timb.weight); formData.append("JenisSampahList", timb.jenisSampah); }); tps.fotoPetugas.forEach((file) => { if (isBrowserFile(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(); } saveState(); } function clearStateIfAllSubmitted() { if ( state.tpsData.length > 0 && state.tpsData.every((item) => item.submitted) ) { clearState(); } } async function submitTpsData() { const tps = state.tpsData[state.activeTpsIndex]; const submitState = getSubmitState(tps); if (!submitState.canSubmit) { showToast(submitState.message, 'error'); return; } const form = elements.tpsContentContainer.querySelector('form'); const submitBtn = form ? form.querySelector('button[type="submit"]') : null; if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = `Menyimpan${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}...`; } const formData = buildSubmitFormData(tps); try { const response = await fetch(ENDPOINTS.submit, { method: 'POST', body: formData }); if (response.ok || response.redirected) { markTpsSubmitted(tps); if (tps.draftKey) { await fetch(`${ENDPOINTS.deleteDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`, { method: 'DELETE' }); } showToast(`Data ${tps.name} berhasil disimpan!`, 'success'); const allSubmitted = state.tpsData.every(item => item.submitted); if (allSubmitted) { setTimeout(() => { window.location.href = '/upst/detail-penjemputan/detail-selesai'; }, 1200); } else { renderTabs(); renderTpsForm(); } } else { const errorText = await response.text(); showToast(errorText || 'Gagal menyimpan data. Silakan coba lagi.', 'error'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`; } } } catch (error) { console.error('Error:', error); showToast('Terjadi kesalahan saat menyimpan data.', 'error'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`; } } } 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`; } function showToast(message, type = 'info') { let container = document.getElementById('espj-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'espj-toast-container'; container.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;'; document.body.appendChild(container); } const toast = document.createElement('div'); const bg = type === 'success' ? '#16a34a' : type === 'error' ? '#dc2626' : '#2563eb'; toast.style.cssText = `background:${bg};color:#fff;padding:10px 18px;border-radius:16px;font-size:13px;font-weight:600;box-shadow:0 4px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;text-align:center;`; toast.textContent = message; container.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3200); } return { init: init, hydrateFromApi: applyApiDraftData, setNomorSpj: function (nomorSpj) { state.nomorSpj = nomorSpj; saveState(); }, }; })(); document.addEventListener('DOMContentLoaded', async function() { try { const response = await fetch('/driver/json/tps-list.json'); const data = await response.json(); const tpsList = data.tpsList.map(tps => ({ name: tps.name, lokasiAngkutId: tps.lokasiAngkutId, spjDetailId: tps.spjDetailId, id: tps.id })); await DetailPenjemputan.init(tpsList); } catch (error) { console.error('Error loading TPS list:', error); } const platNomorEl = document.getElementById('plat-nomor'); if (platNomorEl) { const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); if (nomorSpjEl) { DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim()); } } });