update: TPS dan Tanpa TPS

main
muamars 2026-03-05 16:36:03 +07:00
parent 5b71625672
commit c6e242be68
4 changed files with 319 additions and 74 deletions

View File

@ -126,10 +126,6 @@
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
</div>
<!-- <register-block dynamic-section="scripts" key="jsDetailPenjemputan">
<script src="~/driver/js/detail-penjemputan.js"></script>
</register-block> -->
@section Scripts {
<script src="~/driver/js/detail-penjemputan.js"></script>
}

View File

@ -4,6 +4,10 @@
}
<div class="w-full lg:max-w-sm mx-auto min-h-screen bg-gray-50 pb-24">
<form id="upst-antiforgery" class="hidden" aria-hidden="true">
@Html.AntiForgeryToken()
</form>
<div class="bg-upst text-white px-6 pt-8 pb-16 rounded-b-[40px] shadow-lg relative">
<div class="flex items-center justify-between relative z-10">
<a href="@Url.Action("Index", "Home")" class="w-10 h-10 flex items-center justify-center bg-white/10 rounded-xl backdrop-blur-md">
@ -54,9 +58,26 @@
</div>
</div>
<div class="pt-4 border-t border-gray-100 mt-4">
<div class="flex items-center justify-between mb-2">
<p class="text-[10px] text-gray-400 uppercase tracking-widest">Total Timbangan</p>
<div class="pt-4 border-t border-gray-100 mt-4 space-y-3">
<p class="text-[10px] text-gray-400 uppercase tracking-widest mb-2">Total Berat Semua Sampah</p>
<div class="grid grid-cols-3 gap-2">
<div class="bg-green-50 rounded-xl p-2">
<p class="text-[9px] text-green-600 font-bold uppercase tracking-wider">Organik</p>
<p class="text-sm font-black text-green-700"><span id="grand-total-organik">0,00</span> kg</p>
</div>
<div class="bg-blue-50 rounded-xl p-2">
<p class="text-[9px] text-blue-600 font-bold uppercase tracking-wider">Anorganik</p>
<p class="text-sm font-black text-blue-700"><span id="grand-total-anorganik">0,00</span> kg</p>
</div>
<div class="bg-red-50 rounded-xl p-2">
<p class="text-[9px] text-red-600 font-bold uppercase tracking-wider">Residu</p>
<p class="text-sm font-black text-red-700"><span id="grand-total-residu">0,00</span> kg</p>
</div>
</div>
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
<p class="text-xs font-bold text-gray-600">Total Keseluruhan</p>
<span class="text-2xl font-black text-upst"><span id="grand-total-timbangan">0,00</span> kg</span>
</div>
</div>
@ -96,16 +117,22 @@
<script>
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 = [{
@ -118,6 +145,9 @@ document.addEventListener('DOMContentLoaded', function() {
fotoKedatangan: [],
fotoKedatanganUploaded: false,
timbangan: [],
totalOrganik: 0,
totalAnorganik: 0,
totalResidu: 0,
totalTimbangan: 0,
fotoPetugas: [],
fotoPetugasUploaded: false,
@ -137,6 +167,9 @@ document.addEventListener('DOMContentLoaded', function() {
<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">
@ -193,10 +226,6 @@ document.addEventListener('DOMContentLoaded', function() {
+ Tambah Foto Timbangan
</button>
<div class="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2 flex items-center justify-between">
<span class="text-xs font-semibold text-gray-600">Total Timbangan</span>
<span class="text-base font-black text-upst"><span class="tps-display-total">${formatWeightDisplay(tps.totalTimbangan)}</span> kg</span>
</div>
</section>
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
@ -472,6 +501,116 @@ document.addEventListener('DOMContentLoaded', function() {
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 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: `${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);
});
}
function readFileAsImage(file) {
return new Promise(function(resolve, reject) {
const objectUrl = URL.createObjectURL(file);
@ -585,25 +724,54 @@ document.addEventListener('DOMContentLoaded', function() {
const form = tpsContentContainer.querySelector('form');
if (!form) return;
let total = 0.0;
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');
if (weightInput) {
const jenisSampahSelect = item.querySelector('.input-jenis-sampah');
if (weightInput && jenisSampahSelect) {
const value = parseWeightInput(weightInput.value || '0');
total += value;
const jenis = jenisSampahSelect.value;
if (jenis === 'Organik') {
totalOrganik += value;
} else if (jenis === 'Anorganik') {
totalAnorganik += value;
} else {
totalResidu += value;
}
}
});
tps.totalTimbangan = total;
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 (displayTotal) displayTotal.textContent = formatWeightDisplay(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);
if (grandTotalDisplay) {
grandTotalDisplay.textContent = formatWeightDisplay(total);
grandTotalDisplay.textContent = formatWeightDisplay(tps.totalTimbangan);
}
if (grandTotalOrganikDisplay) {
grandTotalOrganikDisplay.textContent = formatWeightDisplay(totalOrganik);
}
if (grandTotalAnorganikDisplay) {
grandTotalAnorganikDisplay.textContent = formatWeightDisplay(totalAnorganik);
}
if (grandTotalResiduDisplay) {
grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu);
}
}
@ -612,6 +780,7 @@ document.addEventListener('DOMContentLoaded', function() {
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;
@ -636,6 +805,12 @@ document.addEventListener('DOMContentLoaded', function() {
</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) : ''}" />
@ -651,6 +826,7 @@ document.addEventListener('DOMContentLoaded', function() {
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) {
@ -663,14 +839,22 @@ document.addEventListener('DOMContentLoaded', function() {
fileInput.addEventListener('change', async function() {
if (fileInput.files && fileInput.files[0]) {
const localUrl = URL.createObjectURL(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(fileInput.files[0], weightInputDisplay, ocrInfoEl);
await autoFillWeight(watermarkedFile, weightInputDisplay, ocrInfoEl);
const parsed = parseWeightInput(weightInputDisplay.value);
weightInputValue.value = parsed.toFixed(2);
updateTpsTotalTimbangan();
@ -719,6 +903,11 @@ document.addEventListener('DOMContentLoaded', function() {
syncTimbanganToTpsData();
});
jenisSampahSelect.addEventListener('change', function() {
updateTpsTotalTimbangan();
syncTimbanganToTpsData();
});
removeBtn.addEventListener('click', function() {
item.remove();
const form = tpsContentContainer.querySelector('form');
@ -761,11 +950,46 @@ document.addEventListener('DOMContentLoaded', function() {
tps.timbangan.push({
file: fileInput.files[0] || (existingData ? existingData.file : null),
weight: parseWeightInput(weightValue.value),
jenisSampah: item.querySelector('.input-jenis-sampah').value,
uploaded: existingData ? existingData.uploaded : false
});
});
}
function buildSubmitFormData(tps) {
const formData = new FormData();
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];
@ -847,30 +1071,44 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
const formData = new FormData();
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('NamaPetugas', tps.namaPetugas);
tps.fotoKedatangan.forEach((file, i) => {
formData.append(`FotoKedatangan`, file);
});
tps.timbangan.forEach((timb, i) => {
if (timb.file) formData.append(`FotoTimbangan`, timb.file);
formData.append(`BeratTimbangan`, timb.weight);
});
tps.fotoPetugas.forEach((file, i) => {
formData.append(`FotoPetugas`, file);
});
alert(`Submit data pengangkutan:\n- Lokasi: ${tps.alamatJalan}\n- Total: ${tps.totalTimbangan} kg\n- Petugas: ${tps.namaPetugas}\n\n(Implementasi POST ke server)`);
// MODE STATIK (aktif sekarang)
// Hanya validasi + alert, tanpa kirim ke backend.
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;
// MODE PRODUCTION (aktifkan saat backend siap)
// 1) Comment blok MODE STATIK di atas
// 2) Uncomment blok di bawah ini
/*
const formData = buildSubmitFormData(tps);
fetch('/upst/detail-penjemputan', {
method: 'POST',
body: formData
})
.then(async response => {
if (response.ok) {
alert('Data tanpa TPS berhasil disimpan!');
window.location.reload();
} else {
const errorText = await response.text();
if (response.status === 400) {
alert('Sesi submit tidak valid. Silakan refresh halaman lalu coba lagi.');
} else {
alert(errorText || 'Gagal menyimpan data. Silakan coba lagi.');
}
}
})
.catch(error => {
console.error('Error:', error);
alert('Terjadi kesalahan saat menyimpan data.');
});
*/
}
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono');
if (nomorSpjEl && nomorSpjEl.textContent) {
nomorSpj = nomorSpjEl.textContent.trim();
}
initializeLocation();

View File

@ -44,6 +44,7 @@
--color-green-50: oklch(98.2% 0.018 155.826);
--color-green-100: oklch(96.2% 0.044 156.743);
--color-green-200: oklch(92.5% 0.084 155.995);
--color-green-300: oklch(87.1% 0.15 154.449);
--color-green-400: oklch(79.2% 0.209 151.711);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214);
@ -1502,6 +1503,9 @@
.border-green-200 {
border-color: var(--color-green-200);
}
.border-green-300 {
border-color: var(--color-green-300);
}
.border-green-400 {
border-color: var(--color-green-400);
}

View File

@ -57,6 +57,9 @@ const DetailPenjemputan = (function() {
function initializeLocation(tpsList) {
state.availableTpsList = tpsList || [];
if (elements.tpsSelectionContainer) {
elements.tpsSelectionContainer.style.display = 'none';
}
if (state.availableTpsList.length === 0) {
state.selectedTpsList = ['1 Lokasi TPS'];
@ -65,18 +68,17 @@ const DetailPenjemputan = (function() {
renderSingleForm();
return;
}
if (state.availableTpsList.length === 1) {
state.selectedTpsList = [state.availableTpsList[0]];
initializeTpsData(state.selectedTpsList);
elements.tpsTabsContainer.style.display = 'block';
state.selectedTpsList = [...state.availableTpsList];
initializeTpsData(state.selectedTpsList);
elements.tpsTabsContainer.style.display = 'block';
if (state.selectedTpsList.length === 1) {
renderSingleForm();
} else {
renderTabs();
renderTpsForm();
return;
}
renderTpsSelection();
elements.tpsSelectionContainer.style.display = 'block';
}
function initializeTpsData(tpsNames) {
@ -152,31 +154,36 @@ const DetailPenjemputan = (function() {
}
function renderTabs() {
elements.tpsTabsEl.style.display = 'flex';
elements.tpsTabsEl.innerHTML = '';
state.tpsData.forEach((tps, index) => {
const tab = document.createElement('button');
tab.type = 'button';
const isActive = index === state.activeTpsIndex;
const isSubmitted = !!tps.submitted;
elements.tpsTabsEl.style.display = 'block';
elements.tpsTabsEl.className = 'mb-4';
let tabClass = 'bg-gray-100 text-gray-600 hover:bg-gray-200';
if (isSubmitted) {
tabClass = isActive
? 'bg-green-600 text-white'
: 'bg-green-100 text-green-700 hover:bg-green-200';
} else if (isActive) {
tabClass = 'bg-upst text-white';
}
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';
tab.className = `px-4 py-2 rounded-xl font-bold text-sm whitespace-nowrap transition ${tabClass}`;
tab.textContent = tps.name;
if (tps.submitted) {
tab.innerHTML += ' <span class="text-xs">✓</span>';
}
tab.addEventListener('click', () => switchToTps(index));
elements.tpsTabsEl.appendChild(tab);
});
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) {