From 565ce93d4d0bf43899e0c163af6b37d21adc4bdb Mon Sep 17 00:00:00 2001 From: marszayn Date: Mon, 27 Apr 2026 13:11:54 +0700 Subject: [PATCH] update: jgc --- .../DetailController.cs | 184 ++- Program.cs | 1 + Services/DetailPenjemputanJGCService.cs | 221 +++ .../DetailPenjemputan/DetailSelesaiJGC.cshtml | 94 ++ .../DetailPenjemputan/JGC.cshtml | 94 ++ .../Transport/SpjDriverUpst/Home/Index.cshtml | 18 + wwwroot/driver/css/watch.css | 118 -- wwwroot/driver/js/detail-penjemputan-jgc.js | 1256 +++++++++++++++++ .../driver/json/detail-penjemputan-jgc.json | 0 wwwroot/driver/json/selesai-jgc.json | 15 + 10 files changed, 1881 insertions(+), 120 deletions(-) create mode 100644 Services/DetailPenjemputanJGCService.cs create mode 100644 Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiJGC.cshtml create mode 100644 Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/JGC.cshtml create mode 100644 wwwroot/driver/js/detail-penjemputan-jgc.js create mode 100644 wwwroot/driver/json/detail-penjemputan-jgc.json create mode 100644 wwwroot/driver/json/selesai-jgc.json diff --git a/Controllers/SpjDriverUpstController/DetailController.cs b/Controllers/SpjDriverUpstController/DetailController.cs index 240c31d..bf84f49 100644 --- a/Controllers/SpjDriverUpstController/DetailController.cs +++ b/Controllers/SpjDriverUpstController/DetailController.cs @@ -14,19 +14,22 @@ namespace eSPJ.Controllers.SpjDriverUpstController private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly DetailPenjemputanService _detailService; + private readonly DetailPenjemputanJGCService _jgcService; private readonly ILogger _logger; private readonly IWebHostEnvironment _env; public DetailPenjemputanController( - IHttpClientFactory httpClientFactory, + IHttpClientFactory httpClientFactory, IConfiguration configuration, DetailPenjemputanService detailService, + DetailPenjemputanJGCService jgcService, ILogger logger, IWebHostEnvironment env) { _httpClientFactory = httpClientFactory; _configuration = configuration; _detailService = detailService; + _jgcService = jgcService; _logger = logger; _env = env; } @@ -313,6 +316,11 @@ namespace eSPJ.Controllers.SpjDriverUpstController { return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/TanpaTps.cshtml"); } + [HttpGet("jgc")] + public IActionResult JGC() + { + return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/JGC.cshtml"); + } [HttpGet("batal")] public IActionResult Batal() @@ -338,6 +346,12 @@ namespace eSPJ.Controllers.SpjDriverUpstController return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiTanpaTps.cshtml"); } + [HttpGet("detail-selesai-jgc")] + public IActionResult DetailSelesaiJgc() + { + return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiJGC.cshtml"); + } + [HttpGet("api/submitted")] [IgnoreAntiforgeryToken] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] @@ -436,7 +450,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController { TempData["Error"] = result.Message; } - + return RedirectToAction(nameof(Index)); } catch (Exception ex) @@ -457,6 +471,53 @@ namespace eSPJ.Controllers.SpjDriverUpstController } } + [HttpPost("jgc-submit")] + [ValidateAntiForgeryToken] + public async Task SubmitJGC([FromForm] DetailPenjemputanRequest request) + { + var isAjaxRequest = string.Equals(Request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.OrdinalIgnoreCase) + || Request.Headers.Accept.Any(value => value?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true); + + try + { + var result = await _jgcService.SubmitPenjemputanAsync(request); + + if (isAjaxRequest) + { + return result.Success + ? Ok(result) + : BadRequest(result); + } + + if (result.Success) + { + TempData["Success"] = result.Message; + } + else + { + TempData["Error"] = result.Message; + } + + return RedirectToAction(nameof(JGC)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error submitting JGC penjemputan data"); + + if (isAjaxRequest) + { + return StatusCode(500, new DetailPenjemputanResponse + { + Success = false, + Message = "Terjadi kesalahan saat menyimpan data." + }); + } + + TempData["Error"] = "Terjadi kesalahan saat menyimpan data."; + return RedirectToAction(nameof(JGC)); + } + } + [HttpPost("save-record-non-tps")] [IgnoreAntiforgeryToken] public async Task SaveRecordNonTps() @@ -470,6 +531,19 @@ namespace eSPJ.Controllers.SpjDriverUpstController return result.Success ? Ok(result) : StatusCode(500, result); } + [HttpPost("save-record-jgc")] + [IgnoreAntiforgeryToken] + public async Task SaveRecordJgc() + { + var request = await ResolveRecordSaveRequestAsync(); + + if (request == null) + return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." }); + + var result = await _jgcService.SaveRecordJGCAsync(request); + return result.Success ? Ok(result) : StatusCode(500, result); + } + [HttpPost("save-record")] [IgnoreAntiforgeryToken] public async Task SaveRecord() @@ -540,6 +614,62 @@ namespace eSPJ.Controllers.SpjDriverUpstController return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." }); } + [HttpPost("upload-foto-kedatangan-jgc")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoKedatanganJgc( + [FromForm] List? FotoKedatangan, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] string? WaktuKedatangan, + [FromForm] string? Latitude, + [FromForm] string? Longitude, + [FromForm] string? AlamatJalan) + { + if (FotoKedatangan == null || FotoKedatangan.Count == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); + + var fileNames = new List(); + foreach (var file in FotoKedatangan) + { + if (file.Length == 0) continue; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + var name = $"kedatangan_{Guid.NewGuid()}{ext}"; + var path = Path.Combine(uploadDir, name); + await using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + fileNames.Add(name); + } + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList(); + + var saveResult = await SaveUploadedRecordAsync( + isTps: false, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => + { + request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan; + request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude; + request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude; + request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan; + request.FotoKedatanganFileNames = fileUrls; + request.FotoKedatanganUploaded = true; + }); + + if (!saveResult.Success) + { + return StatusCode(500, new { success = false, message = saveResult.Message }); + } + + return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." }); + } + [HttpPost("upload-foto-timbangan-non-tps")] [IgnoreAntiforgeryToken] public async Task UploadFotoTimbanganNonTps( @@ -597,6 +727,56 @@ namespace eSPJ.Controllers.SpjDriverUpstController return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); } + [HttpPost("upload-foto-petugas-jgc")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoPetugasJgc( + [FromForm] List? FotoPetugas, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] string? NamaPetugas) + { + if (FotoPetugas == null || FotoPetugas.Count == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); + + var fileNames = new List(); + foreach (var file in FotoPetugas) + { + if (file.Length == 0) continue; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + var name = $"petugas_{Guid.NewGuid()}{ext}"; + var path = Path.Combine(uploadDir, name); + await using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + fileNames.Add(name); + } + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList(); + + var saveResult = await SaveUploadedRecordAsync( + isTps: false, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => + { + request.FotoPetugasFileNames = fileUrls; + request.FotoPetugasUploaded = true; + request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas; + }); + + if (!saveResult.Success) + { + return StatusCode(500, new { success = false, message = saveResult.Message }); + } + + return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." }); + } + [HttpPost("upload-foto-petugas-non-tps")] [IgnoreAntiforgeryToken] public async Task UploadFotoPetugasNonTps( diff --git a/Program.cs b/Program.cs index 57cee76..4a59335 100644 --- a/Program.cs +++ b/Program.cs @@ -10,6 +10,7 @@ builder.Services.AddHttpClient(); // Register custom services builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); var app = builder.Build(); diff --git a/Services/DetailPenjemputanJGCService.cs b/Services/DetailPenjemputanJGCService.cs new file mode 100644 index 0000000..8a35dfa --- /dev/null +++ b/Services/DetailPenjemputanJGCService.cs @@ -0,0 +1,221 @@ +using eSPJ.Models; + +namespace eSPJ.Services +{ + public class DetailPenjemputanJGCService + { + private readonly IDetailPenjemputanStore _store; + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public DetailPenjemputanJGCService( + IWebHostEnvironment env, + IDetailPenjemputanStore store, + ILogger logger) + { + _env = env; + _store = store; + _logger = logger; + } + + private static string SanitizePathSegment(string? value, string fallback = "jgc") + { + var safe = string.Concat((value ?? string.Empty).Trim().Select(c => + char.IsLetterOrDigit(c) || c == '-' || c == '_' + ? c + : '-')); + + while (safe.Contains("--")) + { + safe = safe.Replace("--", "-"); + } + + safe = safe.Trim('-'); + return string.IsNullOrWhiteSpace(safe) ? fallback : safe; + } + + public async Task SubmitPenjemputanAsync(DetailPenjemputanRequest request) + { + try + { + if (string.IsNullOrEmpty(request.TpsName)) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Nama TPS harus diisi" + }; + } + + if (string.IsNullOrEmpty(request.NamaPetugas)) + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Nama petugas harus diisi" + }; + } + + var existingRecord = await GetRecordDetailAsync(request.NomorSpj, request.SpjDetailId, request.LokasiAngkutId, request.TpsName); + + var now = DateTime.Now; + var datePart = now.ToString("yyyy-MM-dd"); + var spjFolder = SanitizePathSegment(request.NomorSpj, "spj-jgc"); + var tpsFolder = SanitizePathSegment(request.TpsName, "tps-jgc"); + var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", datePart, spjFolder, tpsFolder); + var uploadBaseUrl = $"/uploads/penjemputan/{datePart}/{spjFolder}/{tpsFolder}"; + if (!Directory.Exists(uploadPath)) + { + Directory.CreateDirectory(uploadPath); + } + + var tpsData = existingRecord != null + ? CloneRecord(existingRecord) + : new TpsData(); + + tpsData.NomorSpj = request.NomorSpj; + tpsData.LokasiAngkutId = request.LokasiAngkutId; + tpsData.SpjDetailId = request.SpjDetailId; + tpsData.Name = request.TpsName; + tpsData.Latitude = request.Latitude; + tpsData.Longitude = request.Longitude; + tpsData.AlamatJalan = request.AlamatJalan; + tpsData.WaktuKedatangan = request.WaktuKedatangan; + tpsData.NamaPetugas = request.NamaPetugas; + tpsData.IsSubmit = true; + tpsData.SubmittedAt ??= DateTime.Now; + tpsData.UpdatedAt = DateTime.Now; + + if (request.FotoKedatangan != null && request.FotoKedatangan.Any()) + { + tpsData.FotoKedatangan = new List(); + 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($"{uploadBaseUrl}/{fileName}"); + } + tpsData.FotoKedatanganUploaded = tpsData.FotoKedatangan.Count > 0; + } + else if (existingRecord?.FotoKedatangan?.Any() == true) + { + tpsData.FotoKedatangan = new List(existingRecord.FotoKedatangan); + tpsData.FotoKedatanganUploaded = existingRecord.FotoKedatanganUploaded || tpsData.FotoKedatangan.Count > 0; + } + else + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Foto kedatangan harus diupload" + }; + } + + if (request.FotoPetugas != null && request.FotoPetugas.Any()) + { + tpsData.FotoPetugas = new List(); + 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($"{uploadBaseUrl}/{fileName}"); + } + tpsData.FotoPetugasUploaded = tpsData.FotoPetugas.Count > 0; + } + else if (existingRecord?.FotoPetugas?.Any() == true) + { + tpsData.FotoPetugas = new List(existingRecord.FotoPetugas); + tpsData.FotoPetugasUploaded = existingRecord.FotoPetugasUploaded || tpsData.FotoPetugas.Count > 0; + } + else + { + return new DetailPenjemputanResponse + { + Success = false, + Message = "Foto petugas harus diupload" + }; + } + + await _store.SaveSubmittedAsync(tpsData); + + 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 GetRecordDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null) + { + var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim(); + var normalizedSpjDetailId = (spjDetailId ?? string.Empty).Trim(); + var normalizedLokasiAngkutId = (lokasiAngkutId ?? string.Empty).Trim(); + var normalizedNamaTps = (namaTps ?? string.Empty).Trim(); + + var allData = await _store.GetByNomorSpjAsync(normalizedNomorSpj); + return allData + .OrderByDescending(item => item.UpdatedAt) + .FirstOrDefault(item => + (string.IsNullOrWhiteSpace(normalizedSpjDetailId) || string.Equals((item.SpjDetailId ?? string.Empty).Trim(), normalizedSpjDetailId, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrWhiteSpace(normalizedLokasiAngkutId) || string.Equals((item.LokasiAngkutId ?? string.Empty).Trim(), normalizedLokasiAngkutId, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrWhiteSpace(normalizedNamaTps) || string.Equals((item.Name ?? string.Empty).Trim(), normalizedNamaTps, StringComparison.OrdinalIgnoreCase))); + } + + public async Task SaveRecordJGCAsync(RecordSaveRequest request) + { + try + { + await _store.SaveRecordAsync(request); + return new RecordSaveResponse { Success = true, Message = "Data tersimpan." }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving JGC penjemputan record"); + return new RecordSaveResponse { Success = false, Message = $"Gagal menyimpan data: {ex.Message}" }; + } + } + + private static TpsData CloneRecord(TpsData source) + { + return new TpsData + { + NomorSpj = source.NomorSpj, + LokasiAngkutId = source.LokasiAngkutId, + SpjDetailId = source.SpjDetailId, + Name = source.Name, + Index = source.Index, + Latitude = source.Latitude, + Longitude = source.Longitude, + AlamatJalan = source.AlamatJalan, + WaktuKedatangan = source.WaktuKedatangan, + FotoKedatangan = new List(source.FotoKedatangan ?? new List()), + FotoKedatanganUploaded = source.FotoKedatanganUploaded, + FotoPetugas = new List(source.FotoPetugas ?? new List()), + FotoPetugasUploaded = source.FotoPetugasUploaded, + NamaPetugas = source.NamaPetugas, + IsSubmit = source.IsSubmit, + UpdatedAt = source.UpdatedAt, + SubmittedAt = source.SubmittedAt, + }; + } + } +} diff --git a/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiJGC.cshtml b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiJGC.cshtml new file mode 100644 index 0000000..7b1ca23 --- /dev/null +++ b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiJGC.cshtml @@ -0,0 +1,94 @@ +@{ + Layout = "~/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml"; + ViewData["Title"] = "Detail SPJ"; +} + +
+
+
+ + + +

Detail SPJ

+
+
+
+ +
+
+
+
+
+
-
+
-
+
+

-

+

-

+
+
+ +
+
+ +
+
+

No. SPJ

+

-

+
+
+

Plat Nomor

+

-

+
+
+

No. Pintu

+

-

+
+
+

Petugas

+

-

+
+
+
+
+ + +
+ +@section Scripts { + +} diff --git a/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/JGC.cshtml b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/JGC.cshtml new file mode 100644 index 0000000..b2c3a70 --- /dev/null +++ b/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/JGC.cshtml @@ -0,0 +1,94 @@ +@{ + Layout = "~/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml"; + ViewData["Title"] = "Detail Penjemputan - JGC"; +} + +
+ + +
+
+ + + +

JGC

+
+
+
+ +
+
+
+ Plat Nomor + B 9632 TOR +
+
+
+ No. Pintu +

JGC 005

+
+
+
+ +
+
+
+ + JGC + +
+ +
+
+

Perusahaan

+

Jakarta Garden City

+
+ +
+

SPJ/07-2025/PKM/000476

+
+ +
+
+

+ Jalan TB. Simatupang Kav. 7B Cilandak, Jakarta 12430 +

+
+
+ + +
+
+ +
+
+

Form Pengangkutan

+ JGC +
+
+
+
+ + @if (TempData["Success"] != null) + { +
+ @TempData["Success"] +
+ } + + @if (TempData["Error"] != null) + { +
+ @TempData["Error"] +
+ } +
+ + +
+ +@section Scripts { + +} diff --git a/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml b/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml index 55f4a04..790aac7 100644 --- a/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml @@ -192,6 +192,24 @@ + +
+
+
+ Proses + +
+

Jakarta Garden City

+
+ +

Jakarta Garden City, Jakarta

+
+
+ 1 TPS +
+
+
+
diff --git a/wwwroot/driver/css/watch.css b/wwwroot/driver/css/watch.css index c48fbcc..c5a721c 100644 --- a/wwwroot/driver/css/watch.css +++ b/wwwroot/driver/css/watch.css @@ -25,14 +25,12 @@ --color-orange-700: oklch(55.3% 0.195 38.402); --color-orange-800: oklch(47% 0.157 37.304); --color-amber-50: oklch(98.7% 0.022 95.277); - --color-amber-100: oklch(96.2% 0.059 95.617); --color-amber-200: oklch(92.4% 0.12 95.746); --color-amber-300: oklch(87.9% 0.169 91.605); --color-amber-400: oklch(82.8% 0.189 84.429); --color-amber-500: oklch(76.9% 0.188 70.08); --color-amber-600: oklch(66.6% 0.179 58.318); --color-amber-700: oklch(55.5% 0.163 48.998); - --color-amber-800: oklch(47.3% 0.137 46.201); --color-yellow-50: oklch(98.7% 0.026 102.212); --color-yellow-100: oklch(97.3% 0.071 103.193); --color-yellow-200: oklch(94.5% 0.129 101.54); @@ -434,9 +432,6 @@ .right-full { right: 100%; } - .-bottom-0 { - bottom: calc(var(--spacing) * -0); - } .-bottom-0\.5 { bottom: calc(var(--spacing) * -0.5); } @@ -476,9 +471,6 @@ .left-0 { left: calc(var(--spacing) * 0); } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } @@ -800,9 +792,6 @@ .-mr-16 { margin-right: calc(var(--spacing) * -16); } - .mr-1 { - margin-right: calc(var(--spacing) * 1); - } .mr-1\.5 { margin-right: calc(var(--spacing) * 1.5); } @@ -896,9 +885,6 @@ .aspect-square { aspect-ratio: 1 / 1; } - .h-0 { - height: calc(var(--spacing) * 0); - } .h-0\.5 { height: calc(var(--spacing) * 0.5); } @@ -986,9 +972,6 @@ .h-\[250px\] { height: 250px; } - .h-\[340px\] { - height: 340px; - } .h-\[350px\] { height: 350px; } @@ -1166,10 +1149,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); @@ -1182,10 +1161,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); @@ -1198,12 +1173,6 @@ --tw-translate-y: calc(var(--spacing) * 12); translate: var(--tw-translate-x) var(--tw-translate-y); } - .scale-105 { - --tw-scale-x: 105%; - --tw-scale-y: 105%; - --tw-scale-z: 105%; - scale: var(--tw-scale-x) var(--tw-scale-y); - } .scale-110 { --tw-scale-x: 110%; --tw-scale-y: 110%; @@ -1318,13 +1287,6 @@ .gap-6 { gap: calc(var(--spacing) * 6); } - .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; @@ -1774,9 +1736,6 @@ .bg-blue-800 { background-color: var(--color-blue-800); } - .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)) { @@ -1786,9 +1745,6 @@ .bg-emerald-400 { background-color: var(--color-emerald-400); } - .bg-emerald-600 { - background-color: var(--color-emerald-600); - } .bg-gray-50 { background-color: var(--color-gray-50); } @@ -1846,9 +1802,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)) { @@ -1870,9 +1823,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)) { @@ -1903,12 +1853,6 @@ .bg-slate-100 { background-color: var(--color-slate-100); } - .bg-slate-100\/80 { - background-color: color-mix(in srgb, oklch(96.8% 0.007 247.896) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-100) 80%, transparent); - } - } .bg-slate-200 { background-color: var(--color-slate-200); } @@ -1921,9 +1865,6 @@ .bg-slate-900 { background-color: var(--color-slate-900); } - .bg-slate-950 { - background-color: var(--color-slate-950); - } .bg-slate-950\/60 { background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 60%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1960,12 +1901,6 @@ background-color: color-mix(in oklab, var(--color-white) 70%, transparent); } } - .bg-white\/80 { - background-color: color-mix(in srgb, #fff 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 80%, transparent); - } - } .bg-white\/95 { background-color: color-mix(in srgb, #fff 95%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1981,12 +1916,6 @@ .bg-yellow-400 { background-color: var(--color-yellow-400); } - .bg-yellow-400\/20 { - background-color: color-mix(in srgb, oklch(85.2% 0.199 91.936) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-yellow-400) 20%, transparent); - } - } .bg-yellow-500 { background-color: var(--color-yellow-500); } @@ -2078,13 +2007,6 @@ --tw-gradient-from: var(--color-white); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } - .from-white\/85 { - --tw-gradient-from: color-mix(in srgb, #fff 85%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-from: color-mix(in oklab, var(--color-white) 85%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .from-yellow-50 { --tw-gradient-from: var(--color-yellow-50); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -2136,14 +2058,6 @@ --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } - .via-white\/75 { - --tw-gradient-via: color-mix(in srgb, #fff 75%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-via: color-mix(in oklab, var(--color-white) 75%, transparent); - } - --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-via-stops); - } .to-amber-500 { --tw-gradient-to: var(--color-amber-500); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -2152,17 +2066,6 @@ --tw-gradient-to: var(--color-amber-600); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } - .to-blue-50 { - --tw-gradient-to: var(--color-blue-50); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .to-blue-50\/70 { - --tw-gradient-to: color-mix(in srgb, oklch(97% 0.014 254.604) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-to: color-mix(in oklab, var(--color-blue-50) 70%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .to-blue-200 { --tw-gradient-to: var(--color-blue-200); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -2813,9 +2716,6 @@ .text-yellow-200 { color: var(--color-yellow-200); } - .text-yellow-300 { - color: var(--color-yellow-300); - } .text-yellow-500 { color: var(--color-yellow-500); } @@ -2936,9 +2836,6 @@ --tw-shadow-color: color-mix(in oklab, var(--color-gray-200) var(--tw-shadow-alpha), transparent); } } - .ring-black { - --tw-ring-color: var(--color-black); - } .ring-black\/5 { --tw-ring-color: color-mix(in srgb, #000 5%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -3268,14 +3165,6 @@ } } } - .hover\:-translate-y-1 { - &:hover { - @media (hover: hover) { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - } .hover\:scale-105 { &:hover { @media (hover: hover) { @@ -3359,13 +3248,6 @@ } } } - .hover\:bg-emerald-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-emerald-700); - } - } - } .hover\:bg-gray-50 { &:hover { @media (hover: hover) { diff --git a/wwwroot/driver/js/detail-penjemputan-jgc.js b/wwwroot/driver/js/detail-penjemputan-jgc.js new file mode 100644 index 0000000..d0da622 --- /dev/null +++ b/wwwroot/driver/js/detail-penjemputan-jgc.js @@ -0,0 +1,1256 @@ +document.addEventListener('DOMContentLoaded', async function() { + const tpsContentContainer = document.getElementById('tps-content'); + + let activeTpsIndex = 0; + let tpsData = []; + let nomorSpj = 'SPJ/07-2025/PKM/000476'; + const RECORD_DETAIL_ENDPOINT = '/upst/detail-penjemputan/api/records/detail'; + + let autoSaveTimer = null; + let autoSaveStatusEl = null; + let loadingOverlayEl = null; + let isAutoSaving = false; + let pendingAutoSave = false; + let lastAutoSaveSignature = ""; + + function getLoadingOverlay() { + if (loadingOverlayEl && document.body.contains(loadingOverlayEl)) { + return loadingOverlayEl; + } + + loadingOverlayEl = document.createElement('div'); + loadingOverlayEl.id = 'detail-loading-overlay'; + loadingOverlayEl.className = 'fixed inset-0 z-[9999] hidden bg-white/95 backdrop-blur-sm flex items-center justify-center px-6'; + loadingOverlayEl.innerHTML = ` +
+
+

Sedang memuat data

+

Mohon tunggu sebentar, data penjemputan sedang dipulihkan.

+
+ `; + document.body.appendChild(loadingOverlayEl); + return loadingOverlayEl; + } + + function showLoadingOverlay() { + const overlay = getLoadingOverlay(); + overlay.classList.remove('hidden'); + document.body.style.overflow = 'hidden'; + } + + function hideLoadingOverlay() { + const overlay = getLoadingOverlay(); + overlay.classList.add('hidden'); + document.body.style.overflow = ''; + } + + function scheduleAutoSave() { + clearTimeout(autoSaveTimer); + showAutoSaveStatus('menyimpan...'); + autoSaveTimer = setTimeout(autoSaveRecord, 500); + } + + function showAutoSaveStatus(msg, isOk = false) { + if (!autoSaveStatusEl) { + autoSaveStatusEl = document.getElementById('auto-save-status'); + } + if (!autoSaveStatusEl) return; + autoSaveStatusEl.textContent = msg; + autoSaveStatusEl.className = isOk + ? 'text-[11px] text-green-600 text-center font-medium transition-opacity' + : 'text-[11px] text-amber-500 text-center font-medium transition-opacity'; + autoSaveStatusEl.style.opacity = '1'; + if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500); + } + + function buildAutoSavePayload(tps) { + return { + nomorSpj: nomorSpj || '', + namaTps: tps.name || DEFAULT_TPS_NAME, + lokasiAngkutId: tps.lokasiAngkutId || '', + spjDetailId: tps.spjDetailId || '', + latitude: tps.latitude || '', + longitude: tps.longitude || '', + alamatJalan: tps.alamatJalan || '', + waktuKedatangan: tps.waktuKedatangan || '', + fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [], + fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false, + fotoPetugasFileNames: tps.fotoPetugasFileNames || [], + fotoPetugasUploaded: tps.fotoPetugasUploaded || false, + namaPetugas: tps.namaPetugas || '' + }; + } + + async function autoSaveRecord() { + const tps = tpsData[activeTpsIndex]; + if (!tps) return; + if (tps.submitted) return; + if (isAutoSaving) { + pendingAutoSave = true; + return; + } + + const payload = buildAutoSavePayload(tps); + const payloadSignature = JSON.stringify(payload); + if (payloadSignature === lastAutoSaveSignature) { + showAutoSaveStatus('✓ Data tersimpan', true); + return; + } + + try { + isAutoSaving = true; + const res = await fetch('/upst/detail-penjemputan/save-record-jgc', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: payloadSignature + }); + const data = await res.json(); + if (data.success) { + lastAutoSaveSignature = payloadSignature; + } + showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success); + } catch { + showAutoSaveStatus('✗ Gagal simpan data'); + } finally { + isAutoSaving = false; + if (pendingAutoSave) { + pendingAutoSave = false; + scheduleAutoSave(); + } + } + } + + function normalizeStringList(value) { + if (!Array.isArray(value)) { + return []; + } + + return value.filter((item) => typeof item === 'string' && item.trim()); + } + + function normalizeJenisSampahValue(value) { + if (Array.isArray(value)) { + return normalizeJenisSampahValue(value[0]); + } + + if (typeof value === 'number') { + return JENIS_SAMPAH[value] || DEFAULT_JENIS; + } + + if (typeof value === 'string' && value.trim()) { + const trimmed = value.trim(); + const asNumber = Number(trimmed); + if (!Number.isNaN(asNumber) && String(asNumber) === trimmed) { + return JENIS_SAMPAH[asNumber] || DEFAULT_JENIS; + } + + const matched = JENIS_SAMPAH.find( + (item) => item.toLowerCase() === trimmed.toLowerCase(), + ); + return matched || DEFAULT_JENIS; + } + + return DEFAULT_JENIS; + } + + function applyServerRecordToTps(record) { + if (!record) return; + + const tps = tpsData[activeTpsIndex]; + if (!tps) return; + const fotoKedatangan = normalizeStringList( + record.fotoKedatanganFileNames || record.fotoKedatangan || record.FotoKedatangan, + ); + const fotoPetugas = normalizeStringList( + record.fotoPetugasFileNames || record.fotoPetugas || record.FotoPetugas, + ); + + tps.name = record.namaTps || record.name || record.Name || tps.name || DEFAULT_TPS_NAME; + tps.lokasiAngkutId = record.lokasiAngkutId || record.LokasiAngkutID || tps.lokasiAngkutId; + tps.spjDetailId = record.spjDetailId || record.SpjDetailID || tps.spjDetailId; + tps.latitude = record.latitude || record.Latitude || tps.latitude; + tps.longitude = record.longitude || record.Longitude || tps.longitude; + tps.alamatJalan = record.alamatJalan || record.AlamatJalan || tps.alamatJalan; + tps.waktuKedatangan = record.waktuKedatangan || record.WaktuKedatangan || tps.waktuKedatangan; + tps.fotoKedatangan = []; + tps.fotoKedatanganFileNames = fotoKedatangan; + tps.fotoKedatanganUploaded = Boolean(record.fotoKedatanganUploaded ?? record.FotoKedatanganUploaded) || fotoKedatangan.length > 0; + tps.fotoPetugas = []; + tps.fotoPetugasFileNames = fotoPetugas; + tps.fotoPetugasUploaded = Boolean(record.fotoPetugasUploaded ?? record.FotoPetugasUploaded) || fotoPetugas.length > 0; + tps.namaPetugas = record.namaPetugas || record.NamaPetugas || tps.namaPetugas; + tps.submitted = Boolean(record.isSubmit ?? record.IsSubmit ?? record.submitted ?? record.Submitted ?? tps.submitted); + } + + async function loadRecordForCurrentSpj() { + const tps = tpsData[activeTpsIndex]; + if (!tps || !nomorSpj) return; + + const params = new URLSearchParams({ nomorSpj }); + if (tps.spjDetailId) params.set('spjDetailId', tps.spjDetailId); + if (tps.lokasiAngkutId) params.set('lokasiAngkutId', tps.lokasiAngkutId); + if (tps.name) params.set('namaTps', tps.name); + + try { + const res = await fetch(`${RECORD_DETAIL_ENDPOINT}?${params.toString()}`, { cache: 'no-store' }); + if (!res.ok) return; + + const data = await res.json(); + if (!data.success || !data.hasData || !data.item) return; + + applyServerRecordToTps(data.item); + } catch (error) { + console.warn('Gagal memuat data non-TPS:', error); + } + } + + const OCR_AREAS = [ + { + id: "A", + x: 0.34, + y: 0.35, + w: 0.4, + h: 0.11, + color: "border-lime-400 bg-lime-500/15", + }, + { + id: "B", + x: 0.31, + y: 0.33, + w: 0.45, + h: 0.14, + color: "border-amber-300 bg-amber-400/10", + }, + { + id: "C", + x: 0.29, + y: 0.31, + w: 0.49, + h: 0.17, + color: "border-cyan-300 bg-cyan-400/10", + }, + ]; + const JENIS_SAMPAH = ["Organik", "Anorganik", "Residu"]; + const DEFAULT_JENIS = "Residu"; + const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-jgc.json"; + const DEFAULT_TPS_NAME = "TPS JGC"; + + function isBrowserFile(file) { + return file instanceof File; + } + function resolveStoredPhoto(file) { + return isBrowserFile(file) ? file : null; + } + + function getStoredPhotoUrl(file) { + if (isBrowserFile(file)) return URL.createObjectURL(file); + if (typeof file === 'string') return file; + return ''; + } + + function getStoredPhotoName(file, defaultName = 'Foto') { + if (isBrowserFile(file)) return file.name; + if (typeof file === 'string') return file.split('/').pop() || defaultName; + return defaultName; + } + + function getStoredPhotoSize(file) { + if (isBrowserFile(file)) return file.size; + return 0; + } + + function initializeLocation() { + tpsData = [{ + name: DEFAULT_TPS_NAME, + index: 0, + lokasiAngkutId: '', + spjDetailId: '', + latitude: '', + longitude: '', + alamatJalan: '', + waktuKedatangan: '', + fotoKedatangan: [], + fotoKedatanganFileNames: [], + fotoKedatanganUploaded: false, + fotoPetugas: [], + fotoPetugasFileNames: [], + fotoPetugasUploaded: false, + namaPetugas: '', + submitted: false + }]; + } + + async function loadDetailData() { + try { + const response = await fetch(DETAIL_DATA_URL, { cache: "no-store" }); + if (!response.ok) { + return; + } + + const payload = await response.json(); + const detail = payload.detailPenjemputan || payload; + const namaTps = + detail.namaTps || detail.tpsName || detail.name || DEFAULT_TPS_NAME; + const namaPerusahaan = detail.namaPerusahaan || detail.companyName || ""; + + if (tpsData[0]) { + tpsData[0].name = namaTps; + tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId; + tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId; + } + + nomorSpj = detail.nomorSpj || nomorSpj; + applyDetailDataToView(detail, namaTps, namaPerusahaan); + const _form = tpsContentContainer.querySelector('form'); + if (_form) { + const lokasiInput = _form.querySelector('.tps-lokasi-angkut-id'); + const spjInput = _form.querySelector('.tps-spj-detail-id'); + if (lokasiInput) lokasiInput.value = tpsData[0].lokasiAngkutId; + if (spjInput) spjInput.value = tpsData[0].spjDetailId; + } + } catch (error) { + console.warn('Gagal memuat detail penjemputan non-TPS:', error); + } + } + + function applyDetailDataToView(detail, namaTps, namaPerusahaan) { + const titleEl = document.getElementById("detail-page-title"); + const badgeEl = document.getElementById("detail-tps-badge"); + const formBadgeEl = document.getElementById("detail-form-badge"); + const companyEl = document.getElementById("detail-company-name"); + const spjEl = document.getElementById("detail-spj-number"); + const addressEl = document.getElementById("detail-address"); + const platEl = document.getElementById("plat-nomor"); + const doorEl = document.getElementById("detail-nomor-pintu"); + + if (titleEl) titleEl.textContent = namaTps; + if (badgeEl) badgeEl.textContent = namaTps; + if (formBadgeEl) formBadgeEl.textContent = namaTps; + if (companyEl && namaPerusahaan) companyEl.textContent = namaPerusahaan; + if (spjEl && detail.nomorSpj) spjEl.textContent = detail.nomorSpj; + if (addressEl && detail.alamat) addressEl.textContent = detail.alamat; + if (platEl && detail.platNomor) platEl.textContent = detail.platNomor; + if (doorEl && detail.nomorPintu) doorEl.textContent = detail.nomorPintu; + document.title = `Detail Penjemputan - ${namaTps}`; + } + + function renderTpsForm() { + const tps = tpsData[activeTpsIndex]; + const submitState = getSubmitState(tps); + + const actionMarkup = tps.submitted + ? ` +
+ + + + + Data ${tps.name || DEFAULT_TPS_NAME} sudah disubmit + +
` + : ` +
+ Batal + +
+ ${submitState.canSubmit ? '' : `

${submitState.message}

`} + `; + + tpsContentContainer.innerHTML = ` +
+ + + + + + +
+
+
1
+
+

Foto Kedatangan

+

Upload foto kedatangan

+
+
+ + + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
${getKedatanganUploadStateMarkup(tps)}
+
+ +
+
+
2
+
+

Foto Petugas

+

Upload dokumentasi petugas

+
+
+ + + + + +
+ + +
+ +
${getPetugasUploadStateMarkup(tps)}
+
+ + ${actionMarkup} +

+
+ `; + + attachTpsFormListeners(); + restorePhotoPreview(); + } + + function restorePhotoPreview() { + const tps = tpsData[activeTpsIndex]; + const form = tpsContentContainer.querySelector("form"); + if (!form) return; + + const previewWrapKedatangan = form.querySelector('.tps-preview-kedatangan-wrap'); + const previewImgKedatangan = form.querySelector('.tps-preview-kedatangan-img'); + if (previewWrapKedatangan && previewImgKedatangan) { + let imgSrc = ''; + if (Array.isArray(tps.fotoKedatangan) && tps.fotoKedatangan.length > 0) { + const localUrl = getStoredPhotoUrl(tps.fotoKedatangan[0]); + if (localUrl) { + imgSrc = localUrl; + if (isBrowserFile(resolveStoredPhoto(tps.fotoKedatangan[0]))) { + previewImgKedatangan.onload = () => URL.revokeObjectURL(localUrl); + } + } + } else if (tps.fotoKedatanganUploaded && Array.isArray(tps.fotoKedatanganFileNames) && tps.fotoKedatanganFileNames.length > 0) { + imgSrc = tps.fotoKedatanganFileNames[0]; + } + if (imgSrc) { + previewImgKedatangan.src = imgSrc; + previewWrapKedatangan.classList.remove('hidden'); + } + } + + const previewWrapPetugas = form.querySelector('.tps-preview-petugas-wrap'); + const previewImgPetugas = form.querySelector('.tps-preview-petugas-img'); + if (previewWrapPetugas && previewImgPetugas) { + let imgSrc = ''; + if (Array.isArray(tps.fotoPetugas) && tps.fotoPetugas.length > 0) { + const localUrl = getStoredPhotoUrl(tps.fotoPetugas[0]); + if (localUrl) { + imgSrc = localUrl; + if (isBrowserFile(resolveStoredPhoto(tps.fotoPetugas[0]))) { + previewImgPetugas.onload = () => URL.revokeObjectURL(localUrl); + } + } + } else if (tps.fotoPetugasUploaded && Array.isArray(tps.fotoPetugasFileNames) && tps.fotoPetugasFileNames.length > 0) { + imgSrc = tps.fotoPetugasFileNames[0]; + } + if (imgSrc) { + previewImgPetugas.src = imgSrc; + previewWrapPetugas.classList.remove('hidden'); + } + } + } + + function renderStoredPhotos(files, container) { + container.innerHTML = ""; + container.className = "space-y-2"; + + files.forEach((file, index) => { + const item = document.createElement("div"); + item.className = + "rounded-xl border border-gray-200 overflow-hidden bg-black"; + + const imageUrl = getStoredPhotoUrl(file); + const safeName = getStoredPhotoName(file, `Foto ${index + 1}`).replace( + /"/g, + """, + ); + const fileSize = getStoredPhotoSize(file); + item.innerHTML = ` +
+ Preview ${index + 1} +
+ `; + + const img = item.querySelector(".preview-multi-image"); + if (img && isBrowserFile(resolveStoredPhoto(file))) { + img.onload = function () { + URL.revokeObjectURL(imageUrl); + }; + } + container.appendChild(item); + }); + } + + function attachTpsFormListeners() { + const form = tpsContentContainer.querySelector("form"); + const tps = tpsData[activeTpsIndex]; + + if (tps.submitted) return; + + const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan"); + const fotoPetugasInput = form.querySelector(".tps-foto-petugas"); + const namaPetugasInput = form.querySelector(".tps-nama-petugas"); + + fotoKedatanganInput.addEventListener('change', function() { + if (!this.files || !this.files[0]) return; + const currentTps = tpsData[activeTpsIndex]; + const file = this.files[0]; + + const previewWrap = form.querySelector('.tps-preview-kedatangan-wrap'); + const previewImg = form.querySelector('.tps-preview-kedatangan-img'); + if (previewWrap && previewImg) { + const localUrl = URL.createObjectURL(file); + previewImg.src = localUrl; + previewWrap.classList.remove('hidden'); + previewImg.onload = () => URL.revokeObjectURL(localUrl); + } + + currentTps.fotoKedatangan = [file]; + currentTps.fotoKedatanganFileNames = []; + currentTps.fotoKedatanganUploaded = false; + + updateWaktuKedatangan(); + refreshKedatanganUploadState(form); + }); + + fotoPetugasInput.addEventListener('change', function() { + if (!this.files || !this.files[0]) return; + const currentTps = tpsData[activeTpsIndex]; + const file = this.files[0]; + + const previewWrap = form.querySelector('.tps-preview-petugas-wrap'); + const previewImg = form.querySelector('.tps-preview-petugas-img'); + if (previewWrap && previewImg) { + const localUrl = URL.createObjectURL(file); + previewImg.src = localUrl; + previewWrap.classList.remove('hidden'); + previewImg.onload = () => URL.revokeObjectURL(localUrl); + } + + currentTps.fotoPetugas = [file]; + currentTps.fotoPetugasFileNames = []; + currentTps.fotoPetugasUploaded = false; + + refreshPetugasUploadState(form); + }); + + namaPetugasInput.addEventListener('input', function() { + tpsData[activeTpsIndex].namaPetugas = this.value; + refreshPetugasUploadState(form); + scheduleAutoSave(); + }); + + namaPetugasInput.addEventListener('blur', function() { + tpsData[activeTpsIndex].namaPetugas = this.value; + scheduleAutoSave(); + }); + + const btnUploadKedatangan = form.querySelector( + ".tps-btn-upload-kedatangan", + ); + if (btnUploadKedatangan) { + btnUploadKedatangan.addEventListener("click", uploadFotoKedatangan); + } + + const btnUploadPetugas = form.querySelector(".tps-btn-upload-petugas"); + if (btnUploadPetugas) { + btnUploadPetugas.addEventListener("click", uploadFotoPetugas); + } + + form.addEventListener("submit", function (e) { + e.preventDefault(); + submitTpsData(); + }); + } + + function 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 = ` +
+ Preview ${index + 1} +
+ `; + + 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, + ); + }); + } + + function getKedatanganUploadStateMarkup(tps) { + if (tps.fotoKedatanganUploaded) { + return '
✓ Foto kedatangan sudah diupload
'; + } + if (!tps.fotoKedatangan.length) { + return ""; + } + return ``; + } + + function refreshKedatanganUploadState(form) { + const stateContainer = form.querySelector(".kedatangan-upload-state"); + if (!stateContainer) return; + + const tps = tpsData[activeTpsIndex]; + stateContainer.innerHTML = getKedatanganUploadStateMarkup(tps); + + const btnUploadKedatangan = stateContainer.querySelector( + ".tps-btn-upload-kedatangan", + ); + if (btnUploadKedatangan) { + btnUploadKedatangan.addEventListener("click", uploadFotoKedatangan); + } + + refreshSubmitButtonState(form); + } + + function getPetugasUploadStateMarkup(tps) { + if (tps.fotoPetugasUploaded) { + return '
✓ Foto petugas sudah diupload
'; + } + + if (!tps.fotoPetugas.length) { + return ""; + } + + if (!tps.namaPetugas.trim()) { + return ` + +

Isi nama petugas terlebih dahulu

+ `; + } + + return ``; + } + + function refreshPetugasUploadState(form) { + const stateContainer = form.querySelector(".petugas-upload-state"); + if (!stateContainer) return; + + const tps = tpsData[activeTpsIndex]; + stateContainer.innerHTML = getPetugasUploadStateMarkup(tps); + + const btnUploadPetugas = stateContainer.querySelector( + ".tps-btn-upload-petugas:not([disabled])", + ); + if (btnUploadPetugas) { + btnUploadPetugas.addEventListener("click", uploadFotoPetugas); + } + + refreshSubmitButtonState(form); + } + + function getSubmitState(tps) { + if (tps?.submitted) { + return { canSubmit: false, message: '' }; + } + + if (!tps.fotoKedatanganUploaded) { + if (!tps.fotoKedatangan.length) + return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan.' }; + return { canSubmit: false, message: 'Silakan upload foto kedatangan terlebih dahulu.' }; + } + + if (!tps.fotoPetugasUploaded) { + if (!tps.fotoPetugas.length) + return { canSubmit: false, message: 'Silakan pilih dan upload foto petugas.' }; + return { canSubmit: false, message: 'Silakan upload foto petugas terlebih dahulu.' }; + } + + if (!tps.namaPetugas.trim()) { + return { + canSubmit: false, + message: "Isi nama petugas dulu sebelum submit.", + }; + } + + return { canSubmit: true, message: "" }; + } + + function refreshSubmitButtonState(form) { + const submitButton = form.querySelector('button[type="submit"]'); + const tps = tpsData[activeTpsIndex]; + if (tps?.submitted || !submitButton) return; + + const helperText = form.querySelector(".submit-state-message"); + const submitState = getSubmitState(tps); + + submitButton.disabled = !submitState.canSubmit; + submitButton.className = `w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`; + + let messageEl = helperText; + if (!messageEl) { + messageEl = document.createElement("p"); + messageEl.className = + "submit-state-message text-[11px] text-center text-red-500 font-medium"; + submitButton + .closest(".flex.gap-3") + ?.insertAdjacentElement("afterend", messageEl); + } + + if (submitState.canSubmit) { + messageEl.textContent = ""; + messageEl.classList.add("hidden"); + } else { + messageEl.textContent = submitState.message; + messageEl.classList.remove("hidden"); + } + } + + function buildSubmitFormData(tps) { + const formData = new FormData(); + formData.append("NomorSpj", nomorSpj || ""); + formData.append("TpsName", tps.name || DEFAULT_TPS_NAME); + formData.append("LokasiAngkutId", tps.lokasiAngkutId || ""); + formData.append("SpjDetailId", tps.spjDetailId || ""); + formData.append("Latitude", tps.latitude); + formData.append("Longitude", tps.longitude); + formData.append("AlamatJalan", tps.alamatJalan); + formData.append("WaktuKedatangan", tps.waktuKedatangan); + formData.append("NamaPetugas", tps.namaPetugas); + + tps.fotoKedatangan.forEach((file) => { + if (isBrowserFile(file)) { + formData.append("FotoKedatangan", file); + } + }); + tps.fotoPetugas.forEach((file) => { + if (isBrowserFile(file)) { + formData.append("FotoPetugas", file); + } + }); + + const antiForgeryTokenEl = document.querySelector( + '#upst-antiforgery input[name="__RequestVerificationToken"]', + ); + if (antiForgeryTokenEl && antiForgeryTokenEl.value) { + formData.append("__RequestVerificationToken", antiForgeryTokenEl.value); + } + + return formData; + } + + async function uploadFotoKedatangan() { + const tps = tpsData[activeTpsIndex]; + if (tps.fotoKedatangan.length === 0) { + showToast('Belum ada foto kedatangan yang dipilih!', 'error'); + return; + } + + const form = tpsContentContainer.querySelector('form'); + const btn = form ? form.querySelector('.tps-btn-upload-kedatangan') : null; + if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; } + + const formData = new FormData(); + tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f)); + formData.append('NomorSpj', nomorSpj || ''); + formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); + formData.append('Latitude', tps.latitude || ''); + formData.append('Longitude', tps.longitude || ''); + formData.append('AlamatJalan', tps.alamatJalan || ''); + + try { + const res = await fetch('/upst/detail-penjemputan/upload-foto-kedatangan-jgc', { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.fotoKedatanganUploaded = true; + tps.fotoKedatanganFileNames = data.fileUrls || data.fileNames || []; + showToast(data.message || 'Foto kedatangan berhasil diupload.', 'success'); + if (form) { + const fotoInput = form.querySelector('.tps-foto-kedatangan'); + if (fotoInput) fotoInput.value = ''; + const previewWrap = form.querySelector('.tps-preview-kedatangan-wrap'); + const previewImg = form.querySelector('.tps-preview-kedatangan-img'); + if (previewWrap && previewImg && tps.fotoKedatanganFileNames.length > 0) { + previewImg.src = tps.fotoKedatanganFileNames[0]; + previewWrap.classList.remove('hidden'); + } + refreshKedatanganUploadState(form); + } + scheduleAutoSave(); + } else { + showToast(data.message || 'Gagal upload foto kedatangan.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; } + } + } catch { + showToast('Koneksi gagal saat upload foto kedatangan.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; } + } + } + + async function uploadFotoPetugas() { + const tps = tpsData[activeTpsIndex]; + if (tps.fotoPetugas.length === 0) { + showToast('Belum ada foto petugas yang dipilih!', 'error'); + return; + } + if (!tps.namaPetugas.trim()) { + showToast('Nama petugas wajib diisi sebelum upload foto petugas!', 'error'); + return; + } + + const form = tpsContentContainer.querySelector('form'); + const btn = form ? form.querySelector('.tps-btn-upload-petugas:not([disabled])') : null; + if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; } + + const formData = new FormData(); + tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f)); + formData.append('NomorSpj', nomorSpj || ''); + formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('NamaPetugas', tps.namaPetugas); + + try { + const res = await fetch('/upst/detail-penjemputan/upload-foto-petugas-jgc', { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.fotoPetugasUploaded = true; + tps.fotoPetugasFileNames = data.fileUrls || data.fileNames || []; + showToast(data.message || 'Foto petugas berhasil diupload.', 'success'); + if (form) { + const fotoInput = form.querySelector('.tps-foto-petugas'); + if (fotoInput) fotoInput.value = ''; + const previewWrap = form.querySelector('.tps-preview-petugas-wrap'); + const previewImg = form.querySelector('.tps-preview-petugas-img'); + if (previewWrap && previewImg && tps.fotoPetugasFileNames.length > 0) { + previewImg.src = tps.fotoPetugasFileNames[0]; + previewWrap.classList.remove('hidden'); + } + refreshPetugasUploadState(form); + } + scheduleAutoSave(); + } else { + showToast(data.message || 'Gagal upload foto petugas.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; } + } + } catch { + showToast('Koneksi gagal saat upload foto petugas.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; } + } + } + + async function submitTpsData() { + const tps = tpsData[activeTpsIndex]; + const submitState = getSubmitState(tps); + if (!submitState.canSubmit) { showToast(submitState.message, 'error'); return; } + + const form = tpsContentContainer.querySelector('form'); + const submitBtn = form ? form.querySelector('button[type="submit"]') : null; + if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Menyimpan...'; } + + const formData = buildSubmitFormData(tps); + + try { + const res = await fetch('/upst/detail-penjemputan/jgc-submit', { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + }, + body: formData + }); + const result = await res.json().catch(() => null); + + if (res.ok && result?.success) { + tps.submitted = true; + showToast(result.message || 'Data berhasil disimpan!', 'success'); + setTimeout(() => { window.location.href = '/upst/detail-penjemputan/detail-selesai-jgc'; }, 1500); + } else { + showToast(result?.message || 'Gagal submit data.', 'error'); + if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; } + } + } catch { + showToast('Koneksi gagal saat submit.', 'error'); + if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; } + } + } + + function showToast(message, type = 'info') { + let container = document.getElementById('espj-toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'espj-toast-container'; + container.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;'; + document.body.appendChild(container); + } + const toast = document.createElement('div'); + const bg = type === 'success' ? '#16a34a' : type === 'error' ? '#dc2626' : '#2563eb'; + toast.style.cssText = `background:${bg};color:#fff;padding:10px 18px;border-radius:16px;font-size:13px;font-weight:600;box-shadow:0 4px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;text-align:center;`; + toast.textContent = message; + container.appendChild(toast); + requestAnimationFrame(() => { toast.style.opacity = '1'; }); + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3200); + } + + const nomorSpjEl = document.querySelector(".text-gray-600.font-mono"); + if (nomorSpjEl && nomorSpjEl.textContent) { + nomorSpj = nomorSpjEl.textContent.trim(); + } + + showLoadingOverlay(); + try { + initializeLocation(); + await loadDetailData(); + await loadRecordForCurrentSpj(); + renderTpsForm(); + } finally { + hideLoadingOverlay(); + } + + function renderServerImagePreview(fileUrls, container) { + container.innerHTML = ''; + if (!fileUrls || fileUrls.length === 0) return; + container.className = 'space-y-2'; + fileUrls.forEach((url, index) => { + const item = document.createElement('div'); + item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black'; + const isUrl = typeof url === 'string' && (url.startsWith('/') || url.startsWith('http')); + if (isUrl) { + item.innerHTML = ` +
+ Foto ${index + 1} +
+ `; + } else { + item.className = 'rounded-xl border border-green-200 bg-green-50 h-44 flex items-center justify-center'; + item.innerHTML = `

✓ Foto ${index + 1}

`; + } + container.appendChild(item); + }); + } +}); diff --git a/wwwroot/driver/json/detail-penjemputan-jgc.json b/wwwroot/driver/json/detail-penjemputan-jgc.json new file mode 100644 index 0000000..e69de29 diff --git a/wwwroot/driver/json/selesai-jgc.json b/wwwroot/driver/json/selesai-jgc.json new file mode 100644 index 0000000..ef9c0f9 --- /dev/null +++ b/wwwroot/driver/json/selesai-jgc.json @@ -0,0 +1,15 @@ +{ + "selesai": { + "namaPerusahaan": "Jakarta Garden City", + "alamat": "Jalan TB. Simatupang Kav. 7B Cilandak, Jakarta 12430 ", + "tanggalSelesai": "2026-03-10T10:35:00", + "status": "Selesai", + "nomorSpj": "SPJ/07-2025/PKM/000477", + "platNomor": "B 9041 UPT", + "nomorPintu": "JGC 001", + "petugas": { + "driver": "Andri Saputra", + "checker": "Nabila Putri" + } + } +}