document.addEventListener("DOMContentLoaded", function () { const grandTotalDisplay = document.getElementById("grand-total-timbangan"); const grandTotalOrganikDisplay = document.getElementById( "grand-total-organik", ); const grandTotalAnorganikDisplay = document.getElementById( "grand-total-anorganik", ); const grandTotalResiduDisplay = document.getElementById("grand-total-residu"); const tpsContentContainer = document.getElementById("tps-content"); let activeTpsIndex = 0; let tpsData = []; let nomorSpj = "SPJ/07-2025/PKM/000476"; const STORAGE_KEY = "detailPenjemputanNonTpsState"; function saveState() { try { const stateCopy = JSON.parse( JSON.stringify({ activeTpsIndex, tpsData, nomorSpj }, (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); if (parsed.tpsData && parsed.tpsData.length > 0) { tpsData = parsed.tpsData; } if (parsed.nomorSpj) nomorSpj = parsed.nomorSpj; } } 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, berat: [0], jenisSampah: [DEFAULT_JENIS], lokasiAngkut: [], uploaded: false, ocrInfo: "OCR: belum diproses.", }; } const file = resolveStoredPhoto( item.file || item.foto || item.photo || item.fotoUrl || item.photoUrl || item.fotoFileName, ); const berat = Number(getFirstValue(item.berat) ?? item.weight ?? 0) || 0; const jenisSampah = getFirstValue(item.jenisSampah) || item.JenisSampah || DEFAULT_JENIS; const uploaded = Boolean( item.uploaded ?? item.isUploaded ?? item.IsUploaded ?? hasStoredPhoto(file), ); return { ...item, file, berat: [berat], jenisSampah: [jenisSampah], lokasiAngkut: item.lokasiAngkut || [], uploaded, ocrInfo: item.ocrInfo || item.OcrInfo || (uploaded ? "Foto dari server." : "OCR: belum diproses."), }; } function hydrateSingleTpsFromApi(detail) { const tps = tpsData[0]; if (!tps || !detail) { return; } const fotoKedatangan = normalizePhotoList( detail.fotoKedatangan || detail.FotoKedatangan, ); const fotoPetugas = normalizePhotoList( detail.fotoPetugas || detail.FotoPetugas, ); const apiTimbangan = detail.timbangan || detail.Timbangan; const timbangan = Array.isArray(apiTimbangan) ? apiTimbangan.map(normalizeTimbanganItem) : tps.timbangan; tps.name = detail.namaTps || detail.tpsName || detail.name || tps.name; tps.lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tps.lokasiAngkutId; tps.spjDetailId = detail.spjDetailId || detail.SpjDetailID || tps.spjDetailId; tps.latitude = detail.latitude || detail.Latitude || tps.latitude; tps.longitude = detail.longitude || detail.Longitude || tps.longitude; tps.alamatJalan = detail.alamatJalan || detail.AlamatJalan || tps.alamatJalan; tps.waktuKedatangan = detail.waktuKedatangan || detail.WaktuKedatangan || tps.waktuKedatangan; tps.fotoKedatangan = fotoKedatangan.length ? fotoKedatangan : tps.fotoKedatangan; tps.fotoKedatanganUploaded = Boolean(detail.fotoKedatanganUploaded ?? detail.FotoKedatanganUploaded) || fotoKedatangan.length > 0 || tps.fotoKedatanganUploaded; tps.timbangan = timbangan; tps.totalOrganik = Number(detail.totalOrganik ?? detail.TotalOrganik ?? tps.totalOrganik) || 0; tps.totalAnorganik = Number( detail.totalAnorganik ?? detail.TotalAnorganik ?? tps.totalAnorganik, ) || 0; tps.totalResidu = Number(detail.totalResidu ?? detail.TotalResidu ?? tps.totalResidu) || 0; tps.totalTimbangan = Number( detail.totalTimbangan ?? detail.TotalTimbangan ?? tps.totalTimbangan, ) || 0; tps.fotoPetugas = fotoPetugas.length ? fotoPetugas : tps.fotoPetugas; tps.fotoPetugasUploaded = Boolean(detail.fotoPetugasUploaded ?? detail.FotoPetugasUploaded) || fotoPetugas.length > 0 || tps.fotoPetugasUploaded; tps.namaPetugas = detail.namaPetugas || detail.NamaPetugas || tps.namaPetugas; tps.submitted = Boolean( detail.submitted ?? detail.Submitted ?? tps.submitted, ); saveState(); } const OCR_AREAS = [ { id: "A", x: 0.34, y: 0.35, w: 0.4, 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", }, ]; const JENIS_SAMPAH = ["Organik", "Anorganik", "Residu"]; const DEFAULT_JENIS = "Residu"; const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-non-tps.json"; const DEFAULT_TPS_NAME = "Lokasi Pengangkutan 1"; function initializeLocation() { loadState(); if (tpsData.length > 0) { renderTpsForm(); return; } tpsData = [ { name: DEFAULT_TPS_NAME, index: 0, lokasiAngkutId: "", spjDetailId: "", latitude: "", longitude: "", alamatJalan: "", waktuKedatangan: "", fotoKedatangan: [], fotoKedatanganUploaded: false, timbangan: [], totalOrganik: 0, totalAnorganik: 0, totalResidu: 0, totalTimbangan: 0, fotoPetugas: [], fotoPetugasUploaded: false, namaPetugas: "", submitted: false, }, ]; renderTpsForm(); } async function loadDetailData() { try { const response = await fetch(DETAIL_DATA_URL, { cache: "no-store" }); if (!response.ok) { return; } const payload = await response.json(); const detail = payload.detailPenjemputan || payload; const namaTps = detail.namaTps || detail.tpsName || detail.name || DEFAULT_TPS_NAME; const namaPerusahaan = detail.namaPerusahaan || detail.companyName || ""; if (tpsData[0]) { tpsData[0].name = namaTps; tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId; tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId; } hydrateSingleTpsFromApi(detail); nomorSpj = detail.nomorSpj || nomorSpj; applyDetailDataToView(detail, namaTps, namaPerusahaan); renderTpsForm(); } catch (error) { console.warn("Gagal memuat detail penjemputan non-TPS:", error); } } function applyDetailDataToView(detail, namaTps, namaPerusahaan) { const titleEl = document.getElementById("detail-page-title"); const badgeEl = document.getElementById("detail-tps-badge"); const formBadgeEl = document.getElementById("detail-form-badge"); const companyEl = document.getElementById("detail-company-name"); const spjEl = document.getElementById("detail-spj-number"); const addressEl = document.getElementById("detail-address"); const platEl = document.getElementById("plat-nomor"); const doorEl = document.getElementById("detail-nomor-pintu"); if (titleEl) titleEl.textContent = namaTps; if (badgeEl) badgeEl.textContent = namaTps; if (formBadgeEl) formBadgeEl.textContent = namaTps; if (companyEl && namaPerusahaan) companyEl.textContent = namaPerusahaan; if (spjEl && detail.nomorSpj) spjEl.textContent = detail.nomorSpj; if (addressEl && detail.alamat) addressEl.textContent = detail.alamat; if (platEl && detail.platNomor) platEl.textContent = detail.platNomor; if (doorEl && detail.nomorPintu) doorEl.textContent = detail.nomorPintu; document.title = `Detail Penjemputan - ${namaTps}`; } function renderTpsForm() { const tps = tpsData[activeTpsIndex]; const submitState = getSubmitState(tps); tpsContentContainer.innerHTML = `
1

Foto Kedatangan

Upload foto kedatangan

${getKedatanganUploadStateMarkup(tps)}
2

Foto Timbang Sampah

Upload foto timbangan, berat auto terisi

3

Foto Petugas

Upload dokumentasi petugas

${getPetugasUploadStateMarkup(tps)}
Batal
${submitState.canSubmit ? "" : `

${submitState.message}

`}
`; 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 = getStoredPhotoUrl(file); const safeName = getStoredPhotoName(file, `Foto ${index + 1}`).replace( /"/g, """, ); const fileSize = getStoredPhotoSize(file); item.innerHTML = `
Preview ${index + 1}

${index + 1}. ${safeName}

${fileSize > 0 ? formatFileSize(fileSize) : "Tersimpan di server"}

`; const img = item.querySelector(".preview-multi-image"); if (img && isBrowserFile(resolveStoredPhoto(file))) { 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"); fotoKedatanganInput.addEventListener("change", function () { tps.fotoKedatangan = Array.from(this.files); tps.fotoKedatanganUploaded = false; updateWaktuKedatangan(); updateMultiPreview(this, form.querySelector(".tps-preview-kedatangan")); refreshKedatanganUploadState(form); saveState(); }); fotoPetugasInput.addEventListener("change", function () { tps.fotoPetugas = Array.from(this.files); tps.fotoPetugasUploaded = false; updateMultiPreview(this, form.querySelector(".tps-preview-petugas")); refreshPetugasUploadState(form); saveState(); }); namaPetugasInput.addEventListener("input", function () { tps.namaPetugas = this.value; refreshPetugasUploadState(form); saveState(); }); btnAddTimbangan.addEventListener("click", function () { createTimbanganItem(form.querySelector(".tps-timbangan-repeater")); syncTimbanganToTpsData(); refreshSubmitButtonState(form); }); 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 = 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) => 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(); saveState(); } 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; } saveState(); }) .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; } saveState(); }); } 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, """); 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 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; } 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 timestamp = `${days[now.getDay()]}, ${now.getDate().toString().padStart(2, "0")} ${months[now.getMonth()]} ${now.getFullYear()} • ${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`; 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: `${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.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(); const accentWidth = baseFontSize * 0.3; ctx.beginPath(); 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.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; }); 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); }); } 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: 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 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 updateTpsTotalTimbangan() { const tps = tpsData[activeTpsIndex]; const form = 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 totalResidu += value; } }); tps.totalOrganik = totalOrganik; tps.totalAnorganik = totalAnorganik; tps.totalResidu = totalResidu; tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu; const displayTotal = form.querySelector(".tps-display-total"); if (displayTotal) displayTotal.textContent = formatWeightDisplay(tps.totalTimbangan); if (grandTotalDisplay) grandTotalDisplay.textContent = formatWeightDisplay(tps.totalTimbangan); if (grandTotalOrganikDisplay) grandTotalOrganikDisplay.textContent = formatWeightDisplay(totalOrganik); if (grandTotalAnorganikDisplay) grandTotalAnorganikDisplay.textContent = formatWeightDisplay(totalAnorganik); if (grandTotalResiduDisplay) grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu); saveState(); } function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { if (!hasFile) { return '

Pilih foto timbangan terlebih dahulu

'; } if (isUploaded) { return `
✓ Foto timbangan sudah diupload

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 '
✓ Foto kedatangan sudah diupload
'; } if (!tps.fotoKedatangan.length) { return ""; } return ``; } function refreshKedatanganUploadState(form) { const stateContainer = form.querySelector(".kedatangan-upload-state"); if (!stateContainer) return; const tps = tpsData[activeTpsIndex]; stateContainer.innerHTML = getKedatanganUploadStateMarkup(tps); const btnUploadKedatangan = stateContainer.querySelector( ".tps-btn-upload-kedatangan", ); if (btnUploadKedatangan) { btnUploadKedatangan.addEventListener("click", uploadFotoKedatangan); } refreshSubmitButtonState(form); } function getPetugasUploadStateMarkup(tps) { if (tps.fotoPetugasUploaded) { return '
✓ Foto petugas sudah diupload
'; } if (!tps.fotoPetugas.length) { return ""; } if (!tps.namaPetugas.trim()) { return `

Isi nama petugas terlebih dahulu

`; } return ``; } function refreshPetugasUploadState(form) { const stateContainer = form.querySelector(".petugas-upload-state"); if (!stateContainer) return; const tps = tpsData[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) { const weight = timbanganItem?.berat && timbanganItem.berat.length > 0 ? timbanganItem.berat[0] : 0; return ( hasStoredPhoto(timbanganItem?.file) && Boolean(timbanganItem?.uploaded) && weight > 0 ); } function getSubmitState(tps) { if (!tps.fotoKedatangan.length || !tps.fotoKedatanganUploaded) { 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.fotoPetugas.length || !tps.fotoPetugasUploaded) { 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; const helperText = form.querySelector(".submit-state-message"); const tps = tpsData[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"}`; let messageEl = helperText; 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 = tpsData[activeTpsIndex]; const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null; const fileInput = item.querySelector(".input-foto-timbangan"); const hasFile = hasStoredPhoto(currentData?.file || fileInput?.files?.[0]); const isUploaded = Boolean(currentData?.uploaded); const weightInputValue = item.querySelector(".input-berat-timbangan-value"); const currentWeight = currentData?.berat && currentData.berat.length > 0 ? currentData.berat[0] : 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 = 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}`; } }); } 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.berat && existingData.berat.length > 0 ? existingData.berat[0] : 0 : 0; const jenisSampah = existingData ? existingData.jenisSampah && existingData.jenisSampah.length > 0 ? existingData.jenisSampah[0] : DEFAULT_JENIS : DEFAULT_JENIS; const hasFile = hasStoredPhoto(existingData && existingData.file); const isUploaded = Boolean(existingData && existingData.uploaded); const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : hasFile ? "OCR: diproses." : "OCR: belum diproses."; item.innerHTML = `

Item Timbangan #${photoNumber}

Preview foto timbangan

${ocrInfoText}

${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}
`; 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 && hasStoredPhoto(existingData.file)) { const existingPhoto = resolveStoredPhoto(existingData.file); const photoUrl = getStoredPhotoUrl(existingPhoto); previewImage.src = photoUrl; previewWrap.classList.remove("hidden"); if (isBrowserFile(existingPhoto)) { previewImage.onload = function () { URL.revokeObjectURL(photoUrl); }; } } fileInput.addEventListener("change", async function () { if (fileInput.files && fileInput.files[0]) { const originalFile = fileInput.files[0]; const photoNumber = Number( item.dataset.photoNumber || Array.from(repeater.children).indexOf(item) + 1, ); 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 = tpsData[activeTpsIndex]; const itemIndex = Array.from(repeater.children).indexOf(item); if (itemIndex >= 0 && tps.timbangan[itemIndex]) { tps.timbangan[itemIndex].uploaded = false; refreshTimbanganUploadState(item); saveState(); } } }); 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(); refreshTimbanganUploadState(item); const form = tpsContentContainer.querySelector("form"); if (form) refreshSubmitButtonState(form); }); 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(); refreshTimbanganUploadState(item); const form = tpsContentContainer.querySelector("form"); if (form) refreshSubmitButtonState(form); }); jenisSampahSelect.addEventListener("change", function () { updateTpsTotalTimbangan(); syncTimbanganToTpsData(); const form = tpsContentContainer.querySelector("form"); if (form) refreshSubmitButtonState(form); }); removeBtn.addEventListener("click", function () { item.remove(); const form = tpsContentContainer.querySelector("form"); const rep = form ? form.querySelector(".tps-timbangan-repeater") : null; if (rep) { renumberTimbanganItems(rep); if (rep.children.length === 0) createTimbanganItem(rep); } updateTpsTotalTimbangan(); syncTimbanganToTpsData(); if (form) refreshSubmitButtonState(form); }); repeater.appendChild(item); refreshTimbanganUploadState(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"); 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 weight = parseWeightInput(weightValue.value); const jenisSampah = item.querySelector(".input-jenis-sampah").value; const ocrInfo = item.querySelector(".input-ocr-info")?.textContent || "OCR: belum diproses."; tps.timbangan.push({ file: fileInput.files[0] || previousTimbangan[index]?.file || null, berat: [weight], jenisSampah: [jenisSampah], lokasiAngkut: [], uploaded: previousTimbangan[index]?.uploaded ?? false, ocrInfo, }); }); saveState(); } function buildSubmitFormData(tps) { const formData = new FormData(); formData.append("LokasiAngkutID", tps.lokasiAngkutId || ""); formData.append("SpjDetailID", tps.spjDetailId || ""); 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); const weight = timb.berat && timb.berat.length > 0 ? timb.berat[0] : 0; const jenis = timb.jenisSampah && timb.jenisSampah.length > 0 ? timb.jenisSampah[0] : DEFAULT_JENIS; formData.append("BeratTimbangan", weight); formData.append("JenisSampahList", jenis); }); 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 uploadSingleFotoTimbangan(itemIndex, targetItem = null) { const tps = tpsData[activeTpsIndex]; if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { alert("Belum ada foto timbangan yang dipilih!"); return; } const weight = tps.timbangan[itemIndex].berat && tps.timbangan[itemIndex].berat.length > 0 ? tps.timbangan[itemIndex].berat[0] : 0; if (weight <= 0) { alert( "Berat belum valid. Isi manual dulu sebelum upload foto timbangan.", ); return; } const _ext = ( tps.timbangan[itemIndex].file.name.split(".").pop() || "jpg" ).toLowerCase(); const _jenis = ( tps.timbangan[itemIndex].jenisSampah[0] || "residu" ).toLowerCase(); const _beratStr = parseFloat(weight.toFixed(2)) .toString() .replace(".", "_"); const _newName = `timbangan${itemIndex + 1}-${_jenis}-${_beratStr}.${_ext}`; tps.timbangan[itemIndex].file = new File( [tps.timbangan[itemIndex].file], _newName, { type: tps.timbangan[itemIndex].file.type, lastModified: tps.timbangan[itemIndex].file.lastModified, }, ); alert( `Upload foto timbangan #${itemIndex + 1}\nBerat: ${formatWeightDisplay(weight)} kg\n(Implementasi upload ke server)`, ); tps.timbangan[itemIndex].uploaded = true; if (!targetItem) { const form = tpsContentContainer.querySelector("form"); const repeater = form ? form.querySelector(".tps-timbangan-repeater") : null; const items = repeater ? repeater.querySelectorAll(".timbangan-item") : []; targetItem = items[itemIndex] || null; } if (targetItem) { refreshTimbanganUploadState(targetItem); } syncTimbanganToTpsData(); const form = tpsContentContainer.querySelector("form"); if (form) refreshSubmitButtonState(form); saveState(); } 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\n(Implementasi upload ke server)`, ); tps.fotoKedatanganUploaded = true; const form = tpsContentContainer.querySelector("form"); if (form) refreshKedatanganUploadState(form); saveState(); } function uploadFotoPetugas() { const tps = tpsData[activeTpsIndex]; if (tps.fotoPetugas.length === 0) { alert("Belum ada foto petugas yang dipilih!"); return; } if (!tps.namaPetugas.trim()) { alert("Nama petugas wajib diisi sebelum upload foto petugas!"); return; } alert( `Upload ${tps.fotoPetugas.length} foto petugas untuk ${tps.name}\n(Implementasi upload ke server)`, ); tps.fotoPetugasUploaded = true; const form = tpsContentContainer.querySelector("form"); if (form) refreshPetugasUploadState(form); saveState(); } function submitTpsData() { const tps = tpsData[activeTpsIndex]; const submitState = getSubmitState(tps); if (!submitState.canSubmit) return alert(submitState.message); alert( `Validasi OK (${tps.name}).\n- Organik: ${formatWeightDisplay(tps.totalOrganik)} kg\n- Anorganik: ${formatWeightDisplay(tps.totalAnorganik)} kg\n- Residu: ${formatWeightDisplay(tps.totalResidu)} kg\n- Total: ${formatWeightDisplay(tps.totalTimbangan)} kg\n- Petugas: ${tps.namaPetugas}`, ); tps.submitted = true; saveState(); clearState(); /* const formData = buildSubmitFormData(tps); fetch('/upst/detail-penjemputan', { method: 'POST', body: formData }) .then(response => { if (response.ok) { clearState(); } }); */ } const nomorSpjEl = document.querySelector(".text-gray-600.font-mono"); if (nomorSpjEl && nomorSpjEl.textContent) { nomorSpj = nomorSpjEl.textContent.trim(); } initializeLocation(); loadDetailData(); });