2173 lines
79 KiB
JavaScript
2173 lines
79 KiB
JavaScript
const DetailPenjemputan = (function () {
|
|
"use strict";
|
|
|
|
const CONFIG = {
|
|
OCR_AREAS: [
|
|
{ id: 'A', x: 0.34, y: 0.35, w: 0.40, h: 0.11, color: 'border-lime-400 bg-lime-500/15' },
|
|
{ id: 'B', x: 0.31, y: 0.33, w: 0.45, h: 0.14, color: 'border-amber-300 bg-amber-400/10' },
|
|
{ id: 'C', x: 0.29, y: 0.31, w: 0.49, h: 0.17, color: 'border-cyan-300 bg-cyan-400/10' }
|
|
],
|
|
JENIS_SAMPAH: ['Organik', 'Anorganik', 'Residu'],
|
|
DEFAULT_JENIS: 'Residu'
|
|
};
|
|
|
|
const ENDPOINTS = {
|
|
saveDraft: '/upst/detail-penjemputan/save-draft',
|
|
loadDraft: '/upst/detail-penjemputan/load-draft',
|
|
deleteDraft: '/upst/detail-penjemputan/delete-draft',
|
|
uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan',
|
|
uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan',
|
|
uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas',
|
|
submit: '/upst/detail-penjemputan'
|
|
};
|
|
|
|
let state = {
|
|
activeTpsIndex: 0,
|
|
tpsData: [],
|
|
availableTpsList: [],
|
|
selectedTpsList: [],
|
|
hasRequestedLocation: [],
|
|
nomorSpj: "SPJ/07-2025/PKM/000476",
|
|
};
|
|
|
|
const STORAGE_KEY = "detailPenjemputanTpsState";
|
|
|
|
function saveState() {
|
|
try {
|
|
const stateCopy = JSON.parse(
|
|
JSON.stringify(state, (key, value) => {
|
|
if (value instanceof File) {
|
|
return { name: value.name, size: value.size, type: value.type };
|
|
}
|
|
return value;
|
|
}),
|
|
);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(stateCopy));
|
|
} catch (e) {
|
|
console.warn("Failed to save state to localStorage:", e);
|
|
}
|
|
}
|
|
|
|
function loadState() {
|
|
try {
|
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
if (saved) {
|
|
const parsed = JSON.parse(saved);
|
|
state = { ...state, ...parsed };
|
|
}
|
|
} catch (e) {
|
|
console.warn("Failed to load state from localStorage:", e);
|
|
}
|
|
}
|
|
|
|
function clearState() {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
}
|
|
|
|
function isBrowserFile(value) {
|
|
return typeof File !== "undefined" && value instanceof File;
|
|
}
|
|
|
|
function getFirstValue(value) {
|
|
return Array.isArray(value) ? value[0] : value;
|
|
}
|
|
|
|
function resolveStoredPhoto(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
if (isBrowserFile(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
return {
|
|
url: value,
|
|
name: value.split("/").pop() || "Foto",
|
|
size: 0,
|
|
source: "api",
|
|
};
|
|
}
|
|
|
|
if (typeof value !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const url =
|
|
value.url ||
|
|
value.fileUrl ||
|
|
value.path ||
|
|
value.filePath ||
|
|
value.src ||
|
|
value.fotoUrl ||
|
|
value.photoUrl ||
|
|
value.fotoFileName ||
|
|
value.fileName ||
|
|
"";
|
|
|
|
return {
|
|
...value,
|
|
url,
|
|
name:
|
|
value.name ||
|
|
value.fileName ||
|
|
value.file_name ||
|
|
(url ? url.split("/").pop() : "Foto"),
|
|
size: Number(value.size || value.fileSize || 0) || 0,
|
|
source: value.source || "api",
|
|
};
|
|
}
|
|
|
|
function getStoredPhotoUrl(value) {
|
|
const photo = resolveStoredPhoto(value);
|
|
if (!photo) {
|
|
return "";
|
|
}
|
|
|
|
if (isBrowserFile(photo)) {
|
|
return URL.createObjectURL(photo);
|
|
}
|
|
|
|
return photo.url || "";
|
|
}
|
|
|
|
function getStoredPhotoName(value, fallbackName = "Foto") {
|
|
const photo = resolveStoredPhoto(value);
|
|
if (!photo) {
|
|
return fallbackName;
|
|
}
|
|
|
|
return isBrowserFile(photo)
|
|
? photo.name || fallbackName
|
|
: photo.name || fallbackName;
|
|
}
|
|
|
|
function getStoredPhotoSize(value) {
|
|
const photo = resolveStoredPhoto(value);
|
|
if (!photo) {
|
|
return 0;
|
|
}
|
|
|
|
return isBrowserFile(photo)
|
|
? photo.size || 0
|
|
: Number(photo.size || 0) || 0;
|
|
}
|
|
|
|
function hasStoredPhoto(value) {
|
|
const photo = resolveStoredPhoto(value);
|
|
if (!photo) {
|
|
return false;
|
|
}
|
|
|
|
return isBrowserFile(photo) || Boolean(photo.url || photo.name);
|
|
}
|
|
|
|
function normalizePhotoList(value) {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
|
|
return value.map(resolveStoredPhoto).filter(Boolean);
|
|
}
|
|
|
|
function normalizeTimbanganItem(item) {
|
|
if (!item) {
|
|
return {
|
|
file: null,
|
|
weight: 0,
|
|
jenisSampah: CONFIG.DEFAULT_JENIS,
|
|
uploaded: false,
|
|
ocrInfo: "OCR: belum diproses.",
|
|
};
|
|
}
|
|
|
|
const file = resolveStoredPhoto(
|
|
item.file ||
|
|
item.foto ||
|
|
item.photo ||
|
|
item.fotoUrl ||
|
|
item.photoUrl ||
|
|
item.fotoFileName,
|
|
);
|
|
const weight = Number(item.weight ?? getFirstValue(item.berat) ?? 0) || 0;
|
|
const jenisSampah =
|
|
getFirstValue(item.jenisSampah) ||
|
|
item.JenisSampah ||
|
|
CONFIG.DEFAULT_JENIS;
|
|
const uploaded = Boolean(
|
|
item.uploaded ??
|
|
item.isUploaded ??
|
|
item.IsUploaded ??
|
|
hasStoredPhoto(file),
|
|
);
|
|
|
|
return {
|
|
...item,
|
|
file,
|
|
weight,
|
|
jenisSampah,
|
|
uploaded,
|
|
ocrInfo:
|
|
item.ocrInfo ||
|
|
item.OcrInfo ||
|
|
(uploaded ? "Foto dari server." : "OCR: belum diproses."),
|
|
};
|
|
}
|
|
|
|
function findMatchingApiTps(apiList, currentTps, index) {
|
|
return (
|
|
apiList.find(
|
|
(item) =>
|
|
(currentTps.spjDetailId &&
|
|
(item.spjDetailId === currentTps.spjDetailId ||
|
|
item.SpjDetailID === currentTps.spjDetailId)) ||
|
|
(currentTps.lokasiAngkutId &&
|
|
(item.lokasiAngkutId === currentTps.lokasiAngkutId ||
|
|
item.LokasiAngkutID === currentTps.lokasiAngkutId)) ||
|
|
(item.name || item.Name) === currentTps.name,
|
|
) || apiList[index]
|
|
);
|
|
}
|
|
|
|
function applyApiDraftData(draftData) {
|
|
const apiList = Array.isArray(draftData)
|
|
? draftData
|
|
: draftData?.tpsData || draftData?.draftPenjemputan || [];
|
|
|
|
if (!Array.isArray(apiList) || apiList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (!state.tpsData.length) {
|
|
initializeTpsData(apiList);
|
|
}
|
|
|
|
state.tpsData = state.tpsData.map((currentTps, index) => {
|
|
const apiTps = findMatchingApiTps(apiList, currentTps, index);
|
|
if (!apiTps) {
|
|
return currentTps;
|
|
}
|
|
|
|
const fotoKedatangan = normalizePhotoList(
|
|
apiTps.fotoKedatangan || apiTps.FotoKedatangan,
|
|
);
|
|
const fotoPetugas = normalizePhotoList(
|
|
apiTps.fotoPetugas || apiTps.FotoPetugas,
|
|
);
|
|
const apiTimbangan = apiTps.timbangan || apiTps.Timbangan;
|
|
const timbangan = Array.isArray(apiTimbangan)
|
|
? apiTimbangan.map(normalizeTimbanganItem)
|
|
: currentTps.timbangan;
|
|
|
|
return {
|
|
...currentTps,
|
|
name: apiTps.name || apiTps.Name || currentTps.name,
|
|
lokasiAngkutId:
|
|
apiTps.lokasiAngkutId ||
|
|
apiTps.LokasiAngkutID ||
|
|
currentTps.lokasiAngkutId,
|
|
spjDetailId:
|
|
apiTps.spjDetailId || apiTps.SpjDetailID || currentTps.spjDetailId,
|
|
latitude: apiTps.latitude || apiTps.Latitude || currentTps.latitude,
|
|
longitude: apiTps.longitude || apiTps.Longitude || currentTps.longitude,
|
|
alamatJalan:
|
|
apiTps.alamatJalan || apiTps.AlamatJalan || currentTps.alamatJalan,
|
|
waktuKedatangan:
|
|
apiTps.waktuKedatangan ||
|
|
apiTps.WaktuKedatangan ||
|
|
currentTps.waktuKedatangan,
|
|
fotoKedatangan: fotoKedatangan.length
|
|
? fotoKedatangan
|
|
: currentTps.fotoKedatangan,
|
|
fotoKedatanganUploaded:
|
|
Boolean(
|
|
apiTps.fotoKedatanganUploaded ?? apiTps.FotoKedatanganUploaded,
|
|
) ||
|
|
fotoKedatangan.length > 0 ||
|
|
currentTps.fotoKedatanganUploaded,
|
|
timbangan,
|
|
totalOrganik:
|
|
Number(
|
|
apiTps.totalOrganik ??
|
|
apiTps.TotalOrganik ??
|
|
currentTps.totalOrganik,
|
|
) || 0,
|
|
totalAnorganik:
|
|
Number(
|
|
apiTps.totalAnorganik ??
|
|
apiTps.TotalAnorganik ??
|
|
currentTps.totalAnorganik,
|
|
) || 0,
|
|
totalResidu:
|
|
Number(
|
|
apiTps.totalResidu ?? apiTps.TotalResidu ?? currentTps.totalResidu,
|
|
) || 0,
|
|
totalTimbangan:
|
|
Number(
|
|
apiTps.totalTimbangan ??
|
|
apiTps.TotalTimbangan ??
|
|
currentTps.totalTimbangan,
|
|
) || 0,
|
|
fotoPetugas: fotoPetugas.length ? fotoPetugas : currentTps.fotoPetugas,
|
|
fotoPetugasUploaded:
|
|
Boolean(apiTps.fotoPetugasUploaded ?? apiTps.FotoPetugasUploaded) ||
|
|
fotoPetugas.length > 0 ||
|
|
currentTps.fotoPetugasUploaded,
|
|
namaPetugas:
|
|
apiTps.namaPetugas || apiTps.NamaPetugas || currentTps.namaPetugas,
|
|
submitted: Boolean(
|
|
apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted,
|
|
),
|
|
};
|
|
});
|
|
|
|
state.selectedTpsList = state.tpsData.map((item) => item.name);
|
|
state.activeTpsIndex = Math.min(
|
|
state.activeTpsIndex,
|
|
Math.max(state.tpsData.length - 1, 0),
|
|
);
|
|
elements.tpsTabsContainer.style.display = "block";
|
|
|
|
if (state.tpsData.length === 1) {
|
|
renderSingleForm();
|
|
} else {
|
|
renderTabs();
|
|
renderTpsForm();
|
|
}
|
|
|
|
updateAllTotals();
|
|
saveState();
|
|
}
|
|
|
|
const elements = {
|
|
grandTotalDisplay: null,
|
|
tpsSelectionContainer: null,
|
|
tpsTabsContainer: null,
|
|
tpsCheckboxesContainer: null,
|
|
btnConfirmTps: null,
|
|
tpsTabsEl: null,
|
|
tpsContentContainer: null,
|
|
totalOrganikDisplay: null,
|
|
totalAnorganikDisplay: null,
|
|
totalResiduDisplay: null,
|
|
};
|
|
|
|
let autoSaveTimer = null;
|
|
let autoSaveStatusEl = null;
|
|
|
|
async function init(tpsList) {
|
|
initElements();
|
|
await initializeLocation(tpsList);
|
|
}
|
|
|
|
function initElements() {
|
|
elements.grandTotalDisplay = document.getElementById(
|
|
"grand-total-timbangan",
|
|
);
|
|
elements.tpsSelectionContainer = document.getElementById(
|
|
"tps-selection-container",
|
|
);
|
|
elements.tpsTabsContainer = document.getElementById("tps-tabs-container");
|
|
elements.tpsCheckboxesContainer = document.getElementById("tps-checkboxes");
|
|
elements.btnConfirmTps = document.getElementById("btn-confirm-tps");
|
|
elements.tpsTabsEl = document.getElementById("tps-tabs");
|
|
elements.tpsContentContainer = document.getElementById("tps-content");
|
|
elements.totalOrganikDisplay = document.getElementById(
|
|
"grand-total-organik",
|
|
);
|
|
elements.totalAnorganikDisplay = document.getElementById(
|
|
"grand-total-anorganik",
|
|
);
|
|
elements.totalResiduDisplay = document.getElementById("grand-total-residu");
|
|
|
|
if (elements.btnConfirmTps) {
|
|
elements.btnConfirmTps.addEventListener("click", handleConfirmTps);
|
|
}
|
|
}
|
|
|
|
function buildDraftKey(tps) {
|
|
const spjDetailId = (tps?.spjDetailId || '').trim();
|
|
const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim();
|
|
if (!spjDetailId && !lokasiAngkutId) return '';
|
|
return `tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
}
|
|
|
|
function getActiveTps() {
|
|
return state.tpsData[state.activeTpsIndex] || null;
|
|
}
|
|
|
|
function getAutoSaveStatusEl() {
|
|
if (!autoSaveStatusEl || !document.body.contains(autoSaveStatusEl)) {
|
|
autoSaveStatusEl = document.getElementById('auto-save-status');
|
|
}
|
|
return autoSaveStatusEl;
|
|
}
|
|
|
|
function showAutoSaveStatus(msg, isOk = false) {
|
|
const statusEl = getAutoSaveStatusEl();
|
|
if (!statusEl) return;
|
|
statusEl.textContent = msg;
|
|
statusEl.className = isOk
|
|
? 'text-[11px] text-green-600 text-center font-medium transition-opacity'
|
|
: 'text-[11px] text-amber-500 text-center font-medium transition-opacity';
|
|
statusEl.style.opacity = '1';
|
|
if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500);
|
|
}
|
|
|
|
function scheduleAutoSave() {
|
|
clearTimeout(autoSaveTimer);
|
|
showAutoSaveStatus('menyimpan...');
|
|
autoSaveTimer = setTimeout(autoSaveDraft, 1000);
|
|
}
|
|
|
|
async function autoSaveDraft() {
|
|
const tps = getActiveTps();
|
|
if (!tps || !tps.draftKey) return;
|
|
|
|
const payload = {
|
|
draftKey: tps.draftKey,
|
|
lokasiAngkutId: tps.lokasiAngkutId || '',
|
|
spjDetailId: tps.spjDetailId || '',
|
|
latitude: tps.latitude || '',
|
|
longitude: tps.longitude || '',
|
|
alamatJalan: tps.alamatJalan || '',
|
|
waktuKedatangan: tps.waktuKedatangan || '',
|
|
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
|
|
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
|
timbangan: (tps.timbangan || []).map(item => ({
|
|
berat: item.weight || 0,
|
|
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
|
|
fotoFileName: item.fotoFileName || '',
|
|
uploaded: item.uploaded || false,
|
|
ocrInfo: item.ocrInfo || ''
|
|
})),
|
|
totalOrganik: tps.totalOrganik || 0,
|
|
totalAnorganik: tps.totalAnorganik || 0,
|
|
totalResidu: tps.totalResidu || 0,
|
|
totalTimbangan: tps.totalTimbangan || 0,
|
|
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
|
|
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
|
|
namaPetugas: tps.namaPetugas || ''
|
|
};
|
|
|
|
try {
|
|
const res = await fetch(ENDPOINTS.saveDraft, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success);
|
|
} catch (_) {
|
|
showAutoSaveStatus('✗ Gagal simpan draft');
|
|
}
|
|
}
|
|
|
|
async function loadDraftForTps(tps) {
|
|
if (!tps || !tps.draftKey) return;
|
|
|
|
try {
|
|
const res = await fetch(`${ENDPOINTS.loadDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`);
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
if (!data.success || !data.hasDraft || !data.draft) return;
|
|
|
|
const draft = data.draft;
|
|
tps.lokasiAngkutId = draft.lokasiAngkutId || tps.lokasiAngkutId;
|
|
tps.spjDetailId = draft.spjDetailId || tps.spjDetailId;
|
|
tps.latitude = draft.latitude || '';
|
|
tps.longitude = draft.longitude || '';
|
|
tps.alamatJalan = draft.alamatJalan || '';
|
|
tps.waktuKedatangan = draft.waktuKedatangan || '';
|
|
tps.fotoKedatanganFileNames = draft.fotoKedatanganFileNames || [];
|
|
tps.fotoKedatanganUploaded = draft.fotoKedatanganUploaded || false;
|
|
tps.fotoPetugasFileNames = draft.fotoPetugasFileNames || [];
|
|
tps.fotoPetugasUploaded = draft.fotoPetugasUploaded || false;
|
|
tps.namaPetugas = draft.namaPetugas || '';
|
|
tps.totalOrganik = draft.totalOrganik || 0;
|
|
tps.totalAnorganik = draft.totalAnorganik || 0;
|
|
tps.totalResidu = draft.totalResidu || 0;
|
|
tps.totalTimbangan = draft.totalTimbangan || 0;
|
|
tps.timbangan = (draft.timbangan || []).map(item => ({
|
|
file: null,
|
|
fotoFileName: item.fotoFileName || '',
|
|
weight: item.berat || 0,
|
|
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
|
|
uploaded: item.uploaded || false,
|
|
ocrInfo: item.ocrInfo || 'OCR: belum diproses.'
|
|
}));
|
|
} catch (error) {
|
|
console.warn('Gagal memuat draft TPS:', error);
|
|
}
|
|
}
|
|
|
|
async function loadDraftsForAllTps() {
|
|
await Promise.all(state.tpsData.map(loadDraftForTps));
|
|
}
|
|
|
|
async function initializeLocation(tpsList) {
|
|
state.availableTpsList = tpsList || [];
|
|
if (elements.tpsSelectionContainer) {
|
|
elements.tpsSelectionContainer.style.display = 'none';
|
|
}
|
|
|
|
if (state.availableTpsList.length === 0) {
|
|
state.selectedTpsList = ['1 Lokasi TPS'];
|
|
initializeTpsData(state.selectedTpsList);
|
|
await loadDraftsForAllTps();
|
|
elements.tpsTabsContainer.style.display = 'block';
|
|
renderSingleForm();
|
|
return;
|
|
}
|
|
|
|
state.selectedTpsList = [...state.availableTpsList];
|
|
initializeTpsData(state.selectedTpsList);
|
|
await loadDraftsForAllTps();
|
|
elements.tpsTabsContainer.style.display = 'block';
|
|
|
|
if (state.selectedTpsList.length === 1) {
|
|
renderSingleForm();
|
|
} else {
|
|
renderTabs();
|
|
renderTpsForm();
|
|
}
|
|
}
|
|
|
|
function initializeTpsData(tpsNames) {
|
|
state.tpsData = tpsNames.map((tpsItem, index) => ({
|
|
name: typeof tpsItem === 'string' ? tpsItem : (tpsItem?.name || tpsItem?.Name || `TPS ${index + 1}`),
|
|
index: index,
|
|
lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''),
|
|
spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''),
|
|
draftKey: '',
|
|
latitude: '',
|
|
longitude: '',
|
|
alamatJalan: '',
|
|
waktuKedatangan: '',
|
|
fotoKedatangan: [],
|
|
fotoKedatanganFileNames: [],
|
|
fotoKedatanganUploaded: false,
|
|
timbangan: [],
|
|
totalOrganik: 0,
|
|
totalAnorganik: 0,
|
|
totalResidu: 0,
|
|
totalTimbangan: 0,
|
|
fotoPetugas: [],
|
|
fotoPetugasFileNames: [],
|
|
fotoPetugasUploaded: false,
|
|
namaPetugas: '',
|
|
submitted: false
|
|
})).map(tps => ({ ...tps, draftKey: buildDraftKey(tps) }));
|
|
state.hasRequestedLocation = new Array(tpsNames.length).fill(false);
|
|
}
|
|
|
|
function renderTpsSelection() {
|
|
elements.tpsCheckboxesContainer.innerHTML = "";
|
|
state.availableTpsList.forEach((tpsName) => {
|
|
const wrapper = document.createElement("label");
|
|
wrapper.className =
|
|
"flex items-center gap-3 p-3 rounded-xl border border-gray-200 hover:bg-gray-50 cursor-pointer transition";
|
|
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.value = tpsName;
|
|
checkbox.className =
|
|
"w-5 h-5 rounded border-gray-300 text-upst focus:ring-upst";
|
|
checkbox.checked = true;
|
|
|
|
const label = document.createElement("span");
|
|
label.className = "text-sm font-bold text-gray-700";
|
|
label.textContent = tpsName;
|
|
|
|
wrapper.appendChild(checkbox);
|
|
wrapper.appendChild(label);
|
|
elements.tpsCheckboxesContainer.appendChild(wrapper);
|
|
});
|
|
}
|
|
|
|
async function handleConfirmTps() {
|
|
const checkboxes = elements.tpsCheckboxesContainer.querySelectorAll('input[type="checkbox"]:checked');
|
|
state.selectedTpsList = Array.from(checkboxes).map(cb => cb.value);
|
|
|
|
if (state.selectedTpsList.length === 0) {
|
|
showToast('Pilih minimal 1 TPS untuk diangkut!', 'error');
|
|
return;
|
|
}
|
|
|
|
initializeTpsData(state.selectedTpsList);
|
|
await loadDraftsForAllTps();
|
|
elements.tpsSelectionContainer.style.display = 'none';
|
|
elements.tpsTabsContainer.style.display = 'block';
|
|
|
|
if (state.selectedTpsList.length === 1) {
|
|
renderSingleForm();
|
|
} else {
|
|
renderTabs();
|
|
renderTpsForm();
|
|
}
|
|
}
|
|
|
|
function renderSingleForm() {
|
|
elements.tpsTabsEl.style.display = "none";
|
|
state.activeTpsIndex = 0;
|
|
renderTpsForm();
|
|
}
|
|
|
|
function renderTabs() {
|
|
elements.tpsTabsEl.style.display = "block";
|
|
elements.tpsTabsEl.className = "mb-4";
|
|
|
|
const activeTps = state.tpsData[state.activeTpsIndex];
|
|
const isActiveSubmitted = !!(activeTps && activeTps.submitted);
|
|
const selectClass = isActiveSubmitted
|
|
? "w-full rounded-xl border border-green-300 bg-green-50 text-green-700 px-3 py-2 text-sm font-bold"
|
|
: "w-full rounded-xl border border-gray-200 bg-white text-gray-700 px-3 py-2 text-sm font-bold";
|
|
|
|
const optionsHtml = state.tpsData
|
|
.map((tps, index) => {
|
|
const marker = tps.submitted ? "✅ " : "";
|
|
return `<option value="${index}" ${index === state.activeTpsIndex ? "selected" : ""}>${marker}${tps.name}</option>`;
|
|
})
|
|
.join("");
|
|
|
|
elements.tpsTabsEl.innerHTML = `
|
|
<label class="block text-xs font-semibold text-gray-500 mb-1">Pilih TPS</label>
|
|
<select id="tps-dropdown" class="${selectClass}">
|
|
${optionsHtml}
|
|
</select>
|
|
`;
|
|
|
|
const dropdown = elements.tpsTabsEl.querySelector("#tps-dropdown");
|
|
if (dropdown) {
|
|
dropdown.addEventListener("change", (e) => {
|
|
const nextIndex = parseInt(e.target.value, 10);
|
|
if (!Number.isNaN(nextIndex)) {
|
|
switchToTps(nextIndex);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function switchToTps(index) {
|
|
state.activeTpsIndex = index;
|
|
renderTabs();
|
|
renderTpsForm();
|
|
|
|
if (!state.hasRequestedLocation[index]) {
|
|
state.hasRequestedLocation[index] = true;
|
|
getLocationUpdate();
|
|
}
|
|
|
|
updateAllTotals();
|
|
}
|
|
|
|
function renderTpsForm() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const showTpsName =
|
|
state.selectedTpsList.length > 1 || state.availableTpsList.length > 0;
|
|
const submitState = getSubmitState(tps);
|
|
|
|
elements.tpsContentContainer.innerHTML = `
|
|
<form class="space-y-5 pb-8" data-tps-index="${tps.index}">
|
|
<input type="hidden" class="tps-lokasi-angkut-id" value="${tps.lokasiAngkutId || ""}" />
|
|
<input type="hidden" class="tps-spj-detail-id" value="${tps.spjDetailId || ""}" />
|
|
<input type="hidden" class="tps-latitude" value="${tps.latitude}" />
|
|
<input type="hidden" class="tps-longitude" value="${tps.longitude}" />
|
|
<input type="hidden" class="tps-alamat-jalan" value="${tps.alamatJalan}" />
|
|
<input type="hidden" class="tps-total-timbangan" value="${tps.totalTimbangan}" />
|
|
<input type="hidden" class="tps-total-organik" value="${tps.totalOrganik}" />
|
|
<input type="hidden" class="tps-total-anorganik" value="${tps.totalAnorganik}" />
|
|
<input type="hidden" class="tps-total-residu" value="${tps.totalResidu}" />
|
|
|
|
${renderSection1Kedatangan(tps, showTpsName)}
|
|
${renderSection2Timbangan(tps, showTpsName)}
|
|
${renderSection3Petugas(tps)}
|
|
|
|
<div class="flex gap-3">
|
|
<a href="/upst/detail-penjemputan/batal" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
|
|
<button type="submit" ${submitState.canSubmit ? "" : "disabled"} class="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"}">Submit${showTpsName ? " " + tps.name : ""}</button>
|
|
</div>
|
|
${submitState.canSubmit ? '' : `<p class="submit-state-message text-[11px] text-center text-red-500 font-medium">${submitState.message}</p>`}
|
|
<p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p>
|
|
</form>
|
|
`;
|
|
|
|
attachTpsFormListeners();
|
|
restoreTpsTimbanganItems();
|
|
restorePhotoPreview();
|
|
}
|
|
|
|
function renderSection1Kedatangan(tps, showTpsName) {
|
|
return `
|
|
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">1</div>
|
|
<div>
|
|
<h3 class="font-black text-gray-800">Foto Kedatangan${showTpsName ? " - " + tps.name : ""}</h3>
|
|
<p class="text-xs text-gray-500">Upload foto kedatangan</p>
|
|
</div>
|
|
</div>
|
|
|
|
<label class="block text-xs font-semibold text-gray-600">Upload Foto Kedatangan</label>
|
|
<input type="file" class="tps-foto-kedatangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" accept="image/*" multiple />
|
|
<div class="tps-preview-kedatangan space-y-2"></div>
|
|
|
|
<div class="kedatangan-upload-state">${getKedatanganUploadStateMarkup(tps)}</div>
|
|
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Latitude</label>
|
|
<input type="text" class="tps-display-latitude w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.latitude}" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Longitude</label>
|
|
<input type="text" class="tps-display-longitude w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.longitude}" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Waktu Kedatangan</label>
|
|
<input type="text" class="tps-waktu-kedatangan w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.waktuKedatangan}" />
|
|
</div>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
function renderSection2Timbangan(tps, showTpsName) {
|
|
return `
|
|
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">2</div>
|
|
<div>
|
|
<h3 class="font-black text-gray-800">Foto Timbang Sampah</h3>
|
|
<p class="text-xs text-gray-500">Upload foto timbangan, berat auto terisi</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tps-timbangan-repeater space-y-3"></div>
|
|
|
|
<button type="button" class="tps-btn-add-timbangan w-full border border-dashed border-upst text-upst rounded-xl py-2 text-xs font-bold transition">
|
|
+ Tambah Foto Timbangan
|
|
</button>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
function renderSection3Petugas(tps) {
|
|
return `
|
|
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">3</div>
|
|
<div>
|
|
<h3 class="font-black text-gray-800">Foto Petugas</h3>
|
|
<p class="text-xs text-gray-500">Upload dokumentasi petugas</p>
|
|
</div>
|
|
</div>
|
|
|
|
<label class="block text-xs font-semibold text-gray-600">Upload Foto Petugas</label>
|
|
<input type="file" class="tps-foto-petugas block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" accept="image/*" multiple />
|
|
<div class="tps-preview-petugas space-y-2"></div>
|
|
|
|
<div class="petugas-upload-state">${getPetugasUploadStateMarkup(tps)}</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Nama Petugas</label>
|
|
<input type="text" class="tps-nama-petugas w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Masukkan nama petugas" value="${tps.namaPetugas}" />
|
|
</div>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
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);
|
|
scheduleAutoSave();
|
|
});
|
|
|
|
fotoPetugasInput.addEventListener('change', function() {
|
|
tps.fotoPetugas = Array.from(this.files);
|
|
tps.fotoPetugasUploaded = false;
|
|
updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
|
|
refreshPetugasUploadState(form);
|
|
scheduleAutoSave();
|
|
});
|
|
|
|
namaPetugasInput.addEventListener('input', function() {
|
|
tps.namaPetugas = this.value;
|
|
refreshPetugasUploadState(form);
|
|
});
|
|
|
|
namaPetugasInput.addEventListener('blur', function() {
|
|
tps.namaPetugas = this.value;
|
|
scheduleAutoSave();
|
|
});
|
|
|
|
btnAddTimbangan.addEventListener('click', function() {
|
|
createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
|
|
syncTimbanganToTpsData();
|
|
refreshSubmitButtonState(form);
|
|
scheduleAutoSave();
|
|
});
|
|
|
|
const btnUploadKedatangan = form.querySelector(
|
|
".tps-btn-upload-kedatangan",
|
|
);
|
|
if (btnUploadKedatangan) {
|
|
btnUploadKedatangan.addEventListener("click", uploadFotoKedatangan);
|
|
}
|
|
|
|
const btnUploadPetugas = form.querySelector(".tps-btn-upload-petugas");
|
|
if (btnUploadPetugas) {
|
|
btnUploadPetugas.addEventListener("click", uploadFotoPetugas);
|
|
}
|
|
|
|
form.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
submitTpsData();
|
|
});
|
|
}
|
|
|
|
function restoreTpsTimbanganItems() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const form = elements.tpsContentContainer.querySelector("form");
|
|
const repeater = form.querySelector(".tps-timbangan-repeater");
|
|
|
|
if (tps.timbangan.length === 0) {
|
|
createTimbanganItem(repeater);
|
|
} else {
|
|
tps.timbangan.forEach((timb) => {
|
|
createTimbanganItem(repeater, timb);
|
|
});
|
|
}
|
|
}
|
|
|
|
function restorePhotoPreview() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const form = elements.tpsContentContainer.querySelector("form");
|
|
if (!form) return;
|
|
|
|
const previewKedatangan = form.querySelector('.tps-preview-kedatangan');
|
|
if (previewKedatangan) {
|
|
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 = URL.createObjectURL(file);
|
|
item.innerHTML = `
|
|
<div class="h-44 bg-black/80">
|
|
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
|
|
</div>
|
|
`;
|
|
|
|
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;
|
|
}
|
|
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
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);
|
|
item.innerHTML = `
|
|
<div class="h-44 bg-black/80">
|
|
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
|
|
</div>
|
|
`;
|
|
|
|
const img = item.querySelector('.preview-multi-image');
|
|
if (img) {
|
|
img.onload = function() {
|
|
URL.revokeObjectURL(imageUrl);
|
|
};
|
|
}
|
|
previewContainer.appendChild(item);
|
|
});
|
|
}
|
|
|
|
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 = `
|
|
<div class="h-44 bg-black/80">
|
|
<img src="${url}" alt="Foto ${index + 1}" class="w-full h-full object-contain" loading="lazy" />
|
|
</div>
|
|
`;
|
|
} else {
|
|
item.className = 'rounded-xl border border-green-200 bg-green-50 h-44 flex items-center justify-center';
|
|
item.innerHTML = `<p class="text-xs text-green-700 font-semibold px-3">✓ Foto ${index + 1}</p>`;
|
|
}
|
|
|
|
container.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function createTimbanganItem(repeater, existingData = null) {
|
|
const photoNumber = repeater.children.length + 1;
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50';
|
|
item.dataset.photoNumber = photoNumber;
|
|
|
|
const weight = existingData ? (existingData.weight || 0) : 0;
|
|
const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS;
|
|
const hasFileBlob = Boolean(existingData?.file);
|
|
const hasFile = Boolean(existingData?.file || existingData?.fotoFileName);
|
|
const isUploaded = Boolean(existingData?.uploaded);
|
|
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
|
|
|
|
item.innerHTML = `
|
|
<div class="flex items-center justify-between">
|
|
<p class="text-xs font-bold text-gray-600">Item Timbangan #${photoNumber}</p>
|
|
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
|
</div>
|
|
<input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
|
|
<div class="${(hasFileBlob || (hasFile && existingData?.fotoFileName?.startsWith('/'))) ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
|
|
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
|
|
</div>
|
|
<p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p>
|
|
<div class="timbangan-upload-state">${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}</div>
|
|
<div class="grid grid-cols-1 gap-2">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Jenis Sampah</label>
|
|
<select class="input-jenis-sampah w-full rounded-xl border border-gray-200 px-3 py-2 text-sm">
|
|
${CONFIG.JENIS_SAMPAH.map((js) => `<option value="${js}" ${js === jenisSampah ? "selected" : ""}>${js}</option>`).join("")}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
|
|
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ""}" />
|
|
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 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;
|
|
tps.timbangan[itemIndex].fotoFileName = '';
|
|
refreshTimbanganUploadState(item);
|
|
}
|
|
scheduleAutoSave();
|
|
}
|
|
});
|
|
|
|
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);
|
|
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 = elements.tpsContentContainer.querySelector("form");
|
|
if (form) refreshSubmitButtonState(form);
|
|
});
|
|
|
|
jenisSampahSelect.addEventListener('change', function() {
|
|
updateTpsTotalTimbangan();
|
|
syncTimbanganToTpsData();
|
|
const form = elements.tpsContentContainer.querySelector('form');
|
|
if (form) refreshSubmitButtonState(form);
|
|
scheduleAutoSave();
|
|
});
|
|
|
|
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);
|
|
scheduleAutoSave();
|
|
});
|
|
|
|
repeater.appendChild(item);
|
|
refreshTimbanganUploadState(item);
|
|
return item;
|
|
}
|
|
|
|
function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) {
|
|
if (!hasFile) {
|
|
return '<p class="text-[11px] text-gray-400">Pilih foto timbangan terlebih dahulu</p>';
|
|
}
|
|
|
|
if (isUploaded) {
|
|
return `
|
|
<div class="text-center text-xs text-green-600 font-bold py-2 upload-success-message">✓ Foto timbangan sudah diupload</div>
|
|
<p class="text-[11px] text-gray-500 text-center">Jika ingin revisi, pilih file baru diatas. Status upload akan tereset otomatis.</p>
|
|
`;
|
|
}
|
|
|
|
if (!hasValidWeight) {
|
|
return `
|
|
<button type="button" disabled class="btn-upload-timbangan w-full bg-gray-300 text-gray-500 py-2 rounded-xl font-bold text-xs cursor-not-allowed">Upload Foto Timbangan Ini</button>
|
|
<p class="text-[11px] text-red-500 text-center">Isi berat manual dulu sebelum upload jika berat tidak terbaca.</p>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<button type="button" class="btn-upload-timbangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">Upload Foto Timbangan Ini</button>
|
|
<p class="text-[11px] text-amber-600 text-center">Foto siap diupload.</p>
|
|
`;
|
|
}
|
|
|
|
function getKedatanganUploadStateMarkup(tps) {
|
|
if (tps.fotoKedatanganUploaded) {
|
|
return '<div class="text-center text-xs text-green-600 font-bold py-2">✓ Foto kedatangan sudah diupload</div>';
|
|
}
|
|
if (!tps.fotoKedatangan.length) {
|
|
return "";
|
|
}
|
|
return `<button type="button" class="tps-btn-upload-kedatangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">Upload ${tps.fotoKedatangan.length} Foto Kedatangan</button>`;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function getPetugasUploadStateMarkup(tps) {
|
|
if (tps.fotoPetugasUploaded) {
|
|
return '<div class="text-center text-xs text-green-600 font-bold py-2">✓ Foto petugas sudah diupload</div>';
|
|
}
|
|
|
|
if (!tps.fotoPetugas.length) {
|
|
return "";
|
|
}
|
|
|
|
if (!tps.namaPetugas.trim()) {
|
|
return `
|
|
<button type="button" disabled class="tps-btn-upload-petugas w-full bg-gray-300 text-gray-500 py-2 rounded-xl font-bold text-xs cursor-not-allowed">Upload ${tps.fotoPetugas.length} Foto Petugas</button>
|
|
<p class="text-[11px] text-red-500 text-center">Isi nama petugas terlebih dahulu</p>
|
|
`;
|
|
}
|
|
|
|
return `<button type="button" class="tps-btn-upload-petugas w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">Upload ${tps.fotoPetugas.length} Foto Petugas</button>`;
|
|
}
|
|
|
|
function refreshPetugasUploadState(form) {
|
|
const stateContainer = form.querySelector(".petugas-upload-state");
|
|
if (!stateContainer) return;
|
|
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
stateContainer.innerHTML = getPetugasUploadStateMarkup(tps);
|
|
|
|
const btnUploadPetugas = stateContainer.querySelector(
|
|
".tps-btn-upload-petugas:not([disabled])",
|
|
);
|
|
if (btnUploadPetugas) {
|
|
btnUploadPetugas.addEventListener("click", uploadFotoPetugas);
|
|
}
|
|
|
|
refreshSubmitButtonState(form);
|
|
}
|
|
|
|
function isTimbanganItemReady(timbanganItem) {
|
|
return Boolean(timbanganItem?.file || timbanganItem?.fotoFileName) && Boolean(timbanganItem?.uploaded) && (timbanganItem?.weight || 0) > 0;
|
|
}
|
|
|
|
function getSubmitState(tps) {
|
|
if (!tps.fotoKedatanganUploaded) {
|
|
if (!tps.fotoKedatangan.length) {
|
|
return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan terlebih dahulu.' };
|
|
}
|
|
return { canSubmit: false, message: 'Silakan upload foto kedatangan terlebih dahulu.' };
|
|
}
|
|
|
|
if (!tps.timbangan.length) {
|
|
return {
|
|
canSubmit: false,
|
|
message: "Tambahkan minimal 1 data timbangan sebelum submit.",
|
|
};
|
|
}
|
|
|
|
if (tps.timbangan.some((item) => !isTimbanganItemReady(item))) {
|
|
return {
|
|
canSubmit: false,
|
|
message:
|
|
"Pastikan semua foto timbangan sudah diupload dan beratnya valid.",
|
|
};
|
|
}
|
|
|
|
if (!tps.fotoPetugasUploaded) {
|
|
if (!tps.fotoPetugas.length) {
|
|
return { canSubmit: false, message: 'Silakan pilih dan upload foto petugas terlebih dahulu.' };
|
|
}
|
|
return { canSubmit: false, message: 'Silakan upload foto petugas terlebih dahulu' };
|
|
}
|
|
|
|
if (!tps.namaPetugas.trim()) {
|
|
return {
|
|
canSubmit: false,
|
|
message: "Isi nama petugas dulu sebelum submit.",
|
|
};
|
|
}
|
|
|
|
return { canSubmit: true, message: "" };
|
|
}
|
|
|
|
function refreshSubmitButtonState(form) {
|
|
const submitButton = form.querySelector('button[type="submit"]');
|
|
if (!submitButton) return;
|
|
|
|
let messageEl = form.querySelector(".submit-state-message");
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const submitState = getSubmitState(tps);
|
|
|
|
submitButton.disabled = !submitState.canSubmit;
|
|
submitButton.className = `w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`;
|
|
|
|
if (!messageEl) {
|
|
messageEl = document.createElement("p");
|
|
messageEl.className =
|
|
"submit-state-message text-[11px] text-center text-red-500 font-medium";
|
|
submitButton
|
|
.closest(".flex.gap-3")
|
|
?.insertAdjacentElement("afterend", messageEl);
|
|
}
|
|
|
|
if (submitState.canSubmit) {
|
|
messageEl.textContent = "";
|
|
messageEl.classList.add("hidden");
|
|
} else {
|
|
messageEl.textContent = submitState.message;
|
|
messageEl.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
function refreshTimbanganUploadState(item) {
|
|
const stateContainer = item.querySelector(".timbangan-upload-state");
|
|
if (!stateContainer) return;
|
|
|
|
const repeater = item.parentElement;
|
|
const itemIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1;
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null;
|
|
const fileInput = item.querySelector('.input-foto-timbangan');
|
|
const hasFile = Boolean(currentData?.file || fileInput?.files?.[0] || currentData?.fotoFileName);
|
|
const isUploaded = Boolean(currentData?.uploaded);
|
|
const weightInputValue = item.querySelector('.input-berat-timbangan-value');
|
|
const currentWeight = currentData?.weight ?? parseWeightInput(weightInputValue?.value || '0');
|
|
const hasValidWeight = currentWeight > 0;
|
|
|
|
stateContainer.innerHTML = getTimbanganUploadStateMarkup(
|
|
hasFile,
|
|
isUploaded,
|
|
hasValidWeight,
|
|
);
|
|
|
|
const uploadBtn = stateContainer.querySelector(".btn-upload-timbangan");
|
|
if (uploadBtn) {
|
|
uploadBtn.addEventListener("click", function () {
|
|
const latestIndex = repeater
|
|
? Array.from(repeater.children).indexOf(item)
|
|
: -1;
|
|
uploadSingleFotoTimbangan(latestIndex, item);
|
|
});
|
|
}
|
|
|
|
const form = elements.tpsContentContainer.querySelector("form");
|
|
if (form) {
|
|
refreshSubmitButtonState(form);
|
|
}
|
|
}
|
|
|
|
function renumberTimbanganItems(repeater) {
|
|
const items = repeater.querySelectorAll(".timbangan-item");
|
|
items.forEach((item, index) => {
|
|
const newNumber = index + 1;
|
|
item.dataset.photoNumber = newNumber;
|
|
|
|
const label = item.querySelector(".text-xs.font-bold.text-gray-600");
|
|
if (label) {
|
|
label.textContent = `Item Timbangan #${newNumber}`;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function applyWatermark(file, photoNumber) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
const img = new Image();
|
|
img.onload = function () {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
const now = new Date();
|
|
const days = [
|
|
"Minggu",
|
|
"Senin",
|
|
"Selasa",
|
|
"Rabu",
|
|
"Kamis",
|
|
"Jumat",
|
|
"Sabtu",
|
|
];
|
|
const months = [
|
|
"Jan",
|
|
"Feb",
|
|
"Mar",
|
|
"Apr",
|
|
"Mei",
|
|
"Jun",
|
|
"Jul",
|
|
"Agu",
|
|
"Sep",
|
|
"Okt",
|
|
"Nov",
|
|
"Des",
|
|
];
|
|
|
|
const dayName = days[now.getDay()];
|
|
const date = now.getDate().toString().padStart(2, "0");
|
|
const month = months[now.getMonth()];
|
|
const year = now.getFullYear();
|
|
const hours = now.getHours().toString().padStart(2, "0");
|
|
const minutes = now.getMinutes().toString().padStart(2, "0");
|
|
const seconds = now.getSeconds().toString().padStart(2, "0");
|
|
|
|
const timestamp = `${dayName}, ${date} ${month} ${year} • ${hours}:${minutes}:${seconds}`;
|
|
|
|
const baseFontSize = Math.max(
|
|
16,
|
|
Math.min(img.width, img.height) * 0.025,
|
|
);
|
|
const fontFamily = "'Montserrat', 'Segoe UI', 'Roboto', sans-serif";
|
|
|
|
const lines = [
|
|
{
|
|
text: `FOTO TIMBANG #${photoNumber}`,
|
|
size: baseFontSize * 1.1,
|
|
weight: "900",
|
|
color: "#FFD700",
|
|
},
|
|
{
|
|
text: `${state.nomorSpj}`,
|
|
size: baseFontSize * 0.9,
|
|
weight: "700",
|
|
color: "#FFFFFF",
|
|
},
|
|
{
|
|
text: timestamp,
|
|
size: baseFontSize * 0.75,
|
|
weight: "500",
|
|
color: "#E2E8F0",
|
|
},
|
|
];
|
|
|
|
const paddingX = baseFontSize * 1.2;
|
|
const paddingY = baseFontSize * 1.0;
|
|
const lineGap = baseFontSize * 0.4;
|
|
|
|
let maxWidth = 0;
|
|
let totalHeight = 0;
|
|
|
|
lines.forEach((line) => {
|
|
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
|
|
const metrics = ctx.measureText(line.text);
|
|
maxWidth = Math.max(maxWidth, metrics.width);
|
|
totalHeight += line.size + lineGap;
|
|
});
|
|
totalHeight -= lineGap;
|
|
|
|
const margin = baseFontSize * 1.5;
|
|
const boxWidth = maxWidth + paddingX * 2;
|
|
const boxHeight = totalHeight + paddingY * 2;
|
|
const boxX = img.width - boxWidth - margin;
|
|
const boxY = img.height - boxHeight - margin;
|
|
|
|
ctx.save();
|
|
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) {
|
|
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, baseFontSize * 0.8);
|
|
} else {
|
|
ctx.rect(boxX, boxY, boxWidth, boxHeight);
|
|
}
|
|
ctx.fillStyle = "rgba(15, 23, 42, 0.85)";
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
const accentWidth = baseFontSize * 0.3;
|
|
if (ctx.roundRect) {
|
|
ctx.roundRect(
|
|
boxX + boxWidth - accentWidth,
|
|
boxY,
|
|
accentWidth,
|
|
boxHeight,
|
|
[0, baseFontSize * 0.8, baseFontSize * 0.8, 0],
|
|
);
|
|
} else {
|
|
ctx.rect(
|
|
boxX + boxWidth - accentWidth,
|
|
boxY,
|
|
accentWidth,
|
|
boxHeight,
|
|
);
|
|
}
|
|
ctx.fillStyle = "#FFD700";
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
|
|
ctx.shadowColor = "rgba(0, 0, 0, 0.6)";
|
|
ctx.shadowBlur = 4;
|
|
ctx.shadowOffsetX = 1;
|
|
ctx.shadowOffsetY = 1;
|
|
ctx.textAlign = "right";
|
|
ctx.textBaseline = "top";
|
|
|
|
let currentY = boxY + paddingY;
|
|
const textRightLimit = boxX + boxWidth - paddingX - accentWidth;
|
|
|
|
lines.forEach((line) => {
|
|
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
|
|
ctx.fillStyle = line.color;
|
|
ctx.fillText(line.text, textRightLimit, currentY);
|
|
currentY += line.size + lineGap;
|
|
});
|
|
|
|
ctx.shadowColor = "transparent";
|
|
|
|
canvas.toBlob(
|
|
function (blob) {
|
|
const watermarkedFile = new File([blob], file.name, {
|
|
type: "image/jpeg",
|
|
lastModified: Date.now(),
|
|
});
|
|
resolve(watermarkedFile);
|
|
},
|
|
"image/jpeg",
|
|
0.95,
|
|
);
|
|
};
|
|
img.onerror = reject;
|
|
img.src = e.target.result;
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
async function autoFillWeight(file, weightInput, ocrInfoEl) {
|
|
let guessedWeight = 0;
|
|
weightInput.placeholder = "Membaca angka dari foto...";
|
|
if (ocrInfoEl) ocrInfoEl.textContent = "AI: memproses gambar...";
|
|
|
|
try {
|
|
const img = await readFileAsImage(file);
|
|
let bestRawText = "";
|
|
let isSuccess = false;
|
|
|
|
for (const area of CONFIG.OCR_AREAS) {
|
|
const cropCanvas = createCropCanvas(img, area);
|
|
const cropFile = await canvasToJpegFile(
|
|
cropCanvas,
|
|
`crop-${area.id}.jpg`,
|
|
);
|
|
const aiResult = await requestOpenRouterWeight(cropFile);
|
|
|
|
if (aiResult && aiResult.success && aiResult.weight) {
|
|
guessedWeight = parseWeightInput(aiResult.weight);
|
|
bestRawText = aiResult.raw || aiResult.weight;
|
|
isSuccess = guessedWeight > 0;
|
|
if (isSuccess) break;
|
|
}
|
|
|
|
if (aiResult && aiResult.raw) {
|
|
bestRawText = aiResult.raw;
|
|
}
|
|
}
|
|
|
|
if (ocrInfoEl) {
|
|
const cleaned = (bestRawText || "").replace(/\s+/g, " ").trim();
|
|
ocrInfoEl.textContent = isSuccess
|
|
? `AI terbaca: ${cleaned}`
|
|
: cleaned
|
|
? `AI tidak valid: ${cleaned}`
|
|
: "AI tidak menemukan angka valid.";
|
|
}
|
|
} catch (_) {
|
|
guessedWeight = 0;
|
|
if (ocrInfoEl) ocrInfoEl.textContent = "AI gagal diproses.";
|
|
}
|
|
|
|
if (guessedWeight > 0) {
|
|
weightInput.value = formatWeightDisplay(guessedWeight);
|
|
weightInput.placeholder = "Berat terdeteksi otomatis";
|
|
} else {
|
|
weightInput.placeholder = "Tidak terbaca otomatis, isi manual";
|
|
}
|
|
|
|
updateTpsTotalTimbangan();
|
|
}
|
|
|
|
function readFileAsImage(file) {
|
|
return new Promise(function (resolve, reject) {
|
|
const objectUrl = URL.createObjectURL(file);
|
|
const img = new Image();
|
|
img.onload = function () {
|
|
URL.revokeObjectURL(objectUrl);
|
|
resolve(img);
|
|
};
|
|
img.onerror = function () {
|
|
URL.revokeObjectURL(objectUrl);
|
|
reject(new Error("Gagal membaca gambar."));
|
|
};
|
|
img.src = objectUrl;
|
|
});
|
|
}
|
|
|
|
function createCropCanvas(img, area) {
|
|
const sx = Math.max(0, Math.floor(img.width * area.x));
|
|
const sy = Math.max(0, Math.floor(img.height * area.y));
|
|
const sw = Math.max(1, Math.floor(img.width * area.w));
|
|
const sh = Math.max(1, Math.floor(img.height * area.h));
|
|
|
|
const canvas = document.createElement("canvas");
|
|
const scale = 2.2;
|
|
canvas.width = Math.max(1, Math.floor(sw * scale));
|
|
canvas.height = Math.max(1, Math.floor(sh * scale));
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return canvas;
|
|
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
|
|
return canvas;
|
|
}
|
|
|
|
function canvasToJpegFile(canvas, fileName) {
|
|
return new Promise(function (resolve, reject) {
|
|
canvas.toBlob(
|
|
function (blob) {
|
|
if (!blob) {
|
|
reject(new Error("Gagal membuat crop image."));
|
|
return;
|
|
}
|
|
resolve(new File([blob], fileName, { type: "image/jpeg" }));
|
|
},
|
|
"image/jpeg",
|
|
0.92,
|
|
);
|
|
});
|
|
}
|
|
|
|
async function requestOpenRouterWeight(imageFile) {
|
|
const formData = new FormData();
|
|
formData.append("Foto", imageFile);
|
|
const response = await fetch("/upst/detail-penjemputan/ocr-timbangan", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(result.message || "Request OCR gagal.");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function updateTpsTotalTimbangan() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const form = elements.tpsContentContainer.querySelector("form");
|
|
if (!form) return;
|
|
|
|
let totalOrganik = 0.0;
|
|
let totalAnorganik = 0.0;
|
|
let totalResidu = 0.0;
|
|
|
|
const repeater = form.querySelector(".tps-timbangan-repeater");
|
|
const items = repeater.querySelectorAll(".timbangan-item");
|
|
|
|
items.forEach(function (item) {
|
|
const weightInput = item.querySelector(".input-berat-timbangan-value");
|
|
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
|
|
|
|
if (weightInput && jenisSampahSelect) {
|
|
const value = parseWeightInput(weightInput.value || "0");
|
|
const jenis = jenisSampahSelect.value;
|
|
|
|
if (jenis === "Organik") {
|
|
totalOrganik += value;
|
|
} else if (jenis === "Anorganik") {
|
|
totalAnorganik += value;
|
|
} else if (jenis === "Residu") {
|
|
totalResidu += value;
|
|
}
|
|
}
|
|
});
|
|
|
|
tps.totalOrganik = totalOrganik;
|
|
tps.totalAnorganik = totalAnorganik;
|
|
tps.totalResidu = totalResidu;
|
|
tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu;
|
|
|
|
const displayTotalOrganik = form.querySelector(
|
|
".tps-display-total-organik",
|
|
);
|
|
const displayTotalAnorganik = form.querySelector(
|
|
".tps-display-total-anorganik",
|
|
);
|
|
const displayTotalResidu = form.querySelector(".tps-display-total-residu");
|
|
const displayTotal = form.querySelector(".tps-display-total");
|
|
|
|
if (displayTotalOrganik)
|
|
displayTotalOrganik.textContent = formatWeightDisplay(totalOrganik);
|
|
if (displayTotalAnorganik)
|
|
displayTotalAnorganik.textContent = formatWeightDisplay(totalAnorganik);
|
|
if (displayTotalResidu)
|
|
displayTotalResidu.textContent = formatWeightDisplay(totalResidu);
|
|
if (displayTotal)
|
|
displayTotal.textContent = formatWeightDisplay(tps.totalTimbangan);
|
|
|
|
updateAllTotals();
|
|
saveState();
|
|
}
|
|
|
|
function updateAllTotals() {
|
|
let grandTotal = 0;
|
|
let grandOrganik = 0;
|
|
let grandAnorganik = 0;
|
|
let grandResidu = 0;
|
|
|
|
state.tpsData.forEach((tps) => {
|
|
grandTotal += tps.totalTimbangan;
|
|
grandOrganik += tps.totalOrganik;
|
|
grandAnorganik += tps.totalAnorganik;
|
|
grandResidu += tps.totalResidu;
|
|
});
|
|
|
|
if (elements.grandTotalDisplay) {
|
|
elements.grandTotalDisplay.textContent = formatWeightDisplay(grandTotal);
|
|
}
|
|
if (elements.totalOrganikDisplay) {
|
|
elements.totalOrganikDisplay.textContent =
|
|
formatWeightDisplay(grandOrganik);
|
|
}
|
|
if (elements.totalAnorganikDisplay) {
|
|
elements.totalAnorganikDisplay.textContent =
|
|
formatWeightDisplay(grandAnorganik);
|
|
}
|
|
if (elements.totalResiduDisplay) {
|
|
elements.totalResiduDisplay.textContent =
|
|
formatWeightDisplay(grandResidu);
|
|
}
|
|
saveState();
|
|
}
|
|
|
|
function syncTimbanganToTpsData() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const form = elements.tpsContentContainer.querySelector("form");
|
|
if (!form) return;
|
|
|
|
const repeater = form.querySelector('.tps-timbangan-repeater');
|
|
const items = repeater.querySelectorAll('.timbangan-item');
|
|
const previousTimbangan = [...tps.timbangan];
|
|
|
|
tps.timbangan = [];
|
|
items.forEach((item, index) => {
|
|
const fileInput = item.querySelector('.input-foto-timbangan');
|
|
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
|
const jenisSampahSelect = item.querySelector('.input-jenis-sampah');
|
|
const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.';
|
|
const existingData = previousTimbangan[index];
|
|
|
|
tps.timbangan.push({
|
|
file: fileInput.files[0] || (existingData ? existingData.file : null),
|
|
fotoFileName: existingData ? existingData.fotoFileName || '' : '',
|
|
weight: parseWeightInput(weightValue.value),
|
|
jenisSampah: jenisSampahSelect.value,
|
|
uploaded: existingData ? existingData.uploaded : false,
|
|
ocrInfo
|
|
});
|
|
});
|
|
}
|
|
|
|
async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
|
|
if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) {
|
|
showToast('Belum ada foto timbangan yang dipilih!', 'error');
|
|
return;
|
|
}
|
|
|
|
const timbanganItem = tps.timbangan[itemIndex];
|
|
if (timbanganItem.weight <= 0) {
|
|
showToast('Berat belum valid. Isi manual dulu sebelum upload foto timbangan.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!targetItem) {
|
|
const form = elements.tpsContentContainer.querySelector('form');
|
|
const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null;
|
|
const items = repeater ? repeater.querySelectorAll('.timbangan-item') : [];
|
|
targetItem = items[itemIndex] || null;
|
|
}
|
|
|
|
const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null;
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = true;
|
|
uploadBtn.textContent = 'Mengupload...';
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('FotoTimbangan', timbanganItem.file);
|
|
formData.append('DraftKey', tps.draftKey || '');
|
|
formData.append('SpjDetailId', tps.spjDetailId || '');
|
|
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
|
formData.append('ItemIndex', itemIndex);
|
|
formData.append('JenisSampah', timbanganItem.jenisSampah || CONFIG.DEFAULT_JENIS);
|
|
formData.append('Berat', Number(timbanganItem.weight || 0).toFixed(2));
|
|
|
|
try {
|
|
const res = await fetch(ENDPOINTS.uploadTimbangan, { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
timbanganItem.uploaded = true;
|
|
timbanganItem.fotoFileName = data.fileUrl || data.fileName || '';
|
|
const fotoInputTimb = targetItem?.querySelector('.input-foto-timbangan');
|
|
if (fotoInputTimb) fotoInputTimb.value = '';
|
|
showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success');
|
|
} else {
|
|
showToast(data.message || 'Gagal upload foto timbangan.', 'error');
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = 'Upload Foto Timbangan Ini';
|
|
}
|
|
}
|
|
} catch (_) {
|
|
showToast('Koneksi gagal saat upload foto timbangan.', 'error');
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.textContent = 'Upload Foto Timbangan Ini';
|
|
}
|
|
}
|
|
|
|
if (targetItem) {
|
|
refreshTimbanganUploadState(targetItem);
|
|
}
|
|
|
|
syncTimbanganToTpsData();
|
|
const form = elements.tpsContentContainer.querySelector('form');
|
|
if (form) refreshSubmitButtonState(form);
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
async function uploadFotoKedatangan() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
if (tps.fotoKedatangan.length === 0) {
|
|
showToast('Belum ada foto kedatangan yang dipilih!', 'error');
|
|
return;
|
|
}
|
|
|
|
const form = elements.tpsContentContainer.querySelector('form');
|
|
const btn = form ? form.querySelector('.tps-btn-upload-kedatangan') : null;
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Mengupload...';
|
|
}
|
|
|
|
const formData = new FormData();
|
|
tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file));
|
|
formData.append('DraftKey', tps.draftKey || '');
|
|
formData.append('SpjDetailId', tps.spjDetailId || '');
|
|
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
|
formData.append('WaktuKedatangan', tps.waktuKedatangan || '');
|
|
formData.append('Latitude', tps.latitude || '');
|
|
formData.append('Longitude', tps.longitude || '');
|
|
formData.append('AlamatJalan', tps.alamatJalan || '');
|
|
|
|
try {
|
|
const res = await fetch(ENDPOINTS.uploadKedatangan, { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
tps.fotoKedatanganUploaded = true;
|
|
tps.fotoKedatanganFileNames = data.fileUrls || data.fileNames || [];
|
|
showToast(data.message || 'Foto kedatangan berhasil diupload.', 'success');
|
|
if (form) {
|
|
const fotoInput = form.querySelector('.tps-foto-kedatangan');
|
|
if (fotoInput) fotoInput.value = '';
|
|
refreshKedatanganUploadState(form);
|
|
}
|
|
scheduleAutoSave();
|
|
} else {
|
|
showToast(data.message || 'Gagal upload foto kedatangan.', 'error');
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`;
|
|
}
|
|
}
|
|
} catch (_) {
|
|
showToast('Koneksi gagal saat upload foto kedatangan.', 'error');
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function uploadFotoPetugas() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
if (tps.fotoPetugas.length === 0) {
|
|
showToast('Belum ada foto petugas yang dipilih!', 'error');
|
|
return;
|
|
}
|
|
if (!tps.namaPetugas.trim()) {
|
|
showToast('Nama petugas wajib diisi sebelum upload foto petugas!', 'error');
|
|
return;
|
|
}
|
|
|
|
const form = elements.tpsContentContainer.querySelector('form');
|
|
const btn = form ? form.querySelector('.tps-btn-upload-petugas:not([disabled])') : null;
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Mengupload...';
|
|
}
|
|
|
|
const formData = new FormData();
|
|
tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file));
|
|
formData.append('DraftKey', tps.draftKey || '');
|
|
formData.append('SpjDetailId', tps.spjDetailId || '');
|
|
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
|
formData.append('NamaPetugas', tps.namaPetugas || '');
|
|
|
|
try {
|
|
const res = await fetch(ENDPOINTS.uploadPetugas, { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
tps.fotoPetugasUploaded = true;
|
|
tps.fotoPetugasFileNames = data.fileUrls || data.fileNames || [];
|
|
showToast(data.message || 'Foto petugas berhasil diupload.', 'success');
|
|
if (form) {
|
|
const fotoInput = form.querySelector('.tps-foto-petugas');
|
|
if (fotoInput) fotoInput.value = '';
|
|
refreshPetugasUploadState(form);
|
|
}
|
|
scheduleAutoSave();
|
|
} else {
|
|
showToast(data.message || 'Gagal upload foto petugas.', 'error');
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`;
|
|
}
|
|
}
|
|
} catch (_) {
|
|
showToast('Koneksi gagal saat upload foto petugas.', 'error');
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildSubmitFormData(tps) {
|
|
const formData = new FormData();
|
|
formData.append("LokasiAngkutID", tps.lokasiAngkutId || "");
|
|
formData.append("SpjDetailID", tps.spjDetailId || "");
|
|
formData.append("TpsName", tps.name);
|
|
formData.append("Latitude", tps.latitude);
|
|
formData.append("Longitude", tps.longitude);
|
|
formData.append("AlamatJalan", tps.alamatJalan);
|
|
formData.append("WaktuKedatangan", tps.waktuKedatangan);
|
|
formData.append("TotalTimbangan", tps.totalTimbangan);
|
|
formData.append("TotalOrganik", tps.totalOrganik);
|
|
formData.append("TotalAnorganik", tps.totalAnorganik);
|
|
formData.append("TotalResidu", tps.totalResidu);
|
|
formData.append("NamaPetugas", tps.namaPetugas);
|
|
|
|
tps.fotoKedatangan.forEach((file) => {
|
|
if (isBrowserFile(file)) {
|
|
formData.append("FotoKedatangan", file);
|
|
}
|
|
});
|
|
|
|
tps.timbangan.forEach((timb) => {
|
|
if (isBrowserFile(timb.file)) formData.append("FotoTimbangan", timb.file);
|
|
formData.append("BeratTimbangan", timb.weight);
|
|
formData.append("JenisSampahList", timb.jenisSampah);
|
|
});
|
|
|
|
tps.fotoPetugas.forEach((file) => {
|
|
if (isBrowserFile(file)) {
|
|
formData.append("FotoPetugas", file);
|
|
}
|
|
});
|
|
|
|
const antiForgeryTokenEl = document.querySelector(
|
|
'#upst-antiforgery input[name="__RequestVerificationToken"]',
|
|
);
|
|
if (antiForgeryTokenEl && antiForgeryTokenEl.value) {
|
|
formData.append("__RequestVerificationToken", antiForgeryTokenEl.value);
|
|
}
|
|
|
|
return formData;
|
|
}
|
|
|
|
function markTpsSubmitted(tps) {
|
|
tps.submitted = true;
|
|
if (state.selectedTpsList.length > 1) {
|
|
renderTabs();
|
|
}
|
|
saveState();
|
|
}
|
|
|
|
function clearStateIfAllSubmitted() {
|
|
if (
|
|
state.tpsData.length > 0 &&
|
|
state.tpsData.every((item) => item.submitted)
|
|
) {
|
|
clearState();
|
|
}
|
|
}
|
|
|
|
async function submitTpsData() {
|
|
const tps = state.tpsData[state.activeTpsIndex];
|
|
const submitState = getSubmitState(tps);
|
|
if (!submitState.canSubmit) {
|
|
showToast(submitState.message, 'error');
|
|
return;
|
|
}
|
|
|
|
const form = elements.tpsContentContainer.querySelector('form');
|
|
const submitBtn = form ? form.querySelector('button[type="submit"]') : null;
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = `Menyimpan${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}...`;
|
|
}
|
|
|
|
const formData = buildSubmitFormData(tps);
|
|
|
|
try {
|
|
const response = await fetch(ENDPOINTS.submit, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok || response.redirected) {
|
|
markTpsSubmitted(tps);
|
|
if (tps.draftKey) {
|
|
await fetch(`${ENDPOINTS.deleteDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`, { method: 'DELETE' });
|
|
}
|
|
|
|
showToast(`Data ${tps.name} berhasil disimpan!`, 'success');
|
|
|
|
const allSubmitted = state.tpsData.every(item => item.submitted);
|
|
if (allSubmitted) {
|
|
setTimeout(() => {
|
|
window.location.href = '/upst/detail-penjemputan/detail-selesai';
|
|
}, 1200);
|
|
} else {
|
|
renderTabs();
|
|
renderTpsForm();
|
|
}
|
|
} else {
|
|
const errorText = await response.text();
|
|
showToast(errorText || 'Gagal menyimpan data. Silakan coba lagi.', 'error');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
showToast('Terjadi kesalahan saat menyimpan data.', 'error');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatWeightDisplay(value) {
|
|
if (isNaN(value)) return "0,00";
|
|
return value.toFixed(2).replace(".", ",");
|
|
}
|
|
|
|
function parseWeightInput(value) {
|
|
if (!value) return 0;
|
|
const cleaned = value
|
|
.toString()
|
|
.trim()
|
|
.replace(/\s/g, "")
|
|
.replace(",", ".");
|
|
const parsed = parseFloat(cleaned);
|
|
return isNaN(parsed) ? 0 : parsed;
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
const mb = bytes / (1024 * 1024);
|
|
return `${mb.toFixed(2)} MB`;
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
let container = document.getElementById('espj-toast-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = 'espj-toast-container';
|
|
container.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
const toast = document.createElement('div');
|
|
const bg = type === 'success' ? '#16a34a' : type === 'error' ? '#dc2626' : '#2563eb';
|
|
toast.style.cssText = `background:${bg};color:#fff;padding:10px 18px;border-radius:16px;font-size:13px;font-weight:600;box-shadow:0 4px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;text-align:center;`;
|
|
toast.textContent = message;
|
|
container.appendChild(toast);
|
|
|
|
requestAnimationFrame(() => {
|
|
toast.style.opacity = '1';
|
|
});
|
|
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3200);
|
|
}
|
|
|
|
return {
|
|
init: init,
|
|
hydrateFromApi: applyApiDraftData,
|
|
setNomorSpj: function (nomorSpj) {
|
|
state.nomorSpj = nomorSpj;
|
|
saveState();
|
|
},
|
|
};
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
try {
|
|
const response = await fetch('/driver/json/tps-list.json');
|
|
const data = await response.json();
|
|
const tpsList = data.tpsList.map(tps => ({
|
|
name: tps.name,
|
|
lokasiAngkutId: tps.lokasiAngkutId,
|
|
spjDetailId: tps.spjDetailId,
|
|
id: tps.id
|
|
}));
|
|
await DetailPenjemputan.init(tpsList);
|
|
} catch (error) {
|
|
console.error('Error loading TPS list:', error);
|
|
}
|
|
|
|
const platNomorEl = document.getElementById('plat-nomor');
|
|
if (platNomorEl) {
|
|
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono');
|
|
if (nomorSpjEl) {
|
|
DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim());
|
|
}
|
|
}
|
|
});
|