update: tps id dan detail non tps
parent
c6e242be68
commit
6bbe35c450
|
|
@ -127,5 +127,5 @@
|
|||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/driver/js/detail-penjemputan.js"></script>
|
||||
<script src="~/driver/js/detail-penjemputan-tps.js"></script>
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,854 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const grandTotalDisplay = document.getElementById('grand-total-timbangan');
|
||||
const grandTotalOrganikDisplay = document.getElementById('grand-total-organik');
|
||||
const grandTotalAnorganikDisplay = document.getElementById('grand-total-anorganik');
|
||||
const grandTotalResiduDisplay = document.getElementById('grand-total-residu');
|
||||
const tpsContentContainer = document.getElementById('tps-content');
|
||||
|
||||
let activeTpsIndex = 0;
|
||||
let tpsData = [];
|
||||
let nomorSpj = 'SPJ/07-2025/PKM/000476';
|
||||
|
||||
const OCR_AREAS = [
|
||||
{ id: 'A', x: 0.34, y: 0.35, w: 0.40, h: 0.11, color: 'border-lime-400 bg-lime-500/15' },
|
||||
{ id: 'B', x: 0.31, y: 0.33, w: 0.45, h: 0.14, color: 'border-amber-300 bg-amber-400/10' },
|
||||
{ id: 'C', x: 0.29, y: 0.31, w: 0.49, h: 0.17, color: 'border-cyan-300 bg-cyan-400/10' }
|
||||
];
|
||||
const JENIS_SAMPAH = ['Organik', 'Anorganik', 'Residu'];
|
||||
const DEFAULT_JENIS = 'Residu';
|
||||
|
||||
function initializeLocation() {
|
||||
tpsData = [{
|
||||
name: '',
|
||||
index: 0,
|
||||
lokasiAngkutId: '',
|
||||
spjDetailId: '',
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
alamatJalan: '',
|
||||
waktuKedatangan: '',
|
||||
fotoKedatangan: [],
|
||||
fotoKedatanganUploaded: false,
|
||||
timbangan: [],
|
||||
totalOrganik: 0,
|
||||
totalAnorganik: 0,
|
||||
totalResidu: 0,
|
||||
totalTimbangan: 0,
|
||||
fotoPetugas: [],
|
||||
fotoPetugasUploaded: false,
|
||||
namaPetugas: '',
|
||||
submitted: false
|
||||
}];
|
||||
|
||||
renderTpsForm();
|
||||
}
|
||||
|
||||
function renderTpsForm() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
|
||||
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}" />
|
||||
|
||||
<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</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>
|
||||
|
||||
${tps.fotoKedatangan.length > 0 && !tps.fotoKedatanganUploaded ? `
|
||||
<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>
|
||||
` : tps.fotoKedatanganUploaded ? `
|
||||
<div class="text-center text-xs text-green-600 font-bold py-2">
|
||||
✓ Foto kedatangan sudah diupload
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
${tps.fotoPetugas.length > 0 && !tps.fotoPetugasUploaded ? `
|
||||
<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>
|
||||
` : tps.fotoPetugasUploaded ? `
|
||||
<div class="text-center text-xs text-green-600 font-bold py-2">
|
||||
✓ Foto petugas sudah diupload
|
||||
</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>
|
||||
|
||||
<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" class="w-2/3 bg-upst text-white py-3 rounded-xl font-bold text-sm hover:brightness-110">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
attachTpsFormListeners();
|
||||
restoreTpsTimbanganItems();
|
||||
restorePhotoPreview();
|
||||
}
|
||||
|
||||
function restorePhotoPreview() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
const form = tpsContentContainer.querySelector('form');
|
||||
if (!form) return;
|
||||
|
||||
const previewKedatangan = form.querySelector('.tps-preview-kedatangan');
|
||||
if (previewKedatangan && tps.fotoKedatangan.length > 0) {
|
||||
renderStoredPhotos(tps.fotoKedatangan, previewKedatangan);
|
||||
}
|
||||
|
||||
const previewPetugas = form.querySelector('.tps-preview-petugas');
|
||||
if (previewPetugas && tps.fotoPetugas.length > 0) {
|
||||
renderStoredPhotos(tps.fotoPetugas, previewPetugas);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStoredPhotos(files, container) {
|
||||
container.innerHTML = '';
|
||||
container.className = 'space-y-2';
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
|
||||
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
const safeName = file.name.replace(/"/g, '"');
|
||||
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>
|
||||
<div class="px-2 py-1 bg-white">
|
||||
<p class="text-[11px] font-semibold text-gray-700 truncate">${index + 1}. ${safeName}</p>
|
||||
<p class="text-[10px] text-gray-500">${formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const img = item.querySelector('.preview-multi-image');
|
||||
if (img) {
|
||||
img.onload = function() {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
}
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function attachTpsFormListeners() {
|
||||
const form = tpsContentContainer.querySelector('form');
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
|
||||
const fotoKedatanganInput = form.querySelector('.tps-foto-kedatangan');
|
||||
const fotoPetugasInput = form.querySelector('.tps-foto-petugas');
|
||||
const namaPetugasInput = form.querySelector('.tps-nama-petugas');
|
||||
const btnAddTimbangan = form.querySelector('.tps-btn-add-timbangan');
|
||||
|
||||
fotoKedatanganInput.addEventListener('change', function() {
|
||||
tps.fotoKedatangan = Array.from(this.files);
|
||||
tps.fotoKedatanganUploaded = false;
|
||||
updateWaktuKedatangan();
|
||||
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
|
||||
renderTpsForm();
|
||||
});
|
||||
|
||||
fotoPetugasInput.addEventListener('change', function() {
|
||||
tps.fotoPetugas = Array.from(this.files);
|
||||
tps.fotoPetugasUploaded = false;
|
||||
updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
|
||||
renderTpsForm();
|
||||
});
|
||||
|
||||
namaPetugasInput.addEventListener('input', function() {
|
||||
tps.namaPetugas = this.value;
|
||||
});
|
||||
|
||||
btnAddTimbangan.addEventListener('click', function() {
|
||||
createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
|
||||
});
|
||||
|
||||
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 = `
|
||||
<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>
|
||||
<div class="px-2 py-1 bg-white">
|
||||
<p class="text-[11px] font-semibold text-gray-700 truncate">${index + 1}. ${safeName}</p>
|
||||
<p class="text-[10px] text-gray-500">${formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 OCR: 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 OCR terbaca: ${cleaned}`
|
||||
: (cleaned ? `AI OCR tidak valid: ${cleaned}` : 'AI OCR tidak menemukan angka valid.');
|
||||
}
|
||||
} catch (_) {
|
||||
guessedWeight = 0;
|
||||
if (ocrInfoEl) ocrInfoEl.textContent = 'AI OCR 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 createTimbanganItem(repeater, existingData = null) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50';
|
||||
|
||||
const weight = existingData ? existingData.weight : 0;
|
||||
const jenisSampah = existingData ? (existingData.jenisSampah || DEFAULT_JENIS) : DEFAULT_JENIS;
|
||||
const hasFile = existingData && existingData.file;
|
||||
const isUploaded = existingData && existingData.uploaded;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-bold text-gray-600">Item Timbangan</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="${hasFile ? '' : '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 class="input-crop-overlay absolute inset-0 pointer-events-none"></div>
|
||||
</div>
|
||||
<p class="text-[11px] text-gray-500 input-ocr-info">${hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'}</p>
|
||||
${hasFile && !isUploaded ? `
|
||||
<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>
|
||||
` : isUploaded ? `<div class="text-center text-xs text-green-600 font-bold py-2">✓ Foto timbangan sudah diupload</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">
|
||||
${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;
|
||||
previewImage.onload = function() { URL.revokeObjectURL(localUrl); };
|
||||
}
|
||||
|
||||
fileInput.addEventListener('change', async function() {
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
const originalFile = fileInput.files[0];
|
||||
const 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;
|
||||
const existingUploadBtn = item.querySelector('.btn-upload-timbangan');
|
||||
if (!existingUploadBtn) {
|
||||
const ocrInfo = item.querySelector('.input-ocr-info');
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.type = 'button';
|
||||
uploadBtn.className = 'btn-upload-timbangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110';
|
||||
uploadBtn.textContent = 'Upload Foto Timbangan Ini';
|
||||
uploadBtn.addEventListener('click', function() { uploadSingleFotoTimbangan(itemIndex); });
|
||||
ocrInfo.parentNode.insertBefore(uploadBtn, ocrInfo.nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
jenisSampahSelect.addEventListener('change', function() {
|
||||
updateTpsTotalTimbangan();
|
||||
syncTimbanganToTpsData();
|
||||
});
|
||||
|
||||
removeBtn.addEventListener('click', function() {
|
||||
item.remove();
|
||||
const form = tpsContentContainer.querySelector('form');
|
||||
const rep = form ? form.querySelector('.tps-timbangan-repeater') : null;
|
||||
if (rep && rep.children.length === 0) createTimbanganItem(rep);
|
||||
updateTpsTotalTimbangan();
|
||||
syncTimbanganToTpsData();
|
||||
});
|
||||
|
||||
const btnUploadTimbangan = item.querySelector('.btn-upload-timbangan');
|
||||
if (btnUploadTimbangan) {
|
||||
btnUploadTimbangan.addEventListener('click', function() {
|
||||
const itemIndex = Array.from(repeater.children).indexOf(item);
|
||||
uploadSingleFotoTimbangan(itemIndex);
|
||||
});
|
||||
}
|
||||
|
||||
repeater.appendChild(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');
|
||||
|
||||
tps.timbangan = [];
|
||||
items.forEach(item => {
|
||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
||||
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
||||
tps.timbangan.push({
|
||||
file: fileInput.files[0] || null,
|
||||
weight: parseWeightInput(weightValue.value),
|
||||
jenisSampah: item.querySelector('.input-jenis-sampah').value,
|
||||
uploaded: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildSubmitFormData(tps) {
|
||||
const formData = new FormData();
|
||||
formData.append('LokasiAngkutID', tps.lokasiAngkutId || '');
|
||||
formData.append('SpjDetailID', tps.spjDetailId || '');
|
||||
formData.append('Latitude', tps.latitude);
|
||||
formData.append('Longitude', tps.longitude);
|
||||
formData.append('AlamatJalan', tps.alamatJalan);
|
||||
formData.append('WaktuKedatangan', tps.waktuKedatangan);
|
||||
formData.append('TotalTimbangan', tps.totalTimbangan);
|
||||
formData.append('TotalOrganik', tps.totalOrganik);
|
||||
formData.append('TotalAnorganik', tps.totalAnorganik);
|
||||
formData.append('TotalResidu', tps.totalResidu);
|
||||
formData.append('NamaPetugas', tps.namaPetugas);
|
||||
|
||||
tps.fotoKedatangan.forEach((file) => formData.append('FotoKedatangan', file));
|
||||
tps.timbangan.forEach((timb) => {
|
||||
if (timb.file) formData.append('FotoTimbangan', timb.file);
|
||||
formData.append('BeratTimbangan', timb.weight);
|
||||
formData.append('JenisSampahList', timb.jenisSampah || DEFAULT_JENIS);
|
||||
});
|
||||
tps.fotoPetugas.forEach((file) => formData.append('FotoPetugas', file));
|
||||
|
||||
const antiForgeryTokenEl = document.querySelector('#upst-antiforgery input[name="__RequestVerificationToken"]');
|
||||
if (antiForgeryTokenEl && antiForgeryTokenEl.value) {
|
||||
formData.append('__RequestVerificationToken', antiForgeryTokenEl.value);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
function uploadSingleFotoTimbangan(itemIndex) {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) {
|
||||
alert('Belum ada foto timbangan yang dipilih!');
|
||||
return;
|
||||
}
|
||||
alert(`Upload foto timbangan #${itemIndex + 1}\nBerat: ${tps.timbangan[itemIndex].weight} kg\n(Implementasi upload ke server)`);
|
||||
tps.timbangan[itemIndex].uploaded = true;
|
||||
}
|
||||
|
||||
function uploadFotoKedatangan() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (tps.fotoKedatangan.length === 0) {
|
||||
alert('Belum ada foto kedatangan yang dipilih!');
|
||||
return;
|
||||
}
|
||||
alert(`Upload ${tps.fotoKedatangan.length} foto kedatangan\n(Implementasi upload ke server)`);
|
||||
tps.fotoKedatanganUploaded = true;
|
||||
renderTpsForm();
|
||||
}
|
||||
|
||||
function uploadFotoPetugas() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (tps.fotoPetugas.length === 0) {
|
||||
alert('Belum ada foto petugas yang dipilih!');
|
||||
return;
|
||||
}
|
||||
alert(`Upload ${tps.fotoPetugas.length} foto petugas\n(Implementasi upload ke server)`);
|
||||
tps.fotoPetugasUploaded = true;
|
||||
renderTpsForm();
|
||||
}
|
||||
|
||||
function submitTpsData() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (!tps.fotoKedatangan.length) return alert('Foto kedatangan belum diupload!');
|
||||
if (!tps.timbangan.length) return alert('Belum ada data timbangan!');
|
||||
if (!tps.fotoPetugas.length) return alert('Foto petugas belum diupload!');
|
||||
if (!tps.namaPetugas.trim()) return alert('Nama petugas belum diisi!');
|
||||
|
||||
alert(`Validasi OK (Tanpa TPS).\n- Organik: ${formatWeightDisplay(tps.totalOrganik)} kg\n- Anorganik: ${formatWeightDisplay(tps.totalAnorganik)} kg\n- Residu: ${formatWeightDisplay(tps.totalResidu)} kg\n- Total: ${formatWeightDisplay(tps.totalTimbangan)} kg\n- Petugas: ${tps.namaPetugas}`);
|
||||
tps.submitted = true;
|
||||
|
||||
/*
|
||||
const formData = buildSubmitFormData(tps);
|
||||
fetch('/upst/detail-penjemputan', { method: 'POST', body: formData });
|
||||
*/
|
||||
}
|
||||
|
||||
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono');
|
||||
if (nomorSpjEl && nomorSpjEl.textContent) {
|
||||
nomorSpj = nomorSpjEl.textContent.trim();
|
||||
}
|
||||
|
||||
initializeLocation();
|
||||
});
|
||||
|
|
@ -82,9 +82,11 @@ const DetailPenjemputan = (function() {
|
|||
}
|
||||
|
||||
function initializeTpsData(tpsNames) {
|
||||
state.tpsData = tpsNames.map((name, index) => ({
|
||||
name: name,
|
||||
state.tpsData = tpsNames.map((tpsItem, index) => ({
|
||||
name: typeof tpsItem === 'string' ? tpsItem : (tpsItem?.name || tpsItem?.Name || `TPS ${index + 1}`),
|
||||
index: index,
|
||||
lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''),
|
||||
spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''),
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
alamatJalan: '',
|
||||
|
|
@ -205,6 +207,8 @@ const DetailPenjemputan = (function() {
|
|||
|
||||
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}" />
|
||||
|
|
@ -1101,6 +1105,8 @@ async function applyWatermark(file, photoNumber) {
|
|||
|
||||
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);
|
||||
Loading…
Reference in New Issue