document.addEventListener('DOMContentLoaded', async function() {
const grandTotalDisplay = document.getElementById('grand-total-timbangan');
const grandTotalOrganikDisplay = document.getElementById('grand-total-organik');
const grandTotalAnorganikDisplay = document.getElementById('grand-total-anorganik');
const grandTotalResiduDisplay = document.getElementById('grand-total-residu');
const tpsContentContainer = document.getElementById('tps-content');
let activeTpsIndex = 0;
let tpsData = [];
let nomorSpj = 'SPJ/07-2025/PKM/000476';
const RECORD_DETAIL_ENDPOINT = '/upst/detail-penjemputan/api/records/detail';
let autoSaveTimer = null;
let autoSaveStatusEl = null;
let loadingOverlayEl = null;
let isAutoSaving = false;
let pendingAutoSave = false;
let lastAutoSaveSignature = "";
function getLoadingOverlay() {
if (loadingOverlayEl && document.body.contains(loadingOverlayEl)) {
return loadingOverlayEl;
}
loadingOverlayEl = document.createElement('div');
loadingOverlayEl.id = 'detail-loading-overlay';
loadingOverlayEl.className = 'fixed inset-0 z-[9999] hidden bg-white/95 backdrop-blur-sm flex items-center justify-center px-6';
loadingOverlayEl.innerHTML = `
Sedang memuat data
Mohon tunggu sebentar, data penjemputan sedang dipulihkan.
`;
document.body.appendChild(loadingOverlayEl);
return loadingOverlayEl;
}
function showLoadingOverlay() {
const overlay = getLoadingOverlay();
overlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideLoadingOverlay() {
const overlay = getLoadingOverlay();
overlay.classList.add('hidden');
document.body.style.overflow = '';
}
function scheduleAutoSave() {
clearTimeout(autoSaveTimer);
showAutoSaveStatus('menyimpan...');
autoSaveTimer = setTimeout(autoSaveRecord, 500);
}
function showAutoSaveStatus(msg, isOk = false) {
if (!autoSaveStatusEl) {
autoSaveStatusEl = document.getElementById('auto-save-status');
}
if (!autoSaveStatusEl) return;
autoSaveStatusEl.textContent = msg;
autoSaveStatusEl.className = isOk
? 'text-[11px] text-green-600 text-center font-medium transition-opacity'
: 'text-[11px] text-amber-500 text-center font-medium transition-opacity';
autoSaveStatusEl.style.opacity = '1';
if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500);
}
function buildAutoSavePayload(tps) {
return {
nomorSpj: nomorSpj || '',
namaTps: tps.name || DEFAULT_TPS_NAME,
lokasiAngkutId: tps.lokasiAngkutId || '',
spjDetailId: tps.spjDetailId || '',
latitude: tps.latitude || '',
longitude: tps.longitude || '',
alamatJalan: tps.alamatJalan || '',
waktuKedatangan: tps.waktuKedatangan || '',
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
timbangan: (tps.timbangan || []).map(t => ({
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0,
jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : DEFAULT_JENIS,
fotoFileName: t.fotoFileName || '',
uploaded: t.uploaded || false,
ocrInfo: t.ocrInfo || ''
})),
totalOrganik: tps.totalOrganik || 0,
totalAnorganik: tps.totalAnorganik || 0,
totalResidu: tps.totalResidu || 0,
totalTimbangan: tps.totalTimbangan || 0,
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
namaPetugas: tps.namaPetugas || ''
};
}
async function autoSaveRecord() {
const tps = tpsData[activeTpsIndex];
if (!tps) return;
if (tps.submitted) return;
if (isAutoSaving) {
pendingAutoSave = true;
return;
}
const payload = buildAutoSavePayload(tps);
const payloadSignature = JSON.stringify(payload);
if (payloadSignature === lastAutoSaveSignature) {
showAutoSaveStatus('✓ Data tersimpan', true);
return;
}
try {
isAutoSaving = true;
const res = await fetch('/upst/detail-penjemputan/save-record-non-tps', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payloadSignature
});
const data = await res.json();
if (data.success) {
lastAutoSaveSignature = payloadSignature;
}
showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success);
} catch {
showAutoSaveStatus('✗ Gagal simpan data');
} finally {
isAutoSaving = false;
if (pendingAutoSave) {
pendingAutoSave = false;
scheduleAutoSave();
}
}
}
function normalizeStringList(value) {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item) => typeof item === 'string' && item.trim());
}
function normalizeJenisSampahValue(value) {
if (Array.isArray(value)) {
return normalizeJenisSampahValue(value[0]);
}
if (typeof value === 'number') {
return JENIS_SAMPAH[value] || DEFAULT_JENIS;
}
if (typeof value === 'string' && value.trim()) {
const trimmed = value.trim();
const asNumber = Number(trimmed);
if (!Number.isNaN(asNumber) && String(asNumber) === trimmed) {
return JENIS_SAMPAH[asNumber] || DEFAULT_JENIS;
}
const matched = JENIS_SAMPAH.find(
(item) => item.toLowerCase() === trimmed.toLowerCase(),
);
return matched || DEFAULT_JENIS;
}
return DEFAULT_JENIS;
}
function applyServerRecordToTps(record) {
if (!record) return;
const tps = tpsData[activeTpsIndex];
if (!tps) return;
const fotoKedatangan = normalizeStringList(
record.fotoKedatanganFileNames || record.fotoKedatangan || record.FotoKedatangan,
);
const fotoPetugas = normalizeStringList(
record.fotoPetugasFileNames || record.fotoPetugas || record.FotoPetugas,
);
tps.name = record.namaTps || record.name || record.Name || tps.name || DEFAULT_TPS_NAME;
tps.lokasiAngkutId = record.lokasiAngkutId || record.LokasiAngkutID || tps.lokasiAngkutId;
tps.spjDetailId = record.spjDetailId || record.SpjDetailID || tps.spjDetailId;
tps.latitude = record.latitude || record.Latitude || tps.latitude;
tps.longitude = record.longitude || record.Longitude || tps.longitude;
tps.alamatJalan = record.alamatJalan || record.AlamatJalan || tps.alamatJalan;
tps.waktuKedatangan = record.waktuKedatangan || record.WaktuKedatangan || tps.waktuKedatangan;
tps.fotoKedatangan = [];
tps.fotoKedatanganFileNames = fotoKedatangan;
tps.fotoKedatanganUploaded = Boolean(record.fotoKedatanganUploaded ?? record.FotoKedatanganUploaded) || fotoKedatangan.length > 0;
tps.fotoPetugas = [];
tps.fotoPetugasFileNames = fotoPetugas;
tps.fotoPetugasUploaded = Boolean(record.fotoPetugasUploaded ?? record.FotoPetugasUploaded) || fotoPetugas.length > 0;
tps.namaPetugas = record.namaPetugas || record.NamaPetugas || tps.namaPetugas;
tps.totalOrganik = Number(record.totalOrganik ?? record.TotalOrganik ?? tps.totalOrganik) || 0;
tps.totalAnorganik = Number(record.totalAnorganik ?? record.TotalAnorganik ?? tps.totalAnorganik) || 0;
tps.totalResidu = Number(record.totalResidu ?? record.TotalResidu ?? tps.totalResidu) || 0;
tps.totalTimbangan = Number(record.totalTimbangan ?? record.TotalTimbangan ?? tps.totalTimbangan) || 0;
tps.submitted = Boolean(record.isSubmit ?? record.IsSubmit ?? record.submitted ?? record.Submitted ?? tps.submitted);
const timbangan = record.timbangan || record.Timbangan || [];
if (Array.isArray(timbangan) && timbangan.length > 0) {
tps.timbangan = timbangan.map(item => ({
file: null,
fotoFileName: item.fotoFileName || item.FotoFileName || '',
berat: [Number(item.berat ?? (Array.isArray(item.Berat) ? item.Berat[0] : item.Berat) ?? 0) || 0],
jenisSampah: [normalizeJenisSampahValue(item.jenisSampah ?? item.JenisSampah)],
lokasiAngkut: [],
uploaded: Boolean(item.uploaded ?? item.isUploaded ?? item.IsUploaded ?? item.fotoFileName ?? item.FotoFileName),
ocrInfo: item.ocrInfo || item.OcrInfo || (item.fotoFileName || item.FotoFileName ? 'Foto dari server.' : 'OCR: diproses.')
}));
} else {
tps.timbangan = [];
}
}
async function loadRecordForCurrentSpj() {
const tps = tpsData[activeTpsIndex];
if (!tps || !nomorSpj) return;
const params = new URLSearchParams({ nomorSpj });
if (tps.spjDetailId) params.set('spjDetailId', tps.spjDetailId);
if (tps.lokasiAngkutId) params.set('lokasiAngkutId', tps.lokasiAngkutId);
if (tps.name) params.set('namaTps', tps.name);
try {
const res = await fetch(`${RECORD_DETAIL_ENDPOINT}?${params.toString()}`, { cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
if (!data.success || !data.hasData || !data.item) return;
applyServerRecordToTps(data.item);
} catch (error) {
console.warn('Gagal memuat data non-TPS:', error);
}
}
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 = "TPS 1";
function isBrowserFile(file) {
return file instanceof File;
}
function resolveStoredPhoto(file) {
return isBrowserFile(file) ? file : null;
}
function getStoredPhotoUrl(file) {
if (isBrowserFile(file)) return URL.createObjectURL(file);
if (typeof file === 'string') return file;
return '';
}
function getStoredPhotoName(file, defaultName = 'Foto') {
if (isBrowserFile(file)) return file.name;
if (typeof file === 'string') return file.split('/').pop() || defaultName;
return defaultName;
}
function getStoredPhotoSize(file) {
if (isBrowserFile(file)) return file.size;
return 0;
}
function initializeLocation() {
tpsData = [{
name: DEFAULT_TPS_NAME,
index: 0,
lokasiAngkutId: '',
spjDetailId: '',
latitude: '',
longitude: '',
alamatJalan: '',
waktuKedatangan: '',
fotoKedatangan: [],
fotoKedatanganFileNames: [],
fotoKedatanganUploaded: false,
timbangan: [],
totalOrganik: 0,
totalAnorganik: 0,
totalResidu: 0,
totalTimbangan: 0,
fotoPetugas: [],
fotoPetugasFileNames: [],
fotoPetugasUploaded: false,
namaPetugas: '',
submitted: false
}];
}
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);
const _form = tpsContentContainer.querySelector('form');
if (_form) {
const lokasiInput = _form.querySelector('.tps-lokasi-angkut-id');
const spjInput = _form.querySelector('.tps-spj-detail-id');
if (lokasiInput) lokasiInput.value = tpsData[0].lokasiAngkutId;
if (spjInput) spjInput.value = tpsData[0].spjDetailId;
}
} catch (error) {
console.warn('Gagal memuat detail penjemputan non-TPS:', error);
}
}
function applyDetailDataToView(detail, namaTps, namaPerusahaan) {
const titleEl = document.getElementById("detail-page-title");
const badgeEl = document.getElementById("detail-tps-badge");
const formBadgeEl = document.getElementById("detail-form-badge");
const companyEl = document.getElementById("detail-company-name");
const spjEl = document.getElementById("detail-spj-number");
const addressEl = document.getElementById("detail-address");
const platEl = document.getElementById("plat-nomor");
const doorEl = document.getElementById("detail-nomor-pintu");
if (titleEl) titleEl.textContent = namaTps;
if (badgeEl) badgeEl.textContent = namaTps;
if (formBadgeEl) formBadgeEl.textContent = namaTps;
if (companyEl && namaPerusahaan) companyEl.textContent = namaPerusahaan;
if (spjEl && detail.nomorSpj) spjEl.textContent = detail.nomorSpj;
if (addressEl && detail.alamat) addressEl.textContent = detail.alamat;
if (platEl && detail.platNomor) platEl.textContent = detail.platNomor;
if (doorEl && detail.nomorPintu) doorEl.textContent = detail.nomorPintu;
document.title = `Detail Penjemputan - ${namaTps}`;
}
function renderTpsForm() {
const tps = tpsData[activeTpsIndex];
const submitState = getSubmitState(tps);
const actionMarkup = tps.submitted
? `
Data ${tps.name || DEFAULT_TPS_NAME} sudah disubmit
`
: `
${submitState.canSubmit ? '' : `${submitState.message}
`}
`;
tpsContentContainer.innerHTML = `
`;
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) {
if (tps.fotoKedatangan.length > 0) {
renderStoredPhotos(tps.fotoKedatangan, previewKedatangan);
} else if (tps.fotoKedatanganUploaded && tps.fotoKedatanganFileNames.length > 0) {
renderServerImagePreview(tps.fotoKedatanganFileNames, previewKedatangan);
}
}
const previewPetugas = form.querySelector('.tps-preview-petugas');
if (previewPetugas) {
if (tps.fotoPetugas.length > 0) {
renderStoredPhotos(tps.fotoPetugas, previewPetugas);
} else if (tps.fotoPetugasUploaded && tps.fotoPetugasFileNames.length > 0) {
renderServerImagePreview(tps.fotoPetugasFileNames, previewPetugas);
}
}
}
function renderStoredPhotos(files, container) {
container.innerHTML = "";
container.className = "space-y-2";
files.forEach((file, index) => {
const item = document.createElement("div");
item.className =
"rounded-xl border border-gray-200 overflow-hidden bg-black";
const imageUrl = getStoredPhotoUrl(file);
const safeName = getStoredPhotoName(file, `Foto ${index + 1}`).replace(
/"/g,
""",
);
const fileSize = getStoredPhotoSize(file);
item.innerHTML = `
`;
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];
if (tps.submitted) return;
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);
scheduleAutoSave();
});
namaPetugasInput.addEventListener('blur', function() {
tps.namaPetugas = this.value;
scheduleAutoSave();
});
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);
},
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 = `
`;
const img = item.querySelector(".preview-multi-image");
if (img) {
img.onload = function () {
URL.revokeObjectURL(imageUrl);
};
}
previewContainer.appendChild(item);
});
}
function formatWeightDisplay(value) {
if (isNaN(value)) return "0,00";
return value.toFixed(2).replace(".", ",");
}
function parseWeightInput(value) {
if (!value) return 0;
const cleaned = value
.toString()
.trim()
.replace(/\s/g, "")
.replace(",", ".");
const parsed = parseFloat(cleaned);
return isNaN(parsed) ? 0 : parsed;
}
async function applyWatermark(file, photoNumber) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.onload = function () {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
const now = new Date();
const days = [
"Minggu",
"Senin",
"Selasa",
"Rabu",
"Kamis",
"Jumat",
"Sabtu",
];
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"Mei",
"Jun",
"Jul",
"Agu",
"Sep",
"Okt",
"Nov",
"Des",
];
const timestamp = `${days[now.getDay()]}, ${now.getDate().toString().padStart(2, "0")} ${months[now.getMonth()]} ${now.getFullYear()} • ${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`;
const baseFontSize = Math.max(
16,
Math.min(img.width, img.height) * 0.025,
);
const fontFamily = "'Montserrat', 'Segoe UI', 'Roboto', sans-serif";
const lines = [
{
text: `FOTO TIMBANG #${photoNumber}`,
size: baseFontSize * 1.1,
weight: "900",
color: "#FFD700",
},
{
text: `${nomorSpj}`,
size: baseFontSize * 0.9,
weight: "700",
color: "#FFFFFF",
},
{
text: timestamp,
size: baseFontSize * 0.75,
weight: "500",
color: "#E2E8F0",
},
];
const paddingX = baseFontSize * 1.2;
const paddingY = baseFontSize * 1.0;
const lineGap = baseFontSize * 0.4;
let maxWidth = 0;
let totalHeight = 0;
lines.forEach((line) => {
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
const metrics = ctx.measureText(line.text);
maxWidth = Math.max(maxWidth, metrics.width);
totalHeight += line.size + lineGap;
});
totalHeight -= lineGap;
const margin = baseFontSize * 1.5;
const boxWidth = maxWidth + paddingX * 2;
const boxHeight = totalHeight + paddingY * 2;
const boxX = img.width - boxWidth - margin;
const boxY = img.height - boxHeight - margin;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, baseFontSize * 0.8);
} else {
ctx.rect(boxX, boxY, boxWidth, boxHeight);
}
ctx.fillStyle = "rgba(15, 23, 42, 0.85)";
ctx.fill();
const accentWidth = baseFontSize * 0.3;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(
boxX + boxWidth - accentWidth,
boxY,
accentWidth,
boxHeight,
[0, baseFontSize * 0.8, baseFontSize * 0.8, 0],
);
} else {
ctx.rect(
boxX + boxWidth - accentWidth,
boxY,
accentWidth,
boxHeight,
);
}
ctx.fillStyle = "#FFD700";
ctx.fill();
ctx.textAlign = "right";
ctx.textBaseline = "top";
let currentY = boxY + paddingY;
const textRightLimit = boxX + boxWidth - paddingX - accentWidth;
lines.forEach((line) => {
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
ctx.fillStyle = line.color;
ctx.fillText(line.text, textRightLimit, currentY);
currentY += line.size + lineGap;
});
canvas.toBlob(
function (blob) {
const watermarkedFile = new File([blob], file.name, {
type: "image/jpeg",
lastModified: Date.now(),
});
resolve(watermarkedFile);
},
"image/jpeg",
0.95,
);
};
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function readFileAsImage(file) {
return new Promise(function (resolve, reject) {
const objectUrl = URL.createObjectURL(file);
const img = new Image();
img.onload = function () {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = function () {
URL.revokeObjectURL(objectUrl);
reject(new Error("Gagal membaca gambar."));
};
img.src = objectUrl;
});
}
function createCropCanvas(img, area) {
const sx = Math.max(0, Math.floor(img.width * area.x));
const sy = Math.max(0, Math.floor(img.height * area.y));
const sw = Math.max(1, Math.floor(img.width * area.w));
const sh = Math.max(1, Math.floor(img.height * area.h));
const canvas = document.createElement("canvas");
const scale = 2.2;
canvas.width = Math.max(1, Math.floor(sw * scale));
canvas.height = Math.max(1, Math.floor(sh * scale));
const ctx = canvas.getContext("2d");
if (!ctx) return canvas;
ctx.imageSmoothingEnabled = true;
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
return canvas;
}
function canvasToJpegFile(canvas, fileName) {
return new Promise(function (resolve, reject) {
canvas.toBlob(
function (blob) {
if (!blob) {
reject(new Error("Gagal membuat crop image."));
return;
}
resolve(new File([blob], fileName, { type: "image/jpeg" }));
},
"image/jpeg",
0.92,
);
});
}
async function requestOpenRouterWeight(imageFile) {
const formData = new FormData();
formData.append("Foto", imageFile);
const response = await fetch("/upst/detail-penjemputan/ocr-timbangan", {
method: "POST",
body: formData,
});
const result = await response.json();
if (!response.ok) throw new Error(result.message || "Request OCR gagal.");
return result;
}
async function autoFillWeight(file, weightInput, ocrInfoEl) {
let guessedWeight = 0;
weightInput.placeholder = "Membaca angka dari foto...";
if (ocrInfoEl) ocrInfoEl.textContent = "AI: memproses gambar...";
try {
const img = await readFileAsImage(file);
let bestRawText = "";
let isSuccess = false;
for (const area of OCR_AREAS) {
const cropCanvas = createCropCanvas(img, area);
const cropFile = await canvasToJpegFile(
cropCanvas,
`crop-${area.id}.jpg`,
);
const aiResult = await requestOpenRouterWeight(cropFile);
if (aiResult && aiResult.success && aiResult.weight) {
guessedWeight = parseWeightInput(aiResult.weight);
bestRawText = aiResult.raw || aiResult.weight;
isSuccess = guessedWeight > 0;
if (isSuccess) break;
}
if (aiResult && aiResult.raw) bestRawText = aiResult.raw;
}
if (ocrInfoEl) {
const cleaned = (bestRawText || "").replace(/\s+/g, " ").trim();
ocrInfoEl.textContent = isSuccess
? `AI terbaca: ${cleaned}`
: cleaned
? `AI tidak valid: ${cleaned}`
: "AI tidak menemukan angka valid.";
}
} catch (_) {
guessedWeight = 0;
if (ocrInfoEl) ocrInfoEl.textContent = "AI gagal diproses.";
}
if (guessedWeight > 0) {
weightInput.value = formatWeightDisplay(guessedWeight);
weightInput.placeholder = "Berat terdeteksi otomatis";
} else {
weightInput.placeholder = "Tidak terbaca otomatis, isi manual";
}
updateTpsTotalTimbangan();
}
function updateTpsTotalTimbangan() {
const tps = tpsData[activeTpsIndex];
const form = tpsContentContainer.querySelector("form");
if (!form) return;
let totalOrganik = 0.0;
let totalAnorganik = 0.0;
let totalResidu = 0.0;
const repeater = form.querySelector(".tps-timbangan-repeater");
const items = repeater.querySelectorAll(".timbangan-item");
items.forEach(function (item) {
const weightInput = item.querySelector(".input-berat-timbangan-value");
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
if (weightInput && jenisSampahSelect) {
const value = parseWeightInput(weightInput.value || "0");
const jenis = jenisSampahSelect.value;
if (jenis === "Organik") totalOrganik += value;
else if (jenis === "Anorganik") totalAnorganik += value;
else totalResidu += value;
}
});
tps.totalOrganik = totalOrganik;
tps.totalAnorganik = totalAnorganik;
tps.totalResidu = totalResidu;
tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu;
const displayTotal = form.querySelector(".tps-display-total");
if (displayTotal)
displayTotal.textContent = formatWeightDisplay(tps.totalTimbangan);
if (grandTotalDisplay)
grandTotalDisplay.textContent = formatWeightDisplay(tps.totalTimbangan);
if (grandTotalOrganikDisplay)
grandTotalOrganikDisplay.textContent = formatWeightDisplay(totalOrganik);
if (grandTotalAnorganikDisplay)
grandTotalAnorganikDisplay.textContent =
formatWeightDisplay(totalAnorganik);
if (grandTotalResiduDisplay)
grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu);
}
function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) {
if (isUploaded) {
return `
✓ Foto timbangan sudah diupload
Jika ingin revisi, pilih file baru diatas. Status upload akan tereset otomatis.
`;
}
if (!hasFile) {
return 'Pilih foto timbangan terlebih dahulu
';
}
if (!hasValidWeight) {
return `
Isi berat manual dulu sebelum upload jika berat tidak terbaca.
`;
}
return `
Foto siap diupload.
`;
}
function getKedatanganUploadStateMarkup(tps) {
if (tps.fotoKedatanganUploaded) {
return '✓ Foto kedatangan sudah diupload
';
}
if (!tps.fotoKedatangan.length) {
return "";
}
return ``;
}
function refreshKedatanganUploadState(form) {
const stateContainer = form.querySelector(".kedatangan-upload-state");
if (!stateContainer) return;
const tps = tpsData[activeTpsIndex];
stateContainer.innerHTML = getKedatanganUploadStateMarkup(tps);
const btnUploadKedatangan = stateContainer.querySelector(
".tps-btn-upload-kedatangan",
);
if (btnUploadKedatangan) {
btnUploadKedatangan.addEventListener("click", uploadFotoKedatangan);
}
refreshSubmitButtonState(form);
}
function getPetugasUploadStateMarkup(tps) {
if (tps.fotoPetugasUploaded) {
return '✓ Foto petugas sudah diupload
';
}
if (!tps.fotoPetugas.length) {
return "";
}
if (!tps.namaPetugas.trim()) {
return `
Isi nama petugas terlebih dahulu
`;
}
return ``;
}
function refreshPetugasUploadState(form) {
const stateContainer = form.querySelector(".petugas-upload-state");
if (!stateContainer) return;
const tps = tpsData[activeTpsIndex];
stateContainer.innerHTML = getPetugasUploadStateMarkup(tps);
const btnUploadPetugas = stateContainer.querySelector(
".tps-btn-upload-petugas:not([disabled])",
);
if (btnUploadPetugas) {
btnUploadPetugas.addEventListener("click", uploadFotoPetugas);
}
refreshSubmitButtonState(form);
}
function isTimbanganItemReady(timbanganItem) {
const weight = timbanganItem?.berat && timbanganItem.berat.length > 0 ? timbanganItem.berat[0] : 0;
const hasFile = Boolean(timbanganItem?.file) || Boolean(timbanganItem?.fotoFileName);
return hasFile && Boolean(timbanganItem?.uploaded) && weight > 0;
}
function getSubmitState(tps) {
if (tps?.submitted) {
return { canSubmit: false, message: '' };
}
if (!tps.fotoKedatanganUploaded) {
if (!tps.fotoKedatangan.length)
return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan.' };
return { canSubmit: false, message: 'Silakan upload foto kedatangan terlebih dahulu.' };
}
if (!tps.timbangan.length) {
return {
canSubmit: false,
message: "Tambahkan minimal 1 data timbangan sebelum submit.",
};
}
if (tps.timbangan.some((item) => !isTimbanganItemReady(item))) {
return {
canSubmit: false,
message:
"Pastikan semua foto timbangan sudah diupload dan beratnya valid.",
};
}
if (!tps.fotoPetugasUploaded) {
if (!tps.fotoPetugas.length)
return { canSubmit: false, message: 'Silakan pilih dan upload foto petugas.' };
return { canSubmit: false, message: 'Silakan upload foto petugas terlebih dahulu.' };
}
if (!tps.namaPetugas.trim()) {
return {
canSubmit: false,
message: "Isi nama petugas dulu sebelum submit.",
};
}
return { canSubmit: true, message: "" };
}
function refreshSubmitButtonState(form) {
const submitButton = form.querySelector('button[type="submit"]');
const tps = tpsData[activeTpsIndex];
if (tps?.submitted || !submitButton) return;
const helperText = form.querySelector(".submit-state-message");
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 = Boolean(currentData?.file || fileInput?.files?.[0] || currentData?.fotoFileName);
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 hasFileBlob = Boolean(existingData?.file);
const hasFile = Boolean(existingData?.file || existingData?.fotoFileName);
const isUploaded = Boolean(existingData?.uploaded);
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
item.innerHTML = `
Item Timbangan #${photoNumber}
${ocrInfoText}
${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}
`;
const fileInput = item.querySelector(".input-foto-timbangan");
const previewWrap = item.querySelector(".input-preview-wrap");
const previewImage = item.querySelector(".input-preview-image");
const ocrInfoEl = item.querySelector(".input-ocr-info");
const weightInputDisplay = item.querySelector(
".input-berat-timbangan-display",
);
const weightInputValue = item.querySelector(".input-berat-timbangan-value");
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
const removeBtn = item.querySelector(".btn-remove-timbangan");
if (existingData && existingData.file) {
const localUrl = URL.createObjectURL(existingData.file);
previewImage.src = localUrl;
previewWrap.classList.remove('hidden');
previewImage.onload = function() { URL.revokeObjectURL(localUrl); };
} else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) {
previewImage.src = existingData.fotoFileName;
previewWrap.classList.remove('hidden');
}
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);
scheduleAutoSave();
});
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);
scheduleAutoSave();
});
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,
fotoFileName: previousTimbangan[index]?.fotoFileName || '',
berat: [weight],
jenisSampah: [jenisSampah],
lokasiAngkut: [],
uploaded: previousTimbangan[index]?.uploaded ?? false,
ocrInfo
});
});
}
function buildSubmitFormData(tps) {
const formData = new FormData();
formData.append("NomorSpj", nomorSpj || "");
formData.append("TpsName", tps.name || DEFAULT_TPS_NAME);
formData.append("LokasiAngkutId", tps.lokasiAngkutId || "");
formData.append("SpjDetailId", tps.spjDetailId || "");
formData.append("Latitude", tps.latitude);
formData.append("Longitude", tps.longitude);
formData.append("AlamatJalan", tps.alamatJalan);
formData.append("WaktuKedatangan", tps.waktuKedatangan);
formData.append("TotalTimbangan", tps.totalTimbangan);
formData.append("TotalOrganik", tps.totalOrganik);
formData.append("TotalAnorganik", tps.totalAnorganik);
formData.append("TotalResidu", tps.totalResidu);
formData.append("NamaPetugas", tps.namaPetugas);
tps.fotoKedatangan.forEach((file) => {
if (isBrowserFile(file)) {
formData.append("FotoKedatangan", file);
}
});
tps.timbangan.forEach((timb) => {
if (isBrowserFile(timb.file)) formData.append("FotoTimbangan", timb.file);
const weight = timb.berat && timb.berat.length > 0 ? timb.berat[0] : 0;
const jenis =
timb.jenisSampah && timb.jenisSampah.length > 0
? timb.jenisSampah[0]
: DEFAULT_JENIS;
formData.append("BeratTimbangan", weight);
formData.append("JenisSampahList", jenis);
});
tps.fotoPetugas.forEach((file) => {
if (isBrowserFile(file)) {
formData.append("FotoPetugas", file);
}
});
const antiForgeryTokenEl = document.querySelector(
'#upst-antiforgery input[name="__RequestVerificationToken"]',
);
if (antiForgeryTokenEl && antiForgeryTokenEl.value) {
formData.append("__RequestVerificationToken", antiForgeryTokenEl.value);
}
return formData;
}
async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) {
const tps = tpsData[activeTpsIndex];
if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) {
showToast('Belum ada foto timbangan yang dipilih!', 'error');
return;
}
const weight = tps.timbangan[itemIndex].berat && tps.timbangan[itemIndex].berat.length > 0
? tps.timbangan[itemIndex].berat[0]
: 0;
if (weight <= 0) {
showToast('Isi berat manual dulu sebelum upload foto timbangan.', 'error');
return;
}
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;
}
const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null;
if (uploadBtn) { uploadBtn.disabled = true; uploadBtn.textContent = 'Mengupload...'; }
const formData = new FormData();
formData.append('FotoTimbangan', tps.timbangan[itemIndex].file);
formData.append('NomorSpj', nomorSpj || '');
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('ItemIndex', itemIndex);
formData.append('JenisSampah', (tps.timbangan[itemIndex].jenisSampah && tps.timbangan[itemIndex].jenisSampah[0]) || DEFAULT_JENIS);
formData.append('Berat', weight.toFixed(2));
try {
const res = await fetch('/upst/detail-penjemputan/upload-foto-timbangan-non-tps', { method: 'POST', body: formData });
const data = await res.json();
if (res.ok && data.success) {
tps.timbangan[itemIndex].uploaded = true;
tps.timbangan[itemIndex].fotoFileName = data.fileUrl || data.fileName || '';
const fotoInputTimb = targetItem?.querySelector('.input-foto-timbangan');
if (fotoInputTimb) fotoInputTimb.value = '';
showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success');
} else {
showToast(data.message || 'Gagal upload foto timbangan.', 'error');
if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.textContent = 'Upload Foto Timbangan Ini'; }
}
} catch {
showToast('Koneksi gagal saat upload foto timbangan.', 'error');
if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.textContent = 'Upload Foto Timbangan Ini'; }
}
if (targetItem) refreshTimbanganUploadState(targetItem);
syncTimbanganToTpsData();
const form = tpsContentContainer.querySelector('form');
if (form) refreshSubmitButtonState(form);
scheduleAutoSave();
}
async function uploadFotoKedatangan() {
const tps = tpsData[activeTpsIndex];
if (tps.fotoKedatangan.length === 0) {
showToast('Belum ada foto kedatangan yang dipilih!', 'error');
return;
}
const form = tpsContentContainer.querySelector('form');
const btn = form ? form.querySelector('.tps-btn-upload-kedatangan') : null;
if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; }
const formData = new FormData();
tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f));
formData.append('NomorSpj', nomorSpj || '');
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('WaktuKedatangan', tps.waktuKedatangan || '');
formData.append('Latitude', tps.latitude || '');
formData.append('Longitude', tps.longitude || '');
formData.append('AlamatJalan', tps.alamatJalan || '');
try {
const res = await fetch('/upst/detail-penjemputan/upload-foto-kedatangan-non-tps', { method: 'POST', body: formData });
const data = await res.json();
if (res.ok && data.success) {
tps.fotoKedatanganUploaded = true;
tps.fotoKedatanganFileNames = data.fileUrls || data.fileNames || [];
showToast(data.message || 'Foto kedatangan berhasil diupload.', 'success');
if (form) {
const fotoInput = form.querySelector('.tps-foto-kedatangan');
if (fotoInput) fotoInput.value = '';
refreshKedatanganUploadState(form);
}
scheduleAutoSave();
} else {
showToast(data.message || 'Gagal upload foto kedatangan.', 'error');
if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; }
}
} catch {
showToast('Koneksi gagal saat upload foto kedatangan.', 'error');
if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; }
}
}
async function uploadFotoPetugas() {
const tps = tpsData[activeTpsIndex];
if (tps.fotoPetugas.length === 0) {
showToast('Belum ada foto petugas yang dipilih!', 'error');
return;
}
if (!tps.namaPetugas.trim()) {
showToast('Nama petugas wajib diisi sebelum upload foto petugas!', 'error');
return;
}
const form = tpsContentContainer.querySelector('form');
const btn = form ? form.querySelector('.tps-btn-upload-petugas:not([disabled])') : null;
if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; }
const formData = new FormData();
tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f));
formData.append('NomorSpj', nomorSpj || '');
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('NamaPetugas', tps.namaPetugas);
try {
const res = await fetch('/upst/detail-penjemputan/upload-foto-petugas-non-tps', { method: 'POST', body: formData });
const data = await res.json();
if (res.ok && data.success) {
tps.fotoPetugasUploaded = true;
tps.fotoPetugasFileNames = data.fileUrls || data.fileNames || [];
showToast(data.message || 'Foto petugas berhasil diupload.', 'success');
if (form) {
const fotoInput = form.querySelector('.tps-foto-petugas');
if (fotoInput) fotoInput.value = '';
refreshPetugasUploadState(form);
}
scheduleAutoSave();
} else {
showToast(data.message || 'Gagal upload foto petugas.', 'error');
if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; }
}
} catch {
showToast('Koneksi gagal saat upload foto petugas.', 'error');
if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; }
}
}
async function submitTpsData() {
const tps = tpsData[activeTpsIndex];
const submitState = getSubmitState(tps);
if (!submitState.canSubmit) { showToast(submitState.message, 'error'); return; }
const form = tpsContentContainer.querySelector('form');
const submitBtn = form ? form.querySelector('button[type="submit"]') : null;
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Menyimpan...'; }
const formData = buildSubmitFormData(tps);
try {
const res = await fetch('/upst/detail-penjemputan', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: formData
});
const result = await res.json().catch(() => null);
if (res.ok && result?.success) {
tps.submitted = true;
showToast(result.message || 'Data berhasil disimpan!', 'success');
setTimeout(() => { window.location.href = '/upst/detail-penjemputan/detail-selesai-tanpa-tps'; }, 1500);
} else {
showToast(result?.message || 'Gagal submit data.', 'error');
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; }
}
} catch {
showToast('Koneksi gagal saat submit.', 'error');
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; }
}
}
function showToast(message, type = 'info') {
let container = document.getElementById('espj-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'espj-toast-container';
container.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;';
document.body.appendChild(container);
}
const toast = document.createElement('div');
const bg = type === 'success' ? '#16a34a' : type === 'error' ? '#dc2626' : '#2563eb';
toast.style.cssText = `background:${bg};color:#fff;padding:10px 18px;border-radius:16px;font-size:13px;font-weight:600;box-shadow:0 4px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;text-align:center;`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => { toast.style.opacity = '1'; });
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3200);
}
const nomorSpjEl = document.querySelector(".text-gray-600.font-mono");
if (nomorSpjEl && nomorSpjEl.textContent) {
nomorSpj = nomorSpjEl.textContent.trim();
}
showLoadingOverlay();
try {
initializeLocation();
await loadDetailData();
await loadRecordForCurrentSpj();
renderTpsForm();
} finally {
hideLoadingOverlay();
}
function renderServerImagePreview(fileUrls, container) {
container.innerHTML = '';
if (!fileUrls || fileUrls.length === 0) return;
container.className = 'space-y-2';
fileUrls.forEach((url, index) => {
const item = document.createElement('div');
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
const isUrl = typeof url === 'string' && (url.startsWith('/') || url.startsWith('http'));
if (isUrl) {
item.innerHTML = `
`;
} else {
item.className = 'rounded-xl border border-green-200 bg-green-50 h-44 flex items-center justify-center';
item.innerHTML = `✓ Foto ${index + 1}
`;
}
container.appendChild(item);
});
}
});