From fb75e82964cd0ada2f75731976ea0eef849b2930 Mon Sep 17 00:00:00 2001 From: marszayn Date: Fri, 13 Mar 2026 12:01:13 +0700 Subject: [PATCH] update: localstorage --- Services/DetailPenjemputanService.cs | 16 +- .../driver/js/detail-penjemputan-non-tps.js | 2319 ++++++++------ wwwroot/driver/js/detail-penjemputan-tps.js | 2785 ++++++++++------- 3 files changed, 3094 insertions(+), 2026 deletions(-) diff --git a/Services/DetailPenjemputanService.cs b/Services/DetailPenjemputanService.cs index 2c34785..2670723 100644 --- a/Services/DetailPenjemputanService.cs +++ b/Services/DetailPenjemputanService.cs @@ -114,8 +114,9 @@ namespace eSPJ.Services }; } - // Save files - var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "penjemputan", DateTime.Now.ToString("yyyy-MM-dd")); + var uploadDateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "penjemputan", uploadDateFolder); + var uploadBaseUrl = $"/uploads/penjemputan/{uploadDateFolder}"; if (!Directory.Exists(uploadPath)) { Directory.CreateDirectory(uploadPath); @@ -147,7 +148,7 @@ namespace eSPJ.Services { await file.CopyToAsync(stream); } - tpsData.FotoKedatangan.Add(fileName); + tpsData.FotoKedatangan.Add($"{uploadBaseUrl}/{fileName}"); } // Save foto timbangan @@ -171,7 +172,7 @@ namespace eSPJ.Services tpsData.Timbangan.Add(new TimbanganItem { - FotoFileName = fileName, + FotoFileName = $"{uploadBaseUrl}/{fileName}", Berat = new List { (i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0) }, LokasiAngkut = new List(), JenisSampah = new List { jenisSampah }, @@ -190,10 +191,9 @@ namespace eSPJ.Services { await file.CopyToAsync(stream); } - tpsData.FotoPetugas.Add(fileName); + tpsData.FotoPetugas.Add($"{uploadBaseUrl}/{fileName}"); } - // Load existing data and append var allData = await GetAllTpsDataAsync(); allData.Add(tpsData); await SaveTpsDataAsync(allData); @@ -220,9 +220,7 @@ namespace eSPJ.Services { try { - // TODO: Integrate with OpenRouter API - // For now, return mock response - await Task.Delay(500); // Simulate API call + await Task.Delay(500); return new OcrTimbanganResponse { diff --git a/wwwroot/driver/js/detail-penjemputan-non-tps.js b/wwwroot/driver/js/detail-penjemputan-non-tps.js index c002728..0a292d7 100644 --- a/wwwroot/driver/js/detail-penjemputan-non-tps.js +++ b/wwwroot/driver/js/detail-penjemputan-non-tps.js @@ -1,105 +1,394 @@ -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'); +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'; + let activeTpsIndex = 0; + let tpsData = []; + let nomorSpj = "SPJ/07-2025/PKM/000476"; - const OCR_AREAS = [ - { id: 'A', x: 0.34, y: 0.35, w: 0.40, h: 0.11, color: 'border-lime-400 bg-lime-500/15' }, - { id: 'B', x: 0.31, y: 0.33, w: 0.45, h: 0.14, color: 'border-amber-300 bg-amber-400/10' }, - { id: 'C', x: 0.29, y: 0.31, w: 0.49, h: 0.17, color: 'border-cyan-300 bg-cyan-400/10' } - ]; - 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'; + const STORAGE_KEY = "detailPenjemputanNonTpsState"; - function initializeLocation() { - 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(); + 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); } + } - 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; - } - - nomorSpj = detail.nomorSpj || nomorSpj; - applyDetailDataToView(detail, namaTps, namaPerusahaan); - renderTpsForm(); - } catch (error) { - console.warn('Gagal memuat detail penjemputan non-TPS:', error); + 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; } - 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 (isBrowserFile(value)) { + return value; + } - if (titleEl) titleEl.textContent = namaTps; - if (badgeEl) badgeEl.textContent = namaTps; - if (formBadgeEl) formBadgeEl.textContent = namaTps; + 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}`; - } + 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); + function renderTpsForm() { + const tps = tpsData[activeTpsIndex]; + const submitState = getSubmitState(tps); - tpsContentContainer.innerHTML = ` + tpsContentContainer.innerHTML = `
- - + + @@ -179,44 +468,230 @@ document.addEventListener('DOMContentLoaded', function() {
Batal - +
- ${submitState.canSubmit ? '' : `

${submitState.message}

`} + ${submitState.canSubmit ? "" : `

${submitState.message}

`}
`; - attachTpsFormListeners(); - restoreTpsTimbanganItems(); - restorePhotoPreview(); + 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); } - function restorePhotoPreview() { - const tps = tpsData[activeTpsIndex]; - const form = tpsContentContainer.querySelector('form'); - if (!form) return; + const previewPetugas = form.querySelector(".tps-preview-petugas"); + if (previewPetugas && tps.fotoPetugas.length > 0) { + renderStoredPhotos(tps.fotoPetugas, previewPetugas); + } + } - const previewKedatangan = form.querySelector('.tps-preview-kedatangan'); - if (previewKedatangan && tps.fotoKedatangan.length > 0) { - renderStoredPhotos(tps.fotoKedatangan, previewKedatangan); - } + function renderStoredPhotos(files, container) { + container.innerHTML = ""; + container.className = "space-y-2"; - const previewPetugas = form.querySelector('.tps-preview-petugas'); - if (previewPetugas && tps.fotoPetugas.length > 0) { - renderStoredPhotos(tps.fotoPetugas, previewPetugas); - } + 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); } - function renderStoredPhotos(files, container) { - container.innerHTML = ''; - container.className = 'space-y-2'; + const btnUploadPetugas = form.querySelector(".tps-btn-upload-petugas"); + if (btnUploadPetugas) { + btnUploadPetugas.addEventListener("click", uploadFotoPetugas); + } - files.forEach((file, index) => { - const item = document.createElement('div'); - item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black'; + form.addEventListener("submit", function (e) { + e.preventDefault(); + submitTpsData(); + }); + } - const imageUrl = URL.createObjectURL(file); - const safeName = file.name.replace(/"/g, '"'); - item.innerHTML = ` + 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}
@@ -226,629 +701,589 @@ document.addEventListener('DOMContentLoaded', function() { `; - const img = item.querySelector('.preview-multi-image'); - if (img) { - img.onload = function() { - URL.revokeObjectURL(imageUrl); - }; - } - container.appendChild(item); - }); - } + const img = item.querySelector(".preview-multi-image"); + if (img) { + img.onload = function () { + URL.revokeObjectURL(imageUrl); + }; + } + previewContainer.appendChild(item); + }); + } - function attachTpsFormListeners() { - const form = tpsContentContainer.querySelector('form'); - const tps = tpsData[activeTpsIndex]; + function formatWeightDisplay(value) { + if (isNaN(value)) return "0,00"; + return value.toFixed(2).replace(".", ","); + } - 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'); + 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; + } - fotoKedatanganInput.addEventListener('change', function() { - tps.fotoKedatangan = Array.from(this.files); - tps.fotoKedatanganUploaded = false; - updateWaktuKedatangan(); - updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); - refreshKedatanganUploadState(form); - }); + 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); - fotoPetugasInput.addEventListener('change', function() { - tps.fotoPetugas = Array.from(this.files); - tps.fotoPetugasUploaded = false; - updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); - refreshPetugasUploadState(form); - }); + 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")}`; - namaPetugasInput.addEventListener('input', function() { - tps.namaPetugas = this.value; - refreshPetugasUploadState(form); - }); - - 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(); - } - - function reverseGeocode(lat, lng) { - const tps = tpsData[activeTpsIndex]; - fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`) - .then(res => res.json()) - .then(data => { - const address = data.display_name || `${lat}, ${lng}`; - tps.latitude = lat; - tps.longitude = lng; - tps.alamatJalan = address; - - const form = tpsContentContainer.querySelector('form'); - if (form) { - const latInput = form.querySelector('.tps-display-latitude'); - const lngInput = form.querySelector('.tps-display-longitude'); - if (latInput) latInput.value = lat; - if (lngInput) lngInput.value = lng; - } - }) - .catch(() => { - tps.latitude = lat; - tps.longitude = lng; - tps.alamatJalan = `${lat}, ${lng}`; - - const form = tpsContentContainer.querySelector('form'); - if (form) { - const latInput = form.querySelector('.tps-display-latitude'); - const lngInput = form.querySelector('.tps-display-longitude'); - if (latInput) latInput.value = lat; - if (lngInput) lngInput.value = lng; - } - }); - } - - function getLocationUpdate() { - if (!('geolocation' in navigator)) return; - - navigator.geolocation.getCurrentPosition( - function(position) { - const lat = position.coords.latitude.toFixed(6); - const lng = position.coords.longitude.toFixed(6); - reverseGeocode(lat, lng); + 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", }, - function() { - console.log('Lokasi tidak diizinkan'); - } + { + 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); - 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 (aiResult && aiResult.success && aiResult.weight) { + guessedWeight = parseWeightInput(aiResult.weight); + bestRawText = aiResult.raw || aiResult.weight; + isSuccess = guessedWeight > 0; + if (isSuccess) break; } - if (guessedWeight > 0) { - weightInput.value = formatWeightDisplay(guessedWeight); - weightInput.placeholder = 'Berat terdeteksi otomatis'; - } else { - weightInput.placeholder = 'Tidak terbaca otomatis, isi manual'; - } + if (aiResult && aiResult.raw) bestRawText = aiResult.raw; + } - updateTpsTotalTimbangan(); + 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."; } - 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); + if (guessedWeight > 0) { + weightInput.value = formatWeightDisplay(guessedWeight); + weightInput.placeholder = "Berat terdeteksi otomatis"; + } else { + weightInput.placeholder = "Tidak terbaca otomatis, isi manual"; } - function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { - if (!hasFile) { - return '

Pilih foto timbangan terlebih dahulu

'; - } + updateTpsTotalTimbangan(); + } - if (isUploaded) { - return ` + 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 ` + if (!hasValidWeight) { + return `

Isi berat manual dulu sebelum upload jika berat tidak terbaca.

`; - } + } - return ` + 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); } - function getKedatanganUploadStateMarkup(tps) { - if (tps.fotoKedatanganUploaded) { - return '
✓ Foto kedatangan sudah diupload
'; - } - if (!tps.fotoKedatangan.length) { - return ''; - } - return ``; + refreshSubmitButtonState(form); + } + + function getPetugasUploadStateMarkup(tps) { + if (tps.fotoPetugasUploaded) { + return '
✓ Foto petugas sudah diupload
'; } - 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); + if (!tps.fotoPetugas.length) { + return ""; } - function getPetugasUploadStateMarkup(tps) { - if (tps.fotoPetugasUploaded) { - return '
✓ Foto petugas sudah diupload
'; - } - - if (!tps.fotoPetugas.length) { - return ''; - } - - if (!tps.namaPetugas.trim()) { - 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; + return ``; + } - const tps = tpsData[activeTpsIndex]; - stateContainer.innerHTML = getPetugasUploadStateMarkup(tps); + function refreshPetugasUploadState(form) { + const stateContainer = form.querySelector(".petugas-upload-state"); + if (!stateContainer) return; - const btnUploadPetugas = stateContainer.querySelector('.tps-btn-upload-petugas:not([disabled])'); - if (btnUploadPetugas) { - btnUploadPetugas.addEventListener('click', uploadFotoPetugas); - } + const tps = tpsData[activeTpsIndex]; + stateContainer.innerHTML = getPetugasUploadStateMarkup(tps); - refreshSubmitButtonState(form); + const btnUploadPetugas = stateContainer.querySelector( + ".tps-btn-upload-petugas:not([disabled])", + ); + if (btnUploadPetugas) { + btnUploadPetugas.addEventListener("click", uploadFotoPetugas); } - function isTimbanganItemReady(timbanganItem) { - const weight = timbanganItem?.berat && timbanganItem.berat.length > 0 ? timbanganItem.berat[0] : 0; - return Boolean(timbanganItem?.file) && Boolean(timbanganItem?.uploaded) && weight > 0; + 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.", + }; } - 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: '' }; + if (!tps.timbangan.length) { + return { + canSubmit: false, + message: "Tambahkan minimal 1 data timbangan sebelum submit.", + }; } - 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'); - } + if (tps.timbangan.some((item) => !isTimbanganItemReady(item))) { + return { + canSubmit: false, + message: + "Pastikan semua foto timbangan sudah diupload dan beratnya valid.", + }; } - 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 = Boolean(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); - } + if (!tps.fotoPetugas.length || !tps.fotoPetugasUploaded) { + return { + canSubmit: false, + message: "Silakan upload foto petugas terlebih dahulu", + }; } - 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}`; - } - }); + if (!tps.namaPetugas.trim()) { + return { + canSubmit: false, + message: "Isi nama petugas dulu sebelum submit.", + }; } - 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; + return { canSubmit: true, message: "" }; + } - 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 = existingData && existingData.file; - const isUploaded = existingData && existingData.uploaded; - const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); + function refreshSubmitButtonState(form) { + const submitButton = form.querySelector('button[type="submit"]'); + if (!submitButton) return; - item.innerHTML = ` + 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
@@ -858,261 +1293,331 @@ document.addEventListener('DOMContentLoaded', function() {
- +
`; - 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'); + 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 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); - } - } - }); - - 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; + 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); + }; + } } - function syncTimbanganToTpsData() { - const tps = tpsData[activeTpsIndex]; - const form = tpsContentContainer.querySelector('form'); - if (!form) return; + 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 repeater = form.querySelector('.tps-timbangan-repeater'); - const items = repeater.querySelectorAll('.timbangan-item'); - const previousTimbangan = [...tps.timbangan]; + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(watermarkedFile); + fileInput.files = dataTransfer.files; - 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 - }); - }); - } - - 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) => formData.append('FotoKedatangan', file)); - tps.timbangan.forEach((timb) => { - if (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) => 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); - } + 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 form = tpsContentContainer.querySelector('form'); - if (form) refreshSubmitButtonState(form); + + 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); } - 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); + 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; } - 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); + 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; } - function submitTpsData() { - const tps = tpsData[activeTpsIndex]; - const submitState = getSubmitState(tps); - if (!submitState.canSubmit) return alert(submitState.message); + if (targetItem) { + refreshTimbanganUploadState(targetItem); + } - 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; + 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 }); + 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(); - } + const nomorSpjEl = document.querySelector(".text-gray-600.font-mono"); + if (nomorSpjEl && nomorSpjEl.textContent) { + nomorSpj = nomorSpjEl.textContent.trim(); + } - initializeLocation(); - loadDetailData(); + initializeLocation(); + loadDetailData(); }); diff --git a/wwwroot/driver/js/detail-penjemputan-tps.js b/wwwroot/driver/js/detail-penjemputan-tps.js index e5e7761..7040e19 100644 --- a/wwwroot/driver/js/detail-penjemputan-tps.js +++ b/wwwroot/driver/js/detail-penjemputan-tps.js @@ -1,215 +1,590 @@ -const DetailPenjemputan = (function() { - 'use strict'; +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 CONFIG = { + 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", + }, + ], + JENIS_SAMPAH: ["Organik", "Anorganik", "Residu"], + DEFAULT_JENIS: "Residu", + }; + + 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", }; + } - let state = { - activeTpsIndex: 0, - tpsData: [], - availableTpsList: [], - selectedTpsList: [], - hasRequestedLocation: [], - nomorSpj: 'SPJ/07-2025/PKM/000476' + 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."), }; + } - const elements = { - grandTotalDisplay: null, - tpsSelectionContainer: null, - tpsTabsContainer: null, - tpsCheckboxesContainer: null, - btnConfirmTps: null, - tpsTabsEl: null, - tpsContentContainer: null, - totalOrganikDisplay: null, - totalAnorganikDisplay: null, - totalResiduDisplay: null - }; + 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 init(tpsList) { - initElements(); - initializeLocation(tpsList); + function applyApiDraftData(draftData) { + const apiList = Array.isArray(draftData) + ? draftData + : draftData?.tpsData || draftData?.draftPenjemputan || []; + + if (!Array.isArray(apiList) || apiList.length === 0) { + return; } - 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); - } + if (!state.tpsData.length) { + initializeTpsData(apiList); } - 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); - elements.tpsTabsContainer.style.display = 'block'; - renderSingleForm(); - return; - } + state.tpsData = state.tpsData.map((currentTps, index) => { + const apiTps = findMatchingApiTps(apiList, currentTps, index); + if (!apiTps) { + return currentTps; + } - state.selectedTpsList = [...state.availableTpsList]; - initializeTpsData(state.selectedTpsList); - elements.tpsTabsContainer.style.display = 'block'; + 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; - if (state.selectedTpsList.length === 1) { - renderSingleForm(); - } else { - renderTabs(); - renderTpsForm(); - } + 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(); } - 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 || ''), - 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); + updateAllTotals(); + saveState(); + } + + 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) { + loadState(); + 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 (elements.tpsSelectionContainer) { + elements.tpsSelectionContainer.style.display = "none"; } - 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); - }); - } + if (state.tpsData.length > 0) { + 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"; - 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; + if (state.tpsData.length === 1) { + renderSingleForm(); + } else { + renderTabs(); renderTpsForm(); + } + + updateAllTotals(); + return; } - function renderTabs() { - elements.tpsTabsEl.style.display = 'block'; - elements.tpsTabsEl.className = 'mb-4'; + if (state.availableTpsList.length === 0) { + state.selectedTpsList = ["1 Lokasi TPS"]; + initializeTpsData(state.selectedTpsList); + elements.tpsTabsContainer.style.display = "block"; + renderSingleForm(); + return; + } - 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'; + state.selectedTpsList = [...state.availableTpsList]; + initializeTpsData(state.selectedTpsList); + elements.tpsTabsContainer.style.display = "block"; - const optionsHtml = state.tpsData.map((tps, index) => { - const marker = tps.submitted ? '✅ ' : ''; - return ``; - }).join(''); + if (state.selectedTpsList.length === 1) { + renderSingleForm(); + } else { + renderTabs(); + renderTpsForm(); + } + } - elements.tpsTabsEl.innerHTML = ` + 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 || "", + 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 = "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); - } - }); + 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(); } - function switchToTps(index) { - state.activeTpsIndex = index; - renderTabs(); - renderTpsForm(); - - if (!state.hasRequestedLocation[index]) { - state.hasRequestedLocation[index] = true; - getLocationUpdate(); - } - - updateAllTotals(); - } + 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 = ` + 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 = `
- - + + @@ -224,24 +599,24 @@ const DetailPenjemputan = (function() {
Batal - +
- ${submitState.canSubmit ? '' : `

${submitState.message}

`} + ${submitState.canSubmit ? "" : `

${submitState.message}

`}
`; - attachTpsFormListeners(); - restoreTpsTimbanganItems(); - restorePhotoPreview(); - } + attachTpsFormListeners(); + restoreTpsTimbanganItems(); + restorePhotoPreview(); + } - function renderSection1Kedatangan(tps, showTpsName) { - return ` + function renderSection1Kedatangan(tps, showTpsName) { + return `
1
-

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

+

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

Upload foto kedatangan

@@ -269,10 +644,10 @@ const DetailPenjemputan = (function() {
`; - } + } - function renderSection2Timbangan(tps, showTpsName) { - return ` + function renderSection2Timbangan(tps, showTpsName) { + return `
2
@@ -289,10 +664,10 @@ const DetailPenjemputan = (function() {
`; - } + } - function renderSection3Petugas(tps) { - return ` + function renderSection3Petugas(tps) { + return `
3
@@ -314,100 +689,212 @@ const DetailPenjemputan = (function() {
`; + } + + 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")); + 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); } - 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')); - refreshKedatanganUploadState(form); - }); - - fotoPetugasInput.addEventListener('change', function() { - tps.fotoPetugas = Array.from(this.files); - tps.fotoPetugasUploaded = false; - updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); - refreshPetugasUploadState(form); - }); - - namaPetugasInput.addEventListener('input', function() { - tps.namaPetugas = this.value; - refreshPetugasUploadState(form); - }); - - 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(); - }); + const btnUploadPetugas = form.querySelector(".tps-btn-upload-petugas"); + if (btnUploadPetugas) { + btnUploadPetugas.addEventListener("click", uploadFotoPetugas); } - 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); - }); - } + 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); } - 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); - } + 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'; + function renderStoredPhotos(files, container) { + container.innerHTML = ""; + container.className = "space-y-2"; - const imageUrl = URL.createObjectURL(file); - const safeName = file.name.replace(/"/g, '"'); - item.innerHTML = ` + 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 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(); + 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 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; + } + saveState(); + } + + 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}
@@ -417,133 +904,44 @@ const DetailPenjemputan = (function() { `; - const img = item.querySelector('.preview-multi-image'); - if (img) { - img.onload = function() { - URL.revokeObjectURL(imageUrl); - }; - } - container.appendChild(item); - }); - } + const img = item.querySelector(".preview-multi-image"); + if (img) { + img.onload = function () { + URL.revokeObjectURL(imageUrl); + }; + } + previewContainer.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; + function createTimbanganItem(repeater, existingData = null) { + const photoNumber = repeater.children.length + 1; - getLocationUpdate(); - } + 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; - function getLocationUpdate() { - if (!('geolocation' in navigator)) return; + const weight = existingData ? existingData.weight : 0; + const jenisSampah = existingData + ? existingData.jenisSampah + : CONFIG.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."; - 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; - const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); - - item.innerHTML = ` + item.innerHTML = `

Item Timbangan #${photoNumber}

-
+
Preview foto timbangan

${ocrInfoText}

@@ -552,769 +950,926 @@ const DetailPenjemputan = (function() {
- +
`; - 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'); + 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; - refreshTimbanganUploadState(item); - } - } - }); - - 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 = elements.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 = elements.tpsContentContainer.querySelector('form'); - if (form) refreshSubmitButtonState(form); - }); - - jenisSampahSelect.addEventListener('change', function() { - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - const form = elements.tpsContentContainer.querySelector('form'); - if (form) refreshSubmitButtonState(form); - }); - - 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(); - if (form) refreshSubmitButtonState(form); - }); - - repeater.appendChild(item); - refreshTimbanganUploadState(item); - return item; + 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); + }; + } } - function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { - if (!hasFile) { - return '

Pilih foto timbangan terlebih dahulu

'; - } + fileInput.addEventListener("change", async function () { + if (fileInput.files && fileInput.files[0]) { + const originalFile = fileInput.files[0]; - if (isUploaded) { - return ` + 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; + 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 = elements.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 = elements.tpsContentContainer.querySelector("form"); + if (form) refreshSubmitButtonState(form); + }); + + jenisSampahSelect.addEventListener("change", function () { + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + const form = elements.tpsContentContainer.querySelector("form"); + if (form) refreshSubmitButtonState(form); + }); + + 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(); + if (form) refreshSubmitButtonState(form); + }); + + repeater.appendChild(item); + refreshTimbanganUploadState(item); + return item; + } + + 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 ` + if (!hasValidWeight) { + return `

Isi berat manual dulu sebelum upload jika berat tidak terbaca.

`; - } + } - return ` + 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 = state.tpsData[state.activeTpsIndex]; + stateContainer.innerHTML = getKedatanganUploadStateMarkup(tps); + + const btnUploadKedatangan = stateContainer.querySelector( + ".tps-btn-upload-kedatangan", + ); + if (btnUploadKedatangan) { + btnUploadKedatangan.addEventListener("click", uploadFotoKedatangan); } - function getKedatanganUploadStateMarkup(tps) { - if (tps.fotoKedatanganUploaded) { - return '
✓ Foto kedatangan sudah diupload
'; - } - if (!tps.fotoKedatangan.length) { - return ''; - } - return ``; + refreshSubmitButtonState(form); + } + + function getPetugasUploadStateMarkup(tps) { + if (tps.fotoPetugasUploaded) { + return '
✓ Foto petugas sudah diupload
'; } - function refreshKedatanganUploadState(form) { - const stateContainer = form.querySelector('.kedatangan-upload-state'); - if (!stateContainer) return; - - const tps = state.tpsData[state.activeTpsIndex]; - stateContainer.innerHTML = getKedatanganUploadStateMarkup(tps); - - const btnUploadKedatangan = stateContainer.querySelector('.tps-btn-upload-kedatangan'); - if (btnUploadKedatangan) { - btnUploadKedatangan.addEventListener('click', uploadFotoKedatangan); - } - - refreshSubmitButtonState(form); + if (!tps.fotoPetugas.length) { + return ""; } - function getPetugasUploadStateMarkup(tps) { - if (tps.fotoPetugasUploaded) { - return '
✓ Foto petugas sudah diupload
'; - } - - if (!tps.fotoPetugas.length) { - return ''; - } - - if (!tps.namaPetugas.trim()) { - 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; + return ``; + } - const tps = state.tpsData[state.activeTpsIndex]; - stateContainer.innerHTML = getPetugasUploadStateMarkup(tps); + function refreshPetugasUploadState(form) { + const stateContainer = form.querySelector(".petugas-upload-state"); + if (!stateContainer) return; - const btnUploadPetugas = stateContainer.querySelector('.tps-btn-upload-petugas:not([disabled])'); - if (btnUploadPetugas) { - btnUploadPetugas.addEventListener('click', uploadFotoPetugas); - } + const tps = state.tpsData[state.activeTpsIndex]; + stateContainer.innerHTML = getPetugasUploadStateMarkup(tps); - refreshSubmitButtonState(form); + const btnUploadPetugas = stateContainer.querySelector( + ".tps-btn-upload-petugas:not([disabled])", + ); + if (btnUploadPetugas) { + btnUploadPetugas.addEventListener("click", uploadFotoPetugas); } - function isTimbanganItemReady(timbanganItem) { - return Boolean(timbanganItem?.file) && Boolean(timbanganItem?.uploaded) && (timbanganItem?.weight || 0) > 0; + refreshSubmitButtonState(form); + } + + function isTimbanganItemReady(timbanganItem) { + return ( + hasStoredPhoto(timbanganItem?.file) && + Boolean(timbanganItem?.uploaded) && + (timbanganItem?.weight || 0) > 0 + ); + } + + function getSubmitState(tps) { + if (!tps.fotoKedatangan.length || !tps.fotoKedatanganUploaded) { + return { + canSubmit: false, + message: "Silakan upload foto kedatangan terlebih dahulu.", + }; } - 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: '' }; + if (!tps.timbangan.length) { + return { + canSubmit: false, + message: "Tambahkan minimal 1 data timbangan sebelum submit.", + }; } - 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'); - } + if (tps.timbangan.some((item) => !isTimbanganItemReady(item))) { + return { + canSubmit: false, + message: + "Pastikan semua foto timbangan sudah diupload dan beratnya valid.", + }; } - 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]); - 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); - } + if (!tps.fotoPetugas.length || !tps.fotoPetugasUploaded) { + return { + canSubmit: false, + message: "Silakan upload foto petugas terlebih dahulu", + }; } - 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}`; - } - }); + if (!tps.namaPetugas.trim()) { + return { + canSubmit: false, + message: "Isi nama petugas dulu sebelum submit.", + }; } -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' } - ]; + return { canSubmit: true, message: "" }; + } - 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(); - + function refreshSubmitButtonState(form) { + const submitButton = form.querySelector('button[type="submit"]'); + if (!submitButton) return; - 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); + 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 = hasStoredPhoto(currentData?.file || fileInput?.files?.[0]); + 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 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; + 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"); - 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); + ctx.drawImage(img, 0, 0); - if (aiResult && aiResult.success && aiResult.weight) { - guessedWeight = parseWeightInput(aiResult.weight); - bestRawText = aiResult.raw || aiResult.weight; - isSuccess = guessedWeight > 0; - if (isSuccess) break; - } + 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", + ]; - if (aiResult && aiResult.raw) { - bestRawText = aiResult.raw; - } - } + 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"); - 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.'; + 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 (guessedWeight > 0) { - weightInput.value = formatWeightDisplay(guessedWeight); - weightInput.placeholder = 'Berat terdeteksi otomatis'; - } else { - weightInput.placeholder = 'Tidak terbaca otomatis, isi manual'; + if (aiResult && aiResult.raw) { + bestRawText = aiResult.raw; } + } - updateTpsTotalTimbangan(); + 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."; } - 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; - }); + if (guessedWeight > 0) { + weightInput.value = formatWeightDisplay(guessedWeight); + weightInput.placeholder = "Berat terdeteksi otomatis"; + } else { + weightInput.placeholder = "Tidak terbaca otomatis, isi manual"; } - 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)); + updateTpsTotalTimbangan(); + } - 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; + 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; + }); + } - ctx.imageSmoothingEnabled = true; - ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); - return canvas; - } + 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)); - 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); - }); - } + 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; - 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; - } + ctx.imageSmoothingEnabled = true; + ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); + return canvas; + } - 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'); - 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), - weight: parseWeightInput(weightValue.value), - jenisSampah: jenisSampahSelect.value, - uploaded: existingData ? existingData.uploaded : false, - ocrInfo - }); - }); - } - - function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { - 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]; - if (timbanganItem.weight <= 0) { - alert('Berat belum valid. Isi manual dulu sebelum upload foto timbangan.'); + 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; } + } + }); - const _ext = (timbanganItem.file.name.split('.').pop() || 'jpg').toLowerCase(); - const _jenis = timbanganItem.jenisSampah.toLowerCase(); - const _beratStr = parseFloat(timbanganItem.weight.toFixed(2)).toString().replace('.', '_'); - const _newName = `timbangan${itemIndex + 1}-${_jenis}-${_beratStr}.${_ext}`; - timbanganItem.file = new File([timbanganItem.file], _newName, { type: timbanganItem.file.type, lastModified: timbanganItem.file.lastModified }); + tps.totalOrganik = totalOrganik; + tps.totalAnorganik = totalAnorganik; + tps.totalResidu = totalResidu; + tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu; - 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 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 (!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; - } + 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); - if (targetItem) { - refreshTimbanganUploadState(targetItem); - } + updateAllTotals(); + saveState(); + } - syncTimbanganToTpsData(); - const form = elements.tpsContentContainer.querySelector('form'); - if (form) refreshSubmitButtonState(form); + 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), + weight: parseWeightInput(weightValue.value), + jenisSampah: jenisSampahSelect.value, + uploaded: existingData ? existingData.uploaded : false, + ocrInfo, + }); + }); + + saveState(); + } + + function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { + const tps = state.tpsData[state.activeTpsIndex]; + + if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { + alert("Belum ada foto timbangan yang dipilih!"); + return; } - 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; - const form = elements.tpsContentContainer.querySelector('form'); - if (form) refreshKedatanganUploadState(form); + const timbanganItem = tps.timbangan[itemIndex]; + if (timbanganItem.weight <= 0) { + alert( + "Berat belum valid. Isi manual dulu sebelum upload foto timbangan.", + ); + return; } - function uploadFotoPetugas() { - const tps = state.tpsData[state.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 = elements.tpsContentContainer.querySelector('form'); - if (form) refreshPetugasUploadState(form); + const _ext = ( + timbanganItem.file.name.split(".").pop() || "jpg" + ).toLowerCase(); + const _jenis = timbanganItem.jenisSampah.toLowerCase(); + const _beratStr = parseFloat(timbanganItem.weight.toFixed(2)) + .toString() + .replace(".", "_"); + const _newName = `timbangan${itemIndex + 1}-${_jenis}-${_beratStr}.${_ext}`; + timbanganItem.file = new File([timbanganItem.file], _newName, { + type: timbanganItem.file.type, + lastModified: timbanganItem.file.lastModified, + }); + + alert( + `Upload foto timbangan #${itemIndex + 1} untuk ${tps.name}\nJenis: ${timbanganItem.jenisSampah}\nBerat: ${timbanganItem.weight} kg\n(Implementasi upload ke server)`, + ); + + timbanganItem.uploaded = true; + + 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; } - 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) => { - 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; + if (targetItem) { + refreshTimbanganUploadState(targetItem); } - function markTpsSubmitted(tps) { - tps.submitted = true; - if (state.selectedTpsList.length > 1) { - renderTabs(); - } + syncTimbanganToTpsData(); + const form = elements.tpsContentContainer.querySelector("form"); + if (form) refreshSubmitButtonState(form); + saveState(); + } + + function uploadFotoKedatangan() { + const tps = state.tpsData[state.activeTpsIndex]; + if (tps.fotoKedatangan.length === 0) { + alert("Belum ada foto kedatangan yang dipilih!"); + return; } - function submitTpsData() { - const tps = state.tpsData[state.activeTpsIndex]; - const submitState = getSubmitState(tps); - if (!submitState.canSubmit) { - alert(submitState.message); - return; - } + alert( + `Upload ${tps.fotoKedatangan.length} foto kedatangan untuk ${tps.name}\n(Implementasi upload ke server)`, + ); - // 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).`); + tps.fotoKedatanganUploaded = true; + const form = elements.tpsContentContainer.querySelector("form"); + if (form) refreshKedatanganUploadState(form); + saveState(); + } - // MODE PRODUCTION (aktifkan kalau backend udah ready mas ebik) - /* + function uploadFotoPetugas() { + const tps = state.tpsData[state.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 = elements.tpsContentContainer.querySelector("form"); + if (form) refreshPetugasUploadState(form); + saveState(); + } + + 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(); + } + } + + function submitTpsData() { + const tps = state.tpsData[state.activeTpsIndex]; + const submitState = getSubmitState(tps); + if (!submitState.canSubmit) { + alert(submitState.message); + return; + } + + // MODE STATIK (aktif sekarang) + // Cuma validasi + tandai TPS selesai, ga kirim ke backend. + markTpsSubmitted(tps); + clearStateIfAllSubmitted(); + 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', { @@ -1324,6 +1879,7 @@ async function applyWatermark(file, photoNumber) { .then(async response => { if (response.ok) { markTpsSubmitted(tps); + clearStateIfAllSubmitted(); alert(`Data ${tps.name} berhasil disimpan!`); window.location.reload(); } else { @@ -1340,53 +1896,62 @@ async function applyWatermark(file, photoNumber) { alert('Terjadi kesalahan saat menyimpan data.'); }); */ - } + } - function formatWeightDisplay(value) { - if (isNaN(value)) return '0,00'; - return value.toFixed(2).replace('.', ','); - } + 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 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 formatFileSize(bytes) { + const mb = bytes / (1024 * 1024); + return `${mb.toFixed(2)} MB`; + } - return { - init: init, - setNomorSpj: function(nomorSpj) { - state.nomorSpj = nomorSpj; - } - }; + 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 - })); - DetailPenjemputan.init(tpsList); - } catch (error) { - console.error('Error loading TPS list:', error); +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, + })); + DetailPenjemputan.init(tpsList); + if (data.draftPenjemputan || data.tpsData) { + DetailPenjemputan.hydrateFromApi(data.draftPenjemputan || data.tpsData); } - - const platNomorEl = document.getElementById('plat-nomor'); - if (platNomorEl) { - const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); - if (nomorSpjEl) { - DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim()); - } + } 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()); } + } });