From f9b74caedcb8465f7d02ff9cb0dd420e6d714983 Mon Sep 17 00:00:00 2001 From: muamars Date: Tue, 24 Feb 2026 14:31:22 +0700 Subject: [PATCH] update: driver upst timestamp dll --- .../DetailController.cs | 92 +- Data/detail-penjemputan.json | 1 + Models/DetailPenjemputanModels.cs | 78 ++ Program.cs | 6 +- Services/DetailPenjemputanService.cs | 245 ++++ .../DetailPenjemputan/Batal.cshtml | 219 ++- .../DetailPenjemputan/Index.cshtml | 939 +------------ wwwroot/driver/css/watch.css | 95 -- wwwroot/driver/js/detail-penjemputan.js | 1198 +++++++++++++++++ 9 files changed, 1798 insertions(+), 1075 deletions(-) create mode 100644 Data/detail-penjemputan.json create mode 100644 Models/DetailPenjemputanModels.cs create mode 100644 Services/DetailPenjemputanService.cs create mode 100644 wwwroot/driver/js/detail-penjemputan.js diff --git a/Controllers/SpjDriverUpstController/DetailController.cs b/Controllers/SpjDriverUpstController/DetailController.cs index 8393b78..8c6444e 100644 --- a/Controllers/SpjDriverUpstController/DetailController.cs +++ b/Controllers/SpjDriverUpstController/DetailController.cs @@ -3,6 +3,8 @@ using System.Globalization; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using eSPJ.Models; +using eSPJ.Services; namespace eSPJ.Controllers.SpjDriverUpstController { @@ -11,11 +13,19 @@ namespace eSPJ.Controllers.SpjDriverUpstController { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; + private readonly DetailPenjemputanService _detailService; + private readonly ILogger _logger; - public DetailPenjemputanController(IHttpClientFactory httpClientFactory, IConfiguration configuration) + public DetailPenjemputanController( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + DetailPenjemputanService detailService, + ILogger logger) { _httpClientFactory = httpClientFactory; _configuration = configuration; + _detailService = detailService; + _logger = logger; } [HttpGet("")] @@ -28,61 +38,38 @@ namespace eSPJ.Controllers.SpjDriverUpstController { return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/TanpaTps.cshtml"); } + + [HttpGet("batal")] + public IActionResult Batal() + { + return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml"); + } + [HttpPost("")] [ValidateAntiForgeryToken] - public IActionResult Index( - string? Latitude, - string? Longitude, - string? AlamatJalan, - string? GpsTruck, - string? WaktuKedatangan, - decimal? TotalTimbangan, - List? BeratTimbangan, - List? FotoKedatangan, - List? FotoTimbangan, - List? FotoPetugas) + public async Task Submit([FromForm] DetailPenjemputanRequest request) { - if (FotoKedatangan == null || FotoKedatangan.Count == 0) + try { - TempData["Error"] = "Step 1 wajib upload minimal 1 foto kedatangan."; + var result = await _detailService.SubmitPenjemputanAsync(request); + + if (result.Success) + { + TempData["Success"] = result.Message; + } + else + { + TempData["Error"] = result.Message; + } + return RedirectToAction(nameof(Index)); } - - if (FotoTimbangan == null || FotoTimbangan.Count == 0) + catch (Exception ex) { - TempData["Error"] = "Step 2 wajib upload minimal 1 foto timbangan."; + _logger.LogError(ex, "Error submitting penjemputan data"); + TempData["Error"] = "Terjadi kesalahan saat menyimpan data."; return RedirectToAction(nameof(Index)); } - - if (FotoPetugas == null || FotoPetugas.Count == 0) - { - TempData["Error"] = "Step 3 wajib upload minimal 1 foto petugas."; - return RedirectToAction(nameof(Index)); - } - - var totalByDetail = (BeratTimbangan ?? new List()) - .Where(x => x > 0) - .Sum(); - - var total = TotalTimbangan.GetValueOrDefault() > 0 - ? TotalTimbangan.GetValueOrDefault() - : totalByDetail; - - var totalDisplay = total.ToString("N2", CultureInfo.GetCultureInfo("id-ID")); - - TempData["Success"] = - $"Data tersimpan. Kedatangan: {FotoKedatangan.Count} foto, " + - $"Timbangan: {FotoTimbangan.Count} foto, Total: {totalDisplay} kg, " + - $"Petugas: {FotoPetugas.Count} foto."; - - // TODO: simpan ke database - _ = Latitude; - _ = Longitude; - _ = AlamatJalan; - _ = GpsTruck; - _ = WaktuKedatangan; - - return RedirectToAction(nameof(Index)); } [HttpPost("ocr-timbangan")] @@ -94,7 +81,6 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Foto tidak ditemukan." }); } - // limit size biar ga gila (contoh 5MB) if (Foto.Length > 5 * 1024 * 1024) { return BadRequest(new { success = false, message = "Ukuran foto terlalu besar. Maksimal 5MB." }); @@ -119,7 +105,10 @@ namespace eSPJ.Controllers.SpjDriverUpstController var payload = new { - model = "nvidia/nemotron-nano-12b-v2-vl:free", + // model = "nvidia/nemotron-nano-12b-v2-vl:free", + model = "google/gemini-2.5-flash-image", + // model = "google/gemini-2.5-flash-lite", + // model = "google/gemini-2.5-flash-lite-preview-09-2025", temperature = 0, messages = new object[] { @@ -253,10 +242,5 @@ namespace eSPJ.Controllers.SpjDriverUpstController }); } - [HttpGet("batal")] - public IActionResult Batal() - { - return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml"); - } } } diff --git a/Data/detail-penjemputan.json b/Data/detail-penjemputan.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/Data/detail-penjemputan.json @@ -0,0 +1 @@ +[] diff --git a/Models/DetailPenjemputanModels.cs b/Models/DetailPenjemputanModels.cs new file mode 100644 index 0000000..0e71882 --- /dev/null +++ b/Models/DetailPenjemputanModels.cs @@ -0,0 +1,78 @@ +namespace eSPJ.Models +{ + public enum JenisSampah + { + Organik, + Anorganik, + Residu + } + + public class TimbanganItem + { + public string? FotoFileName { get; set; } + public decimal Berat { get; set; } + public JenisSampah JenisSampah { get; set; } = JenisSampah.Residu; + public bool IsUploaded { get; set; } + public DateTime? WaktuUpload { get; set; } + } + + public class TpsData + { + public string Name { get; set; } = string.Empty; + public int Index { get; set; } + public string Latitude { get; set; } = string.Empty; + public string Longitude { get; set; } = string.Empty; + public string AlamatJalan { get; set; } = string.Empty; + public string WaktuKedatangan { get; set; } = string.Empty; + public List FotoKedatangan { get; set; } = new(); + public bool FotoKedatanganUploaded { get; set; } + public List Timbangan { get; set; } = new(); + public decimal TotalOrganik { get; set; } + public decimal TotalAnorganik { get; set; } + public decimal TotalResidu { get; set; } + public decimal TotalTimbangan { get; set; } + public List FotoPetugas { get; set; } = new(); + public bool FotoPetugasUploaded { get; set; } + public string NamaPetugas { get; set; } = string.Empty; + public bool Submitted { get; set; } + } + + public class DetailPenjemputanRequest + { + public string TpsName { get; set; } = string.Empty; + public string Latitude { get; set; } = string.Empty; + public string Longitude { get; set; } = string.Empty; + public string AlamatJalan { get; set; } = string.Empty; + public string WaktuKedatangan { get; set; } = string.Empty; + public decimal TotalTimbangan { get; set; } + public decimal TotalOrganik { get; set; } + public decimal TotalAnorganik { get; set; } + public decimal TotalResidu { get; set; } + public string NamaPetugas { get; set; } = string.Empty; + public List? FotoKedatangan { get; set; } + public List? FotoTimbangan { get; set; } + public List? BeratTimbangan { get; set; } + public List? JenisSampahList { get; set; } + public List? FotoPetugas { get; set; } + } + + public class DetailPenjemputanResponse + { + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public object? Data { get; set; } + } + + public class OcrTimbanganRequest + { + public IFormFile? Foto { get; set; } + } + + public class OcrTimbanganResponse + { + public bool Success { get; set; } + public string? Weight { get; set; } + public string? Raw { get; set; } + public string Message { get; set; } = string.Empty; + } +} diff --git a/Program.cs b/Program.cs index ef25a4d..0998118 100644 --- a/Program.cs +++ b/Program.cs @@ -1,9 +1,14 @@ +using eSPJ.Services; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddHttpClient(); +// Register custom services +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -26,5 +31,4 @@ app.MapControllerRoute( pattern: "{controller=Home}/{action=Index}/{id?}") .WithStaticAssets(); - app.Run(); diff --git a/Services/DetailPenjemputanService.cs b/Services/DetailPenjemputanService.cs new file mode 100644 index 0000000..860f255 --- /dev/null +++ b/Services/DetailPenjemputanService.cs @@ -0,0 +1,245 @@ +using System.Text.Json; +using eSPJ.Models; + +namespace eSPJ.Services +{ + public class DetailPenjemputanService + { + private readonly string _dataFilePath; + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public DetailPenjemputanService( + IWebHostEnvironment env, + ILogger logger) + { + _env = env; + _logger = logger; + _dataFilePath = Path.Combine(_env.ContentRootPath, "Data", "detail-penjemputan.json"); + } + + public async Task> GetAllTpsDataAsync() + { + try + { + if (!File.Exists(_dataFilePath)) + { + return new List(); + } + + var json = await File.ReadAllTextAsync(_dataFilePath); + var data = JsonSerializer.Deserialize>(json); + return data ?? new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading TPS data from JSON"); + return new List(); + } + } + + public async Task SaveTpsDataAsync(List data) + { + try + { + var directory = Path.GetDirectoryName(_dataFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + + var json = JsonSerializer.Serialize(data, options); + await File.WriteAllTextAsync(_dataFilePath, json); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving TPS data to JSON"); + return false; + } + } + + public async Task SubmitPenjemputanAsync(DetailPenjemputanRequest request) + { + try + { + // Validate request + if (string.IsNullOrEmpty(request.TpsName)) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Nama TPS harus diisi" + }; + } + + if (request.FotoKedatangan == null || !request.FotoKedatangan.Any()) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Foto kedatangan harus diupload" + }; + } + + if (request.FotoTimbangan == null || !request.FotoTimbangan.Any()) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Foto timbangan harus diupload" + }; + } + + if (request.FotoPetugas == null || !request.FotoPetugas.Any()) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Foto petugas harus diupload" + }; + } + + if (string.IsNullOrEmpty(request.NamaPetugas)) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Nama petugas harus diisi" + }; + } + + // Save files + var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "penjemputan", DateTime.Now.ToString("yyyy-MM-dd")); + if (!Directory.Exists(uploadPath)) + { + Directory.CreateDirectory(uploadPath); + } + + var tpsData = new TpsData + { + Name = request.TpsName, + Latitude = request.Latitude, + Longitude = request.Longitude, + AlamatJalan = request.AlamatJalan, + WaktuKedatangan = request.WaktuKedatangan, + TotalTimbangan = request.TotalTimbangan, + TotalOrganik = request.TotalOrganik, + TotalAnorganik = request.TotalAnorganik, + TotalResidu = request.TotalResidu, + NamaPetugas = request.NamaPetugas, + Submitted = true, + FotoKedatanganUploaded = true, + FotoPetugasUploaded = true + }; + + // Save foto kedatangan + foreach (var file in request.FotoKedatangan) + { + var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(uploadPath, fileName); + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + tpsData.FotoKedatangan.Add(fileName); + } + + // Save foto timbangan + if (request.BeratTimbangan != null && request.JenisSampahList != null) + { + for (int i = 0; i < request.FotoTimbangan.Count; i++) + { + var file = request.FotoTimbangan[i]; + var fileName = $"timbangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(uploadPath, fileName); + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + var jenisSampah = JenisSampah.Residu; + if (i < request.JenisSampahList.Count && Enum.TryParse(request.JenisSampahList[i], out var parsed)) + { + jenisSampah = parsed; + } + + tpsData.Timbangan.Add(new TimbanganItem + { + FotoFileName = fileName, + Berat = i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0, + JenisSampah = jenisSampah, + IsUploaded = true, + WaktuUpload = DateTime.Now + }); + } + } + + // Save foto petugas + foreach (var file in request.FotoPetugas) + { + var fileName = $"petugas_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + var filePath = Path.Combine(uploadPath, fileName); + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + tpsData.FotoPetugas.Add(fileName); + } + + // Load existing data and append + var allData = await GetAllTpsDataAsync(); + allData.Add(tpsData); + await SaveTpsDataAsync(allData); + + return new DetailPenjemputanResponse + { + Success = true, + Message = "Data penjemputan berhasil disimpan", + Data = tpsData + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error submitting penjemputan data"); + return new DetailPenjemputanResponse + { + Success = false, + Message = $"Terjadi kesalahan: {ex.Message}" + }; + } + } + + public async Task ProcessOcrTimbanganAsync(IFormFile foto) + { + try + { + // TODO: Integrate with OpenRouter API + // For now, return mock response + await Task.Delay(500); // Simulate API call + + return new OcrTimbanganResponse + { + Success = true, + Weight = "54.50", + Raw = "54.50 kg", + Message = "OCR processed successfully (mock)" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing OCR timbangan"); + return new OcrTimbanganResponse + { + Success = false, + Message = $"Terjadi kesalahan: {ex.Message}" + }; + } + } + } +} diff --git a/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml index b41040a..2212e2f 100644 --- a/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml @@ -4,21 +4,22 @@ }
-
-
- + +
+
+ -

Pembatalan Penjemputan

-
+

Batal Angkut

+
-
- +
+

CV Tri Berkah Sejahtera

@@ -34,8 +35,8 @@
-
- +
+

Form Pembatalan

@@ -57,13 +58,26 @@
+ +
+ + +

Upload foto sebagai bukti pembatalan +

+
+
Batal
@@ -71,8 +85,9 @@
- - + + +
@@ -81,6 +96,186 @@ document.addEventListener('DOMContentLoaded', function() { const alasanTextarea = document.querySelector('textarea[name="AlasanPembatalan"]'); const form = document.querySelector('form'); const validationMessage = document.getElementById('validation-message'); + const fotoBuktiInput = document.getElementById('fotoBukti'); + const previewContainer = document.getElementById('preview-container'); + + // Fungsi watermark untuk foto pembatalan + 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 PEMBATALAN #${photoNumber}`, size: baseFontSize * 1.1, weight: '900', color: '#EF4444' }, + { text: 'SPJ/07-2025/PKM/000476', 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(); + + // Garis aksen merah untuk pembatalan + 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 = '#EF4444'; + 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); + }); + } + + // Preview foto dengan watermark + fotoBuktiInput.addEventListener('change', async function() { + previewContainer.innerHTML = ''; + + if (this.files.length > 5) { + alert('Maksimal 5 foto yang dapat diupload'); + this.value = ''; + return; + } + + // Tampilkan loading + previewContainer.innerHTML = '

Memproses foto...

'; + + const watermarkedFiles = []; + for (let i = 0; i < this.files.length; i++) { + const file = this.files[i]; + try { + const watermarkedFile = await applyWatermark(file, i + 1); + watermarkedFiles.push(watermarkedFile); + } catch (error) { + console.error('Error applying watermark:', error); + watermarkedFiles.push(file); + } + } + + // Hapus loading dan tampilkan preview + previewContainer.innerHTML = ''; + + watermarkedFiles.forEach((file, index) => { + const reader = new FileReader(); + reader.onload = function(e) { + const previewItem = document.createElement('div'); + previewItem.className = 'rounded-xl border border-red-200 overflow-hidden bg-black shadow-sm hover:shadow-md transition-shadow'; + previewItem.innerHTML = ` +
+ Preview ${index + 1} +
+
+
+
+ ${index + 1} +
+

${file.name}

+
+
+ ${(file.size / (1024 * 1024)).toFixed(2)} MB + ✓ Watermark +
+
+ `; + previewContainer.appendChild(previewItem); + }; + reader.readAsDataURL(file); + }); + + // Update file input dengan watermarked files + const dataTransfer = new DataTransfer(); + watermarkedFiles.forEach(file => dataTransfer.items.add(file)); + this.files = dataTransfer.files; + }); form.addEventListener('submit', function(e) { if (!alasanTextarea.value.trim()) { diff --git a/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Index.cshtml b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Index.cshtml index 772790d..0583751 100644 --- a/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Index.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Index.cshtml @@ -54,9 +54,26 @@
-
-
-

Total Berat Semua TPS

+
+

Total Berat Semua TPS

+ +
+
+

Organik

+

0,00 kg

+
+
+

Anorganik

+

0,00 kg

+
+
+

Residu

+

0,00 kg

+
+
+ +
+

Total Keseluruhan

0,00 kg
@@ -105,914 +122,10 @@
- - - +@section Scripts { + +} \ No newline at end of file diff --git a/wwwroot/driver/css/watch.css b/wwwroot/driver/css/watch.css index 21dcd7a..a0a0d7b 100644 --- a/wwwroot/driver/css/watch.css +++ b/wwwroot/driver/css/watch.css @@ -65,7 +65,6 @@ --color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-700: oklch(48.8% 0.243 264.376); --color-blue-800: oklch(42.4% 0.199 265.638); - --color-blue-900: oklch(37.9% 0.146 265.522); --color-indigo-50: oklch(96.2% 0.018 272.314); --color-indigo-100: oklch(93% 0.034 272.788); --color-indigo-300: oklch(78.5% 0.115 274.713); @@ -109,8 +108,6 @@ --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); - --text-3xl: 1.875rem; - --text-3xl--line-height: calc(2.25 / 1.875); --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; @@ -350,9 +347,6 @@ .-top-4 { top: calc(var(--spacing) * -4); } - .-top-5 { - top: calc(var(--spacing) * -5); - } .top-0 { top: calc(var(--spacing) * 0); } @@ -422,9 +416,6 @@ .right-full { right: 100%; } - .-bottom-0 { - bottom: calc(var(--spacing) * -0); - } .-bottom-0\.5 { bottom: calc(var(--spacing) * -0.5); } @@ -461,9 +452,6 @@ .left-0 { left: calc(var(--spacing) * 0); } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } @@ -857,12 +845,6 @@ .table-row { display: table-row; } - .aspect-square { - aspect-ratio: 1 / 1; - } - .h-0 { - height: calc(var(--spacing) * 0); - } .h-0\.5 { height: calc(var(--spacing) * 0.5); } @@ -968,9 +950,6 @@ .w-1 { width: calc(var(--spacing) * 1); } - .w-1\/2 { - width: calc(1/2 * 100%); - } .w-1\/3 { width: calc(1/3 * 100%); } @@ -1106,10 +1085,6 @@ .border-collapse { border-collapse: collapse; } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1122,10 +1097,6 @@ --tw-translate-x: calc(var(--spacing) * 16); translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-y-1 { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1186,9 +1157,6 @@ .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .grid-cols-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -1246,13 +1214,6 @@ .gap-5 { gap: calc(var(--spacing) * 5); } - .space-y-0 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse))); - } - } .space-y-0\.5 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -1538,9 +1499,6 @@ .border-green-400 { border-color: var(--color-green-400); } - .border-green-600 { - border-color: var(--color-green-600); - } .border-green-600\/20 { border-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1643,12 +1601,6 @@ background-color: color-mix(in oklab, var(--color-black) 50%, transparent); } } - .bg-black\/70 { - background-color: color-mix(in srgb, #000 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 70%, transparent); - } - } .bg-black\/75 { background-color: color-mix(in srgb, #000 75%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1673,9 +1625,6 @@ .bg-blue-500 { background-color: var(--color-blue-500); } - .bg-cyan-400 { - background-color: var(--color-cyan-400); - } .bg-cyan-400\/10 { background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1736,9 +1685,6 @@ .bg-indigo-300 { background-color: var(--color-indigo-300); } - .bg-lime-500 { - background-color: var(--color-lime-500); - } .bg-lime-500\/15 { background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1760,9 +1706,6 @@ .bg-orange-500 { background-color: var(--color-orange-500); } - .bg-orange-700 { - background-color: var(--color-orange-700); - } .bg-orange-700\/30 { background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2171,9 +2114,6 @@ .py-16 { padding-block: calc(var(--spacing) * 16); } - .py-\[1px\] { - padding-block: 1px; - } .ps-0 { padding-inline-start: calc(var(--spacing) * 0); } @@ -2898,31 +2838,16 @@ background-color: var(--color-red-100); } } - .file\:mr-2 { - &::file-selector-button { - margin-right: calc(var(--spacing) * 2); - } - } .file\:mr-3 { &::file-selector-button { margin-right: calc(var(--spacing) * 3); } } - .file\:mr-4 { - &::file-selector-button { - margin-right: calc(var(--spacing) * 4); - } - } .file\:rounded-lg { &::file-selector-button { border-radius: var(--radius-lg); } } - .file\:rounded-xl { - &::file-selector-button { - border-radius: var(--radius-xl); - } - } .file\:border-0 { &::file-selector-button { border-style: var(--tw-border-style); @@ -2934,16 +2859,6 @@ padding-inline: calc(var(--spacing) * 3); } } - .file\:px-4 { - &::file-selector-button { - padding-inline: calc(var(--spacing) * 4); - } - } - .file\:py-1 { - &::file-selector-button { - padding-block: calc(var(--spacing) * 1); - } - } .file\:py-2 { &::file-selector-button { padding-block: calc(var(--spacing) * 2); @@ -3236,16 +3151,6 @@ } } } - .hover\:file\:brightness-110 { - &:hover { - @media (hover: hover) { - &::file-selector-button { - --tw-brightness: brightness(110%); - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - } - } - } .focus\:border-gray-500 { &:focus { border-color: var(--color-gray-500); diff --git a/wwwroot/driver/js/detail-penjemputan.js b/wwwroot/driver/js/detail-penjemputan.js new file mode 100644 index 0000000..9a1951c --- /dev/null +++ b/wwwroot/driver/js/detail-penjemputan.js @@ -0,0 +1,1198 @@ +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' + }; + + let state = { + activeTpsIndex: 0, + tpsData: [], + availableTpsList: [], + selectedTpsList: [], + hasRequestedLocation: [], + nomorSpj: 'SPJ/07-2025/PKM/000476' + }; + + const elements = { + grandTotalDisplay: null, + tpsSelectionContainer: null, + tpsTabsContainer: null, + tpsCheckboxesContainer: null, + btnConfirmTps: null, + tpsTabsEl: null, + tpsContentContainer: null, + totalOrganikDisplay: null, + totalAnorganikDisplay: null, + totalResiduDisplay: null + }; + + function init(tpsList) { + initElements(); + initializeLocation(tpsList); + } + + function initElements() { + elements.grandTotalDisplay = document.getElementById('grand-total-timbangan'); + elements.tpsSelectionContainer = document.getElementById('tps-selection-container'); + elements.tpsTabsContainer = document.getElementById('tps-tabs-container'); + elements.tpsCheckboxesContainer = document.getElementById('tps-checkboxes'); + elements.btnConfirmTps = document.getElementById('btn-confirm-tps'); + elements.tpsTabsEl = document.getElementById('tps-tabs'); + elements.tpsContentContainer = document.getElementById('tps-content'); + elements.totalOrganikDisplay = document.getElementById('grand-total-organik'); + elements.totalAnorganikDisplay = document.getElementById('grand-total-anorganik'); + elements.totalResiduDisplay = document.getElementById('grand-total-residu'); + + if (elements.btnConfirmTps) { + elements.btnConfirmTps.addEventListener('click', handleConfirmTps); + } + } + + function initializeLocation(tpsList) { + state.availableTpsList = tpsList || []; + + if (state.availableTpsList.length === 0) { + state.selectedTpsList = ['1 Lokasi TPS']; + initializeTpsData(state.selectedTpsList); + elements.tpsTabsContainer.style.display = 'block'; + renderSingleForm(); + return; + } + + if (state.availableTpsList.length === 1) { + state.selectedTpsList = [state.availableTpsList[0]]; + initializeTpsData(state.selectedTpsList); + elements.tpsTabsContainer.style.display = 'block'; + renderTabs(); + renderTpsForm(); + return; + } + + renderTpsSelection(); + elements.tpsSelectionContainer.style.display = 'block'; + } + + function initializeTpsData(tpsNames) { + state.tpsData = tpsNames.map((name, index) => ({ + name: name, + index: index, + latitude: '', + longitude: '', + alamatJalan: '', + waktuKedatangan: '', + fotoKedatangan: [], + fotoKedatanganUploaded: false, + timbangan: [], + totalOrganik: 0, + totalAnorganik: 0, + totalResidu: 0, + totalTimbangan: 0, + fotoPetugas: [], + fotoPetugasUploaded: false, + namaPetugas: '', + submitted: false + })); + state.hasRequestedLocation = new Array(tpsNames.length).fill(false); + } + + function renderTpsSelection() { + elements.tpsCheckboxesContainer.innerHTML = ''; + state.availableTpsList.forEach((tpsName) => { + const wrapper = document.createElement('label'); + wrapper.className = 'flex items-center gap-3 p-3 rounded-xl border border-gray-200 hover:bg-gray-50 cursor-pointer transition'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = tpsName; + checkbox.className = 'w-5 h-5 rounded border-gray-300 text-upst focus:ring-upst'; + checkbox.checked = true; + + const label = document.createElement('span'); + label.className = 'text-sm font-bold text-gray-700'; + label.textContent = tpsName; + + wrapper.appendChild(checkbox); + wrapper.appendChild(label); + elements.tpsCheckboxesContainer.appendChild(wrapper); + }); + } + + function handleConfirmTps() { + const checkboxes = elements.tpsCheckboxesContainer.querySelectorAll('input[type="checkbox"]:checked'); + state.selectedTpsList = Array.from(checkboxes).map(cb => cb.value); + + if (state.selectedTpsList.length === 0) { + alert('Pilih minimal 1 TPS untuk diangkut!'); + return; + } + + initializeTpsData(state.selectedTpsList); + elements.tpsSelectionContainer.style.display = 'none'; + elements.tpsTabsContainer.style.display = 'block'; + + if (state.selectedTpsList.length === 1) { + renderSingleForm(); + } else { + renderTabs(); + renderTpsForm(); + } + } + + function renderSingleForm() { + elements.tpsTabsEl.style.display = 'none'; + state.activeTpsIndex = 0; + renderTpsForm(); + } + + function renderTabs() { + elements.tpsTabsEl.style.display = 'flex'; + elements.tpsTabsEl.innerHTML = ''; + state.tpsData.forEach((tps, index) => { + const tab = document.createElement('button'); + tab.type = 'button'; + tab.className = `px-4 py-2 rounded-xl font-bold text-sm whitespace-nowrap transition ${ + index === state.activeTpsIndex + ? 'bg-upst text-white' + : 'bg-gray-100 text-gray-600 hover:bg-gray-200' + }`; + tab.textContent = tps.name; + if (tps.submitted) { + tab.innerHTML += ' '; + } + tab.addEventListener('click', () => switchToTps(index)); + elements.tpsTabsEl.appendChild(tab); + }); + } + + 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; + + elements.tpsContentContainer.innerHTML = ` +
+ + + + + + + + + ${renderSection1Kedatangan(tps, showTpsName)} + ${renderSection2Timbangan(tps, showTpsName)} + ${renderSection3Petugas(tps)} + +
+ Batal + +
+
+ `; + + attachTpsFormListeners(); + restoreTpsTimbanganItems(); + restorePhotoPreview(); + } + + function renderSection1Kedatangan(tps, showTpsName) { + return ` +
+
+
1
+
+

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

+

Upload foto kedatangan

+
+
+ + + +
+ + ${tps.fotoKedatangan.length > 0 && !tps.fotoKedatanganUploaded ? ` + + ` : tps.fotoKedatanganUploaded ? ` +
+ ✓ Foto kedatangan sudah diupload +
+ ` : ''} + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ `; + } + + function renderSection2Timbangan(tps, showTpsName) { + return ` +
+
+
2
+
+

Foto Timbang Sampah

+

Upload foto timbangan, berat auto terisi

+
+
+ +
+ + +
+ `; + } + + function renderSection3Petugas(tps) { + return ` +
+
+
3
+
+

Foto Petugas

+

Upload dokumentasi petugas

+
+
+ + + +
+ + ${tps.fotoPetugas.length > 0 && !tps.fotoPetugasUploaded ? ` + + ` : tps.fotoPetugasUploaded ? ` +
+ ✓ Foto petugas sudah diupload +
+ ` : ''} + +
+ + +
+
+ `; + } + + 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')); + 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 = state.tpsData[state.activeTpsIndex]; + const form = elements.tpsContentContainer.querySelector('form'); + const repeater = form.querySelector('.tps-timbangan-repeater'); + + if (tps.timbangan.length === 0) { + createTimbanganItem(repeater); + } else { + tps.timbangan.forEach(timb => { + createTimbanganItem(repeater, timb); + }); + } + } + + function restorePhotoPreview() { + const tps = state.tpsData[state.activeTpsIndex]; + const form = elements.tpsContentContainer.querySelector('form'); + if (!form) return; + + const previewKedatangan = form.querySelector('.tps-preview-kedatangan'); + if (previewKedatangan && tps.fotoKedatangan.length > 0) { + renderStoredPhotos(tps.fotoKedatangan, previewKedatangan); + } + + 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 = ` +
+ Preview ${index + 1} +
+
+

${index + 1}. ${safeName}

+

${formatFileSize(file.size)}

+
+ `; + + const img = item.querySelector('.preview-multi-image'); + if (img) { + 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(); + } + + 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; + } + } + + function updateMultiPreview(input, previewContainer) { + if (!input || !previewContainer) return; + + previewContainer.innerHTML = ''; + previewContainer.className = 'space-y-2'; + + if (!input.files || input.files.length === 0) return; + + Array.from(input.files).forEach((file, index) => { + const item = document.createElement('div'); + item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black'; + + const imageUrl = URL.createObjectURL(file); + const safeName = file.name.replace(/"/g, '"'); + item.innerHTML = ` +
+ Preview ${index + 1} +
+
+

${index + 1}. ${safeName}

+

${formatFileSize(file.size)}

+
+ `; + + const img = item.querySelector('.preview-multi-image'); + if (img) { + img.onload = function() { + URL.revokeObjectURL(imageUrl); + }; + } + previewContainer.appendChild(item); + }); + } + + function createTimbanganItem(repeater, existingData = null) { + // Calculate photo number dynamically based on position + const photoNumber = repeater.children.length + 1; + + const item = document.createElement('div'); + item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50'; + item.dataset.photoNumber = photoNumber; + + const weight = existingData ? existingData.weight : 0; + const jenisSampah = existingData ? existingData.jenisSampah : CONFIG.DEFAULT_JENIS; + const hasFile = existingData && existingData.file; + const isUploaded = existingData && existingData.uploaded; + + item.innerHTML = ` +
+

Item Timbangan #${photoNumber}

+ +
+ +
+ Preview foto timbangan +
+

${hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'}

+ ${hasFile && !isUploaded ? ` + + ` : isUploaded ? ` +
+ ✓ Foto timbangan sudah diupload +
+ ` : ''} +
+
+ + +
+
+ + + +
+
+ `; + + const fileInput = item.querySelector('.input-foto-timbangan'); + const previewWrap = item.querySelector('.input-preview-wrap'); + const previewImage = item.querySelector('.input-preview-image'); + const ocrInfoEl = item.querySelector('.input-ocr-info'); + const weightInputDisplay = item.querySelector('.input-berat-timbangan-display'); + const weightInputValue = item.querySelector('.input-berat-timbangan-value'); + const jenisSampahSelect = item.querySelector('.input-jenis-sampah'); + const removeBtn = item.querySelector('.btn-remove-timbangan'); + + if (existingData && existingData.file) { + const localUrl = URL.createObjectURL(existingData.file); + previewImage.src = localUrl; + previewImage.onload = function() { + URL.revokeObjectURL(localUrl); + }; + } + + fileInput.addEventListener('change', async function() { + if (fileInput.files && fileInput.files[0]) { + const originalFile = fileInput.files[0]; + + const watermarkedFile = await applyWatermark(originalFile, photoNumber); + + // Update file input dengan watermarked file + 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; + + 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 = elements.tpsContentContainer.querySelector('form'); + const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null; + + // Renumber all remaining items + if (repeater) { + renumberTimbanganItems(repeater); + if (repeater.children.length === 0) { + createTimbanganItem(repeater); + } + } + + 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 renumberTimbanganItems(repeater) { + const items = repeater.querySelectorAll('.timbangan-item'); + items.forEach((item, index) => { + const newNumber = index + 1; + item.dataset.photoNumber = newNumber; + + // Update label text + 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(); + + // Garis Aksen Vertikal + 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(); + } + + function updateAllTotals() { + let grandTotal = 0; + let grandOrganik = 0; + let grandAnorganik = 0; + let grandResidu = 0; + + state.tpsData.forEach(tps => { + grandTotal += tps.totalTimbangan; + grandOrganik += tps.totalOrganik; + grandAnorganik += tps.totalAnorganik; + grandResidu += tps.totalResidu; + }); + + if (elements.grandTotalDisplay) { + elements.grandTotalDisplay.textContent = formatWeightDisplay(grandTotal); + } + if (elements.totalOrganikDisplay) { + elements.totalOrganikDisplay.textContent = formatWeightDisplay(grandOrganik); + } + if (elements.totalAnorganikDisplay) { + elements.totalAnorganikDisplay.textContent = formatWeightDisplay(grandAnorganik); + } + if (elements.totalResiduDisplay) { + elements.totalResiduDisplay.textContent = formatWeightDisplay(grandResidu); + } + } + + function syncTimbanganToTpsData() { + const tps = state.tpsData[state.activeTpsIndex]; + const form = elements.tpsContentContainer.querySelector('form'); + if (!form) return; + + const repeater = form.querySelector('.tps-timbangan-repeater'); + const items = repeater.querySelectorAll('.timbangan-item'); + + tps.timbangan = []; + items.forEach(item => { + const fileInput = item.querySelector('.input-foto-timbangan'); + const weightValue = item.querySelector('.input-berat-timbangan-value'); + const jenisSampahSelect = item.querySelector('.input-jenis-sampah'); + + const existingIndex = tps.timbangan.length; + const existingData = tps.timbangan[existingIndex]; + + tps.timbangan.push({ + file: fileInput.files[0] || (existingData ? existingData.file : null), + weight: parseWeightInput(weightValue.value), + jenisSampah: jenisSampahSelect.value, + uploaded: existingData ? existingData.uploaded : false + }); + }); + } + + function uploadSingleFotoTimbangan(itemIndex) { + const tps = state.tpsData[state.activeTpsIndex]; + + if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { + alert('Belum ada foto timbangan yang dipilih!'); + return; + } + + const timbanganItem = tps.timbangan[itemIndex]; + + alert(`Upload foto timbangan #${itemIndex + 1} untuk ${tps.name}\nJenis: ${timbanganItem.jenisSampah}\nBerat: ${timbanganItem.weight} kg\n(Implementasi upload ke server)`); + + timbanganItem.uploaded = true; + + const form = elements.tpsContentContainer.querySelector('form'); + const repeater = form.querySelector('.tps-timbangan-repeater'); + const items = repeater.querySelectorAll('.timbangan-item'); + const targetItem = items[itemIndex]; + + if (targetItem) { + const uploadBtn = targetItem.querySelector('.btn-upload-timbangan'); + if (uploadBtn) { + uploadBtn.remove(); + } + + const ocrInfo = targetItem.querySelector('.input-ocr-info'); + if (ocrInfo && !targetItem.querySelector('.upload-success-message')) { + const successMsg = document.createElement('div'); + successMsg.className = 'text-center text-xs text-green-600 font-bold py-2 upload-success-message'; + successMsg.textContent = '✓ Foto timbangan sudah diupload'; + ocrInfo.parentNode.insertBefore(successMsg, ocrInfo.nextSibling); + } + } + } + + function uploadFotoKedatangan() { + const tps = state.tpsData[state.activeTpsIndex]; + if (tps.fotoKedatangan.length === 0) { + alert('Belum ada foto kedatangan yang dipilih!'); + return; + } + + alert(`Upload ${tps.fotoKedatangan.length} foto kedatangan untuk ${tps.name}\n(Implementasi upload ke server)`); + + tps.fotoKedatanganUploaded = true; + renderTpsForm(); + } + + function uploadFotoPetugas() { + const tps = state.tpsData[state.activeTpsIndex]; + if (tps.fotoPetugas.length === 0) { + alert('Belum ada foto petugas yang dipilih!'); + return; + } + + alert(`Upload ${tps.fotoPetugas.length} foto petugas untuk ${tps.name}\n(Implementasi upload ke server)`); + + tps.fotoPetugasUploaded = true; + renderTpsForm(); + } + + function submitTpsData() { + const tps = state.tpsData[state.activeTpsIndex]; + + if (!tps.fotoKedatangan.length) { + alert('Foto kedatangan belum diupload!'); + return; + } + if (!tps.timbangan.length) { + alert('Belum ada data timbangan!'); + return; + } + if (!tps.fotoPetugas.length) { + alert('Foto petugas belum diupload!'); + return; + } + if (!tps.namaPetugas.trim()) { + alert('Nama petugas belum diisi!'); + return; + } + + const formData = new FormData(); + formData.append('TpsName', tps.name); + formData.append('Latitude', tps.latitude); + formData.append('Longitude', tps.longitude); + formData.append('AlamatJalan', tps.alamatJalan); + formData.append('WaktuKedatangan', tps.waktuKedatangan); + formData.append('TotalTimbangan', tps.totalTimbangan); + formData.append('TotalOrganik', tps.totalOrganik); + formData.append('TotalAnorganik', tps.totalAnorganik); + formData.append('TotalResidu', tps.totalResidu); + formData.append('NamaPetugas', tps.namaPetugas); + + tps.fotoKedatangan.forEach((file) => { + formData.append(`FotoKedatangan`, file); + }); + + tps.timbangan.forEach((timb) => { + if (timb.file) formData.append(`FotoTimbangan`, timb.file); + formData.append(`BeratTimbangan`, timb.weight); + formData.append(`JenisSampahList`, timb.jenisSampah); + }); + + tps.fotoPetugas.forEach((file) => { + formData.append(`FotoPetugas`, file); + }); + + // Submit to server + fetch('/upst/detail-penjemputan', { + method: 'POST', + body: formData + }) + .then(response => { + if (response.ok) { + tps.submitted = true; + if (state.selectedTpsList.length > 1) { + renderTabs(); + } + alert(`Data ${tps.name} berhasil disimpan!`); + window.location.reload(); + } else { + alert('Gagal menyimpan data. Silakan coba lagi.'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Terjadi kesalahan saat menyimpan data.'); + }); + } + + // Utility functions + 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`; + } + + // Public API + return { + init: init, + setNomorSpj: function(nomorSpj) { + state.nomorSpj = nomorSpj; + } + }; +})(); + +document.addEventListener('DOMContentLoaded', function() { + DetailPenjemputan.init(['TPS A', 'TPS B', 'TPS C']); + + const platNomorEl = document.getElementById('plat-nomor'); + if (platNomorEl) { + const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); + if (nomorSpjEl) { + DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim()); + } + } +});